diff --git a/.gitignore b/.gitignore index a6318e378e3..c5be8f38512 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,9 @@ stack-dev.yaml # HIE db files (e.g. generated for stan) *.hie +# dump timings +*.dump-timings + # generated files under .local .local diff --git a/CHANGELOG.md b/CHANGELOG.md index 3de95aec62e..021decd6505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,318 @@ +# [2024-10-30] (Chart Release 5.6.0) + +## Release notes + + +* To remove phone keys from brig's `user_keys` table an ad hoc data-migration can be run. See PR https://github.com/wireapp/wire-server/pull/4146 which contains the implementation. (#4130) + +* Because the `phone` column is deleted from Brig's `user` table in a schema + migration, temporarily there might be 5xx errors during deployment if Wire + server 5.4.0 was not deployed previously. To avoid these errors, please deploy + the Wire server 5.4.0 release first. (#4130) + +* With this release it will be possible to invite personal users to teams. In `brig`'s config, `emailSMS.team.tExistingUserInvitationUrl` is required to be set to a value that points to the correct teams/account page. + If `emailSMS.team` is not defined at all in the current environment, the value of `externalUrls.teamSettings` (or, if not present, `externalUrls.nginz`) will be used to construct the correct url, and no configuration change is necessary. (#4229) + +* charts/wire-server: There is a new config value called `background-worker.config.enableFederation` which defaults to `false`. This must be kept in sync with `tags.federation`. (#4243) + +* If you are mapping an email address to the `externalId` field in the + scim schema, please check the following list for items that apply to + you and recommended steps before/during/after upgrade. + + - **Situation:** the `emails` field of in your scim user records is + empty. + + **What you need to do:** change your schema mapping to contain the + same address in `externalId` and (as a record with one element) in + `emails`. + + - **Situation:** the `emails` field of your scim user records is + non-empty. + + **What you need to do:** make sure `emails` contains exactly one + entry, which is the email from `externalId`. If there is a + discrepancy, the address from `emails` will become the new + (unvalidated) address of the user, and the user will receive an + email to validate it. If the email cannot be sent or is ignored + by the recipient, the *valid* address will not be changed. (#4221) + +* A schema migration drops column 'phone' from Brig's 'team_invitation' table. Previous releases were still reading this column. As there is no Team Settings UI action to enter a phone number, this reading will not miss to read actual phone numbers. Therefore, during deployment this will lead to benign 5xx errors. (#4149) + +* Password hashing can now be done using argon2id instead of scrypt. The argon2id parameters can be configured using these options: + + ```yaml + brig: + optSettings: + setPasswordHashingOptions: + algorithm: argon2id + iterations: ... + memory: ... # memory needed in KiB + parallelism: ... + galley: + settings: + passwordHashingOptions: + algorithm: argon2id + iterations: ... + memory: ... # memory needed in KiB + parallelism: ... + ``` + + The default option is still to use scrypt as moving to argon2id might require + allocating more resources according to configured parameters. + + When configured to use argon2id, the DB will be migrated slowly over time as the + users enter their passwords (either to login or to do other operations which + require explicit password entry). This migration is **NOT** done in reverse, + i.e., if a deployment started with argon2id as the algorithm then chose to move + to scrypt, the passwords will not get rehashed automatically, instead the users + will have to reset their passwords if that is desired. + + **NOTE** It is highly recommended to move to argon2id as it will be made the + only available choice for the `algorithm` config option in future. + + (#4291, #4291) + +* Config value `gundeck.config.bulkPush` has been removed. This is purely an + internal change, in case the value was overriden to `false`, operators might see + more spiky usage of CPU and memory from gundeck due to bulk processing. (#4290) + + +## API changes + + +* A new endpoint `POST /teams/invitations/accept` allows a non-team user to accept an invitation to join a team (#4229) + +* Services allowlist are blocked by 409 (mls-services-not-allowed) for teams with default protocol MLS. (#4266) + +* The `POST /clients` and `PUT /clients/:cid` endpoints support a new capability "consume-notifications" (#4259) + +* New variant in API version 7 of endpoints for creating and listing SCIM tokens that support a `name` field. New endpoint in version 7 for updating a SCIM token name. (#4307) + +* All the phone number-based functionality is removed from the client API v6 (#4149) + +* The team CSV export endpoint has gained two extra columns: `last_active` and `status`. The streaming behaviour has also been improved. (#4293) + +* The changes to the `capabilities` field of the `Client` structure, introduced in v6, have now been postponed to v7 (#4179) + +* Finalise version 6 and introduce new development version 7 (#4179, #4179) + +* From API version 7 the `GET /mls/public-key` and `GET /conversations/one2one/:domain/:uid` endpoints now take a `format` query parameter which can be either `raw` (default, for raw base64-encoded keys) or `jwk` (for JWK keys) (#4216, #4224) + +* `GET /conversations/one2one/:domain/:uid` now returns `public_keys` along with the conversation containing all MLS public keys for the backend which will host this conversation (since v6). (#4224) + +* Remove the ability to set the TTL of a feature flag. Existing TTLs are still retrieved and returned as before. Note that this only applies to the conferenceCalling feature, as none of the others supported TTL anyway. (#4164) + +* Add useSFTForOneToOneCalls as a config option for the Conference Calling feature flag and make its lock status explicit. (#4164) + +* Add endpoint to upgrade a personal user to a team owner (#4251) + + +## Features + + +* DB migration for dropping `phone` column from `user` table (#4130) + +* A text status field was added to user and user profile (#4155) + +* Allow an existing non-team user to migrate to a team (#4229, #4268, #4315) + +* Makes it impossible for a user to join an MLS conversation while already under legalhold (at least pending) + + This implies two things: + 1. If a user is under legalhold they cannot ever join an MLS conversation, not even an MLS self conversation. + 2. A user has to reject to be put under legalhold when they want to join an MLS conversation (ignoring the request to be put under legalhold is not enough). (#4242) + +* Email template for inviting a personal user to a team added (#4310) + +* Clients can declare to be supporting a capability for consuming notifications (#4259) + +* New endpoint to revoke an OAuth session (#4213) + +* Adds a field which contains a list of all active sessions to each OAuth application in the response of `GET /oauth/applications` (#4211) + +* SCIM's emails field is now handled and the external ID is not restricted to being an email anymore (#4221) + +* Added human readable names for SCIM tokens (#4307) + +* allow subconversations for MLS 1-1 conversations (#4133) + +* Allow choosing hashing algorithm and configuring argon2id parameters (#4291, #4291) + +* Deny requests for a legalhold device for users who are part of any MLS conversations (#4245) + +* Allow setting of Kubernetes annotations for the `coturn` Service. (#4189) + +* Add `initialConfig` setting for the `mls` feature flag (#4262) + +* Add `federationProtocols` setting to galley, which can be used to disable the creation of federated conversations with a given protocol (#4278) + +* added open telemetry instrumentation for brig, galley, gundeck and cannon (#3901) + +* Send confirmation email after adding a personal user to a new team (#4253) + +* The SFT and turn usernames returned by `/calls/config/v2` are now deterministically computed from the user ID (#4156) + +* Use latest stable RabbitMQ version (`3.13.7`) and Helm chart (`14.6.9`). Please + note that this minor RabbitMQ version upgrade (`3.11.x` to `3.13.x`) may need + special treatment regarding existing RabbitMQ instances. See + https://www.rabbitmq.com/docs/upgrade#rabbitmq-version-upgradability . The major + Helm chart version upgrade may (depending on your setup/values) need attention + as well: https://github.com/bitnami/charts/tree/main/bitnami/rabbitmq#upgrading (#4227) + + +## Bug fixes and other updates + + +* Fixed API version check. It has now precedence over other checks like e.g. method check. (#4152) + +* Fix handling of defaults of `mlsE2EID` feature config (#4233) + +* Match cipher suite tag in query parameters against key packages on replacing key packages (#4158) + +* Users with SAML-SSO are allowed to delete their email address on the rest api. If they do that, the search indices are not updated correctly, and finding the user by the removed email address is still possible. (#4260) + +* Re-add accidentally removed add-bot@v6 route in nginz, fixes #4302 (#4318) + +* Exclude exception message from error response (#4153) + +* Return HTTP 400 instead of 500 when property key is not printable ASCII (#4148) + +* move cipher suite updates into the commit lock (#4151) + +* Fix feature flag default calculation for `mlsMigration` and `enforceFileDownloadLocation` (#4265) + +* Allow setting existing properties even if we have max properties (#4148) + +* removed spam from nginx (nginz) by using the new style http/2 directive (#3901) + +* brig: Make `GET /services/tags` work again (#4250) + +* Process bounce and complaint notifications from SES correctly. (#4301) + + +## Documentation + + +* Call graph of federated endpoints was removed from the docs (#4299) + +* Restored LegalHold internal API swagger as part of Brig. (#4191) + +* Fix: show openapi docs for blocked versions (#4309) + +* Move docs from docs.wire.com to generated helper page served by brig (#4311) + +* Deleted proteus-specific test documentation tags and added some new tags to MLS tests (#4240) + +* Fix openapi validation errors (#4295, #4295) + +* Re-introduce test case tags for BSI audit (revert #4041) (#4192) + + +## Internal changes + + +* Introduced API versioning and version negotiation for external LegalHold Service supporting `v0` and `v1` (#4284) + +* Read sftTokenSecret from secrets.yaml and mount to /etc/wire/brig/secrets/sftTokenSecret by default (#4214) + +* Added node based topology constraint to ensure pods are distributed uniformly on all nodes. (#4222) + +* Move smallstep-accomp` helm charts to `wireapp/helm-charts` (#4204) + +* Remove coturn helm chart. It is moved to `wireapp/coturn`. (#4209) + +* Additional test for password reset, port tests to new integration test suite (#4249) + +* Remove unused invitation tables from brig. (#4263) + +* Improve abstraction in the invitation store and hide DB interaction-specific internal types from the application code. (#4280) + +* Move some invitation handling from brig to wire-subsystems. + + - introduce cyclically dependent effects: UserSubsystem, AuthenticationSubsystem (see Brig.CanonicalInterpreter). + - introduce TeamInvitationSubsystem with operations inviteUser, internalCreateInvitation. + - add verifyPassword to AuthenticationSubsystem. + - add sendInvitationMail, sendInvitationMailPersonalUser to EmailSubsystem. + - add getTeamSize to IndexedUserStore (this is morally internal to wire-subsystems, and making another ES subsystem would mean adding a lot of code everywhere). + - add updateUserTeam to UserStore. + - add acceptTeamInvitation, internalFindTeamInvitation to UserSubsystem. + - make a few small rest api handlers in brig polysemic (Handler -> Sem). (#4264) + +* tools/db/team-info: collects last login times of all team members (#4274) + +* Introduce length-preserving function mapRange to replace Functor instance for Range data type. (#4279) + +* TransitiveAnns compiler plugin was removed (#4299) + +* Servantify internal routing table for proxy. (#4296) + +* Servantify gundeck internal api (#4246) + +* Removed `indexReindex` and `indexReindexIfSameOrNewer` from internal Brig/SearchIndex. (#4188) + +* Introduced ElasticSearch effects related to user search. (#4188) + +* Brig was refactored by pulling out email block-listing into a wire subsystems effect, and its actions are exposed via the user subsystem. (#4167) + +* charts/wire-server: Deploy background-worker even when tags.federation is `false` (#4342, #4248) + +* Updated email templates to v1.0.122 (#4308) + +* Refactor feature flags + - Improved naming slightly. Features types are now called `Feature`, `LockableFeature` and `LockableFeaturePatch` + - Turned `AllFeatures` into an extensible record type + - Removed `WithStatusBase` barbie. + - Deleted obsolete `computeFeatureConfigForTeamUser` + - Abstracted `getFeature` and `setFeature` + - Abstracted getAllTeamFeatures (#4181) + +* Clean up and reorganise feature flag endpoints (#4193) + +* Clean up feature default configuration code (#4196) + +* Add federation-v1 environment for testing compatibility of the federation API with version 1 (#4125) + +* Fix overlapping paths errors in galley's internal API (#4313) + +* Local integration tests of federation version V1 fixed (#4320) + +* nginz/local-conf: Update list of endpoints (#4176) + +* Expose gundeck internal API on swagger. Mv some types and routes to wire-api. (#4247) + +* dockerephemeral: Use inbucket for SMTP (#4176) + +* Makefile: Add target `crm` to run services tuned for manual usage (#4176) + +* Postgresql helm chart is removed from charts/ directory and migrated to wireapp/helm-charts repo (#4208) + +* Simplify NewTeam and related types and remove lenses (#4257) + +* Add openapi validation test to integration (#4302) + +* Optimize getting a lot of users by concurrently getting target users (#4140) + +* charts/{brig,galley}: Allow setting a preStop hook for the deployments (#4200) + +* Introduce proeprty subsytem (#4148) + +* Factored out our Email type in favour of EmailAddress from email-validate. (#4206) + +* Move CSV export test to integration (#4292) + +* add the TODO pattern and the todo function to Imports (#4198) + +* Refactor user feature logic (#4178) + +* Remove `UserAccount` and `ExtendedUserAccount` and their fields to the `User` type (#4275) + +* Started weeding out dead code. (#4170) + +* New user subsystem operation `getAccountsBy` for complex account lookups. (#4218) + +* Added warning when deploying wire-server helm chart with User/Team creation over internet enabled. (#4212) + + # [2024-07-09] (Chart Release 5.5.0) ## Bug fixes and other updates diff --git a/Makefile b/Makefile index 7ec7a59287a..77a68d9cc10 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ DOCKER_TAG ?= $(USER) # default helm chart version must be 0.0.42 for local development (because 42 is the answer to the universe and everything) HELM_SEMVER ?= 0.0.42 # The list of helm charts needed on internal kubernetes testing environments -CHARTS_INTEGRATION := wire-server databases-ephemeral redis-cluster rabbitmq fake-aws ingress-nginx-controller nginx-ingress-controller nginx-ingress-services fluent-bit kibana restund coturn k8ssandra-test-cluster +CHARTS_INTEGRATION := wire-server databases-ephemeral redis-cluster rabbitmq fake-aws ingress-nginx-controller nginx-ingress-controller nginx-ingress-services fluent-bit kibana restund k8ssandra-test-cluster # The list of helm charts to publish on S3 # FUTUREWORK: after we "inline local subcharts", # (e.g. move charts/brig to charts/wire-server/brig) @@ -17,8 +17,8 @@ CHARTS_RELEASE := wire-server redis-ephemeral redis-cluster rabbitmq rabbitmq-ex fake-aws fake-aws-s3 fake-aws-sqs aws-ingress fluent-bit kibana backoffice \ calling-test demo-smtp elasticsearch-curator elasticsearch-external \ elasticsearch-ephemeral minio-external cassandra-external \ -nginx-ingress-controller ingress-nginx-controller nginx-ingress-services reaper restund coturn \ -k8ssandra-test-cluster postgresql ldap-scim-bridge smallstep-accomp +nginx-ingress-controller ingress-nginx-controller nginx-ingress-services reaper restund \ +k8ssandra-test-cluster ldap-scim-bridge KIND_CLUSTER_NAME := wire-server HELM_PARALLELISM ?= 1 # 1 for sequential tests; 6 for all-parallel tests @@ -49,9 +49,14 @@ install: init ./hack/bin/cabal-run-all-tests.sh ./hack/bin/cabal-install-artefacts.sh all +.PHONY: rabbit-clean +rabbit-clean: + rabbitmqadmin -f pretty_json list queues vhost name messages | jq -r '.[] | "rabbitmqadmin delete queue name=\(.name) --vhost=\(.vhost)"' | bash + # Clean .PHONY: full-clean full-clean: clean + make rabbit-clean rm -rf ~/.cache/hie-bios rm -rf ./dist-newstyle ./.env direnv reload @@ -77,7 +82,10 @@ cabal.project.local: # Usage: make c package=brig test=1 .PHONY: c -c: treefmt +c: treefmt c-fast + +.PHONY: c +c-fast: cabal build $(WIRE_CABAL_BUILD_OPTIONS) $(package) || ( make clean-hint; false ) ifeq ($(test), 1) ./hack/bin/cabal-run-tests.sh $(package) $(testargs) @@ -118,6 +126,9 @@ ci: cr: c db-migrate ./dist/run-services +crm: c db-migrate + ./dist/run-services -m + # Run integration from new test suite # Usage: make devtest # Usage: TEST_INCLUDE=test1,test2 make devtest @@ -126,7 +137,8 @@ devtest: ghcid --command 'cabal repl integration' --test='Testlib.Run.mainI []' .PHONY: sanitize-pr -sanitize-pr: +sanitize-pr: + ./hack/bin/check-weed.sh make lint-all-shallow make git-add-cassandra-schema @git diff-files --quiet -- || ( echo "There are unstaged changes, please take a look, consider committing them, and try again."; exit 1 ) @@ -162,7 +174,7 @@ lint-all: formatc hlint-check-all lint-common # The extra 'hlint-check-pr' has been witnessed to be necessary due to # some bu in `hlint-inplace-pr`. Details got lost in history. .PHONY: lint-all-shallow -lint-all-shallow: formatf hlint-inplace-pr hlint-check-pr lint-common +lint-all-shallow: lint-common formatf hlint-inplace-pr hlint-check-pr .PHONY: lint-common lint-common: check-local-nix-derivations treefmt-check # weeder (does not work on CI yet) @@ -238,11 +250,11 @@ add-license: .PHONY: treefmt treefmt: - treefmt + treefmt -u debug .PHONY: treefmt-check treefmt-check: - treefmt --fail-on-change + treefmt --fail-on-change -u debug ################################# ## docker targets @@ -591,3 +603,14 @@ upload-bombon: --project-version $(HELM_SEMVER) \ --api-key $(DEPENDENCY_TRACK_API_KEY) \ --auto-create + +.PHONY: openapi-validate +openapi-validate: + @echo -e "Make sure you are running the backend in another terminal (make cr)\n" + vacuum lint -a -d -e <(curl http://localhost:8082/v7/api/swagger.json) + vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/cannon-swagger.json) + vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/cargohold-swagger.json) + vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/spar-swagger.json) + vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/gundeck-swagger.json) + vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/brig-swagger.json) + vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/galley-swagger.json) diff --git a/cabal.project b/cabal.project index fe2c42af262..2daabf40f47 100644 --- a/cabal.project +++ b/cabal.project @@ -11,7 +11,6 @@ packages: , libs/dns-util/ , libs/deriving-swagger2/ , libs/galley-types/ - , libs/gundeck-types/ , libs/hscim/ , libs/http2-manager/ , libs/imports/ @@ -29,6 +28,7 @@ packages: , libs/wai-utilities/ , libs/wire-api/ , libs/wire-api-federation/ + , libs/wire-otel/ , libs/wire-message-proto-lens/ , libs/wire-subsystems/ , libs/zauth/ @@ -49,9 +49,9 @@ packages: , tools/db/move-team/ , tools/db/phone-users/ , tools/db/repair-handles/ + , tools/db/team-info/ , tools/db/repair-brig-clients-table/ , tools/db/service-backfill/ - , tools/fedcalls/ , tools/rabbitmq-consumer , tools/rex/ , tools/stern/ @@ -74,3 +74,4 @@ program-options allow-newer: , proto-lens-protoc:base , proto-lens-protoc:ghc + , proto-lens-setup:Cabal diff --git a/cassandra-schema.cql b/cassandra-schema.cql index bbeefe5b6e3..28fad0acf4a 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -112,25 +112,6 @@ CREATE TABLE brig_test.rich_info ( AND read_repair_chance = 0.0 AND speculative_retry = '99PERCENTILE'; -CREATE TABLE brig_test.user_keys_hash ( - key blob PRIMARY KEY, - key_type int, - user uuid -) 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 brig_test.service_tag ( bucket int, tag bigint, @@ -372,10 +353,9 @@ CREATE TABLE brig_test.oauth_user_refresh_token ( AND read_repair_chance = 0.0 AND speculative_retry = '99PERCENTILE'; -CREATE TABLE brig_test.invitation_info ( - code ascii PRIMARY KEY, - id uuid, - inviter uuid +CREATE TABLE brig_test.users_pending_activation ( + user uuid PRIMARY KEY, + expires_at timestamp ) WITH bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} AND comment = '' @@ -643,7 +623,6 @@ CREATE TABLE brig_test.team_invitation ( created_by uuid, email text, name text, - phone text, role int, PRIMARY KEY (team, id) ) WITH CLUSTERING ORDER BY (id ASC) @@ -678,7 +657,6 @@ CREATE TABLE brig_test.user ( managed_by int, name text, password blob, - phone text, picture list, provider uuid, searchable boolean, @@ -686,7 +664,9 @@ CREATE TABLE brig_test.user ( sso_id text, status int, supported_protocols int, - team uuid + team uuid, + text_status text, + write_time_bumper int ) WITH bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} AND comment = '' @@ -886,24 +866,6 @@ CREATE TABLE brig_test.connection_remote ( AND speculative_retry = '99PERCENTILE'; CREATE INDEX connection_remote_right_domain_idx ON brig_test.connection_remote (right_domain); -CREATE TABLE brig_test.users_pending_activation ( - user uuid PRIMARY KEY, - expires_at timestamp -) WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - CREATE TABLE brig_test.connection ( left uuid, right uuid, @@ -929,27 +891,6 @@ CREATE TABLE brig_test.connection ( AND speculative_retry = '99PERCENTILE'; CREATE INDEX conn_status ON brig_test.connection (status); -CREATE TABLE brig_test.password_reset ( - key ascii PRIMARY KEY, - code ascii, - retries int, - timeout timestamp, - user uuid -) 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 brig_test.federation_remotes ( domain text PRIMARY KEY, restriction int, @@ -969,20 +910,16 @@ CREATE TABLE brig_test.federation_remotes ( AND read_repair_chance = 0.0 AND speculative_retry = '99PERCENTILE'; -CREATE TABLE brig_test.invitation ( - inviter uuid, - id uuid, +CREATE TABLE brig_test.password_reset ( + key ascii PRIMARY KEY, code ascii, - created_at timestamp, - email text, - name text, - phone text, - PRIMARY KEY (inviter, id) -) WITH CLUSTERING ORDER BY (id ASC) - AND bloom_filter_fp_chance = 0.01 + retries int, + timeout timestamp, + user uuid +) 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.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + 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 @@ -1190,6 +1127,8 @@ CREATE TABLE galley_test.team_features ( 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, enforce_file_download_location text, enforce_file_download_location_lock_status int, @@ -2026,6 +1965,7 @@ CREATE TABLE spar_test.team_provisioning_by_team ( created_at timestamp, descr text, idp uuid, + name text, token_ text, PRIMARY KEY (team, id) ) WITH CLUSTERING ORDER BY (id ASC) @@ -2110,6 +2050,7 @@ CREATE TABLE spar_test.team_provisioning_by_token ( descr text, id uuid, idp uuid, + name text, team uuid ) WITH bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} diff --git a/charts/background-worker/templates/configmap.yaml b/charts/background-worker/templates/configmap.yaml index fea77ab59d5..8840a43764e 100644 --- a/charts/background-worker/templates/configmap.yaml +++ b/charts/background-worker/templates/configmap.yaml @@ -26,7 +26,9 @@ data: host: {{ .host }} port: {{ .port }} vHost: {{ .vHost }} + {{- if $.Values.config.enableFederation }} adminPort: {{ .adminPort }} + {{- end }} enableTls: {{ .enableTls }} insecureSkipVerifyTls: {{ .insecureSkipVerifyTls }} {{- if .tlsCaSecretRef }} diff --git a/charts/background-worker/values.yaml b/charts/background-worker/values.yaml index e38cd9c8225..8b79f6af6be 100644 --- a/charts/background-worker/values.yaml +++ b/charts/background-worker/values.yaml @@ -18,6 +18,7 @@ metrics: 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 rabbitmq: host: rabbitmq port: 5672 diff --git a/charts/brig/templates/configmap.yaml b/charts/brig/templates/configmap.yaml index 669e047bdc9..7c732c7b590 100644 --- a/charts/brig/templates/configmap.yaml +++ b/charts/brig/templates/configmap.yaml @@ -179,14 +179,17 @@ data: team: {{- if .emailSMS.team }} tInvitationUrl: {{ .emailSMS.team.tInvitationUrl }} + tExistingUserInvitationUrl: {{ .emailSMS.team.tExistingUserInvitationUrl }} tActivationUrl: {{ .emailSMS.team.tActivationUrl }} tCreatorWelcomeUrl: {{ .emailSMS.team.tCreatorWelcomeUrl }} tMemberWelcomeUrl: {{ .emailSMS.team.tMemberWelcomeUrl }} {{- else }} {{- if .externalUrls.teamSettings }} tInvitationUrl: {{ .externalUrls.teamSettings }}/join/?team-code=${code} + tExistingUserInvitationUrl: {{ .externalUrls.teamSettings }}/accept-invitation/?team-code=${code} {{- else }} tInvitationUrl: {{ .externalUrls.nginz }}/register?team=${team}&team_code=${code} + tExistingUserInvitationUrl: {{ .externalUrls.nginz }}/accept-invitation/?team-code=${code} {{- end }} tActivationUrl: {{ .externalUrls.nginz }}/register?team=${team}&team_code=${code} tCreatorWelcomeUrl: {{ .externalUrls.teamCreatorWelcome }} @@ -233,11 +236,11 @@ data: {{- if .sftDiscoveryIntervalSeconds }} sftDiscoveryIntervalSeconds: {{ .sftDiscoveryIntervalSeconds }} {{- end }} - {{- if .sftToken }} + {{- if $.Values.secrets.sftTokenSecret }} sftToken: {{- with .sftToken }} ttl: {{ .ttl }} - secret: {{ .secret }} + secret: {{ .secret | default "/etc/wire/brig/secrets/sftTokenSecret" }} {{- end }} {{- end }} {{- end }} @@ -365,5 +368,6 @@ data: {{- if .setOAuthMaxActiveRefreshTokens }} setOAuthMaxActiveRefreshTokens: {{ .setOAuthMaxActiveRefreshTokens }} {{- end }} + setPasswordHashingOptions: {{ toYaml .setPasswordHashingOptions | nindent 8 }} {{- end }} {{- end }} diff --git a/charts/brig/templates/deployment.yaml b/charts/brig/templates/deployment.yaml index cff8bffd9bb..08403170c07 100644 --- a/charts/brig/templates/deployment.yaml +++ b/charts/brig/templates/deployment.yaml @@ -30,6 +30,13 @@ spec: fluentbit.io/parser: json spec: serviceAccountName: {{ .Values.serviceAccount.name }} + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: brig volumes: - name: "brig-config" configMap: @@ -165,5 +172,10 @@ spec: scheme: HTTP path: /i/status port: {{ .Values.service.internalPort }} + {{- if .Values.preStop }} + lifecycle: + preStop: +{{ toYaml .Values.preStop | indent 14 }} + {{- end }} resources: {{ toYaml .Values.resources | indent 12 }} diff --git a/charts/brig/templates/secret.yaml b/charts/brig/templates/secret.yaml index b596954c7d8..0a566d04a00 100644 --- a/charts/brig/templates/secret.yaml +++ b/charts/brig/templates/secret.yaml @@ -20,6 +20,9 @@ data: awsKeyId: {{ .awsKeyId | b64enc | quote }} awsSecretKey: {{ .awsSecretKey | b64enc | quote }} {{- end }} + {{- if .sftTokenSecret }} + sftTokenSecret: {{ .sftTokenSecret | b64enc | quote }} + {{- end }} {{- if (not $.Values.config.useSES) }} smtp-password.txt: {{ .smtpPassword | b64enc | quote }} {{- end }} diff --git a/charts/brig/values.yaml b/charts/brig/values.yaml index 7dcedbce2dc..a7fd85eceb5 100644 --- a/charts/brig/values.yaml +++ b/charts/brig/values.yaml @@ -14,6 +14,10 @@ resources: metrics: serviceMonitor: enabled: false +# 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 @@ -63,7 +67,7 @@ config: useSES: true multiSFT: enabled: false # keep multiSFT default in sync with sft chart's multiSFT.enabled - enableFederation: false # keep enableFederation default in sync with galley and cargohold chart's config.enableFederation as well as wire-server chart's tags.federation + enableFederation: false # keep in sync with background-worker, cargohold and galley charts' config.enableFederation as well as wire-server chart's tags.federation # Not used if enableFederation is false rabbitmq: host: rabbitmq @@ -95,6 +99,14 @@ config: 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 @@ -138,6 +150,12 @@ config: setDisabledAPIVersions: [ development ] setFederationStrategy: allowNone setFederationDomainConfigsUpdateFreq: 10 + setPasswordHashingOptions: + algorithm: scrypt # or argon2id + # When algorithm is argon2id, these can be configured: + # iterations: + # parallelism: + # memory: smtp: passwordFile: /etc/wire/brig/secrets/smtp-password.txt proxy: {} diff --git a/charts/cannon/templates/statefulset.yaml b/charts/cannon/templates/statefulset.yaml index 2d7db645c36..2931ce01b90 100644 --- a/charts/cannon/templates/statefulset.yaml +++ b/charts/cannon/templates/statefulset.yaml @@ -34,6 +34,13 @@ spec: {{- end }} spec: terminationGracePeriodSeconds: {{ add .Values.config.drainOpts.gracePeriodSeconds 5 }} + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: cannon containers: {{- if .Values.service.nginz.enabled }} - name: nginz diff --git a/charts/cargohold/templates/deployment.yaml b/charts/cargohold/templates/deployment.yaml index 99222b2092a..fe25a506cc2 100644 --- a/charts/cargohold/templates/deployment.yaml +++ b/charts/cargohold/templates/deployment.yaml @@ -28,6 +28,13 @@ spec: checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} spec: serviceAccountName: {{ .Values.serviceAccount.name }} + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: cargohold volumes: - name: "cargohold-config" configMap: diff --git a/charts/cargohold/values.yaml b/charts/cargohold/values.yaml index 14cfaedce64..0eb8718e0ca 100644 --- a/charts/cargohold/values.yaml +++ b/charts/cargohold/values.yaml @@ -18,7 +18,7 @@ config: logLevel: Info logFormat: StructuredJSON logNetStrings: false - enableFederation: false # keep enableFederation default in sync with brig and galley chart's config.enableFederation as well as wire-server chart's tags.federation + 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 diff --git a/charts/coturn/Chart.yaml b/charts/coturn/Chart.yaml deleted file mode 100644 index 6a8abef6c9d..00000000000 --- a/charts/coturn/Chart.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v2 -name: coturn -description: coturn - a STUN and TURN server -type: application - -# This is the chart version. This version number should be incremented each time you make changes -# to the chart and its templates, including the app version. -# Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.0.42 - -# This is the version number of the application being deployed. This version number should be -# incremented each time you make changes to the application. Versions are not expected to -# follow Semantic Versioning. They should reflect the version the application is using. -appVersion: 4.6.2-federation-wireapp.16 diff --git a/charts/coturn/README.md b/charts/coturn/README.md deleted file mode 100644 index db46c3bf45f..00000000000 --- a/charts/coturn/README.md +++ /dev/null @@ -1,25 +0,0 @@ -**Warning**: this chart is currently considered beta. Use at your own risk! - -This chart deploys Wire's fork of [coturn](https://github.com/coturn/coturn), -a STUN and TURN server, with some additional features developed by Wire (see -[here](https://github.com/wireapp/coturn/tree/wireapp)) to support our calling -services. - -You need to supply a list of one or more zrest secrets at the key -`secrets.zrestSecrets`. The secret provided to the brig chart in -`secrets.turn.secret` must be included in this list. - -Note that coturn pods are deployed with `hostNetwork: true`, as they need to -listen on a wide range of UDP ports. Additionally, some TCP ports need to be -exposed on the hosting node, which are listed in `values.yaml`. - -Due to the nature of TURN, this service might also expose the -internal network to which the hosting node is connected. It is -therefore recommended to run coturn on a separate Kubernetes cluster -from the rest of the Wire services. Further details may be found in -Wire's documentation for Restund, another TURN implementation, on -[this](https://docs.wire.com/understand/restund.html#network) page. - -coturn can optionally be configured to expose a TLS control port. The TLS -private key and certificates should be provided in a `Secret` whose name is -given in `tls.secretRef`. diff --git a/charts/coturn/templates/_helpers.tpl b/charts/coturn/templates/_helpers.tpl deleted file mode 100644 index 70a70f30cd5..00000000000 --- a/charts/coturn/templates/_helpers.tpl +++ /dev/null @@ -1,54 +0,0 @@ -{{- define "coturn.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "coturn.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "coturn.labels" -}} -helm.sh/chart: {{ include "coturn.chart" . }} -{{ include "coturn.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Values.image.tag | default .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "coturn.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{- define "coturn.selectorLabels" -}} -app.kubernetes.io/name: {{ include "coturn.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* 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/coturn/templates/configmap-coturn-conf-template.yaml b/charts/coturn/templates/configmap-coturn-conf-template.yaml deleted file mode 100644 index f829900ad1c..00000000000 --- a/charts/coturn/templates/configmap-coturn-conf-template.yaml +++ /dev/null @@ -1,112 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: coturn - -data: - coturn.conf.template: | - ## disable dtls control plane; don't permit relaying tcp connections. - no-dtls - no-tcp-relay - - ## tls handling - {{- if .Values.tls.enabled }} - cert=/secrets-tls/tls.crt - pkey=/secrets-tls/tls.key - {{- if .Values.tls.ciphers }} - cipher-list={{ .Values.tls.ciphers }} - {{- end }} - {{- else }} - no-tls - {{- end }} - - # This is mandatory for federated DTLS - CA-file=/etc/ssl/certs/ca-certificates.crt - - ## don't turn on coturn's cli. - no-cli - - pidfile="/var/tmp/turnserver.pid" - - ## turn, stun. - listening-ip={{ default "__COTURN_EXT_IP__" .Values.coturnTurnListenIP }} - listening-port={{ .Values.coturnTurnListenPort }} - relay-ip={{ default "__COTURN_EXT_IP__" .Values.coturnTurnRelayIP }} - {{- if .Values.coturnTurnExternalIP }} - external-ip={{ default "__COTURN_EXT_IP__" .Values.coturnTurnExternalIP }} - {{- end }} - realm=dummy.io - no-stun-backward-compatibility - secure-stun - no-rfc5780 - - ## prometheus metrics - prometheus-ip={{ default "__COTURN_POD_IP__" .Values.coturnPrometheusIP }} - prometheus-port={{ .Values.coturnMetricsListenPort }} - - ## logs - log-file=stdout - {{- if .Values.config.verboseLogging }} - verbose - {{- end }} - - ## access control settings. - # the address ranges listed here are reserved for special use according - # to the iana registries for special-purposes ipv4 and ipv6 addresses. note - # however that these ranges do *not* include rfc1918 ipv4 space, or ula - # ipv6 space, as these may be valid peer addresses in some private network - # environments. - # - # ref: - # - https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml - # - https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml - # - https://www.rtcsec.com/article/cve-2020-26262-bypass-of-coturns-access-control-protection/#further-concerns-what-else - no-multicast-peers - denied-peer-ip=0.0.0.0-0.255.255.255 - denied-peer-ip=100.64.0.0-100.127.255.255 - denied-peer-ip=127.0.0.0-127.255.255.255 - denied-peer-ip=169.254.0.0-169.254.255.255 - denied-peer-ip=192.0.0.0-192.0.0.255 - denied-peer-ip=192.0.2.0-192.0.2.255 - denied-peer-ip=192.88.99.0-192.88.99.255 - denied-peer-ip=198.18.0.0-198.19.255.255 - denied-peer-ip=198.51.100.0-198.51.100.255 - denied-peer-ip=203.0.113.0-203.0.113.255 - denied-peer-ip=240.0.0.0-255.255.255.255 - denied-peer-ip=::1 - denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff - denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255 - denied-peer-ip=100::-100::ffff:ffff:ffff:ffff - denied-peer-ip=2001::-2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff - denied-peer-ip=2002::-2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff - # fc00::/7 is reserved for ipv6 ula, but fc00::/8 is not assigned at present. - denied-peer-ip=fc00::-fcff:ffff:ffff:ffff:ffff:ffff:ffff:ffff - denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff - - # FUTUREWORK: expose customisable access control settings. - - ## authentication setup - zrest - ## static authentication secrets will be added below this line when the - ## runtime configuration is generated. - - {{- if .Values.federate.enabled }} - ### federation setup - federation-listening-ip={{ default "__COTURN_EXT_IP__" .Values.coturnFederationListeningIP }} - federation-listening-port={{ .Values.federate.port }} - federation-no-dtls={{ not .Values.federate.dtls.enabled }} - {{- if .Values.federate.dtls.enabled }} - federation-cert=/coturn-dtls-certificate/tls.crt - federation-pkey=/coturn-dtls-certificate/tls.key - {{ if hasKey .Values.federate.dtls.tls "privateKeyPassword" }} - federation-pkey-pwd={{ .Values.federate.dtls.tls.privateKeyPassword }} - {{ end }} - # list of host/ip/cert common names / subject alt names, and optional issuer - # names to accept DTLS connections from. There can be multiple entries, each - # entry is formated as: - # [,] - {{ range $entry := .Values.federate.dtls.remoteWhitelist }} - federation-remote-whitelist={{ $entry.host }}{{ if hasKey $entry "issuer" }},{{ $entry.issuer }}{{end}} - {{ end }} - {{ end }} - {{ end }} diff --git a/charts/coturn/templates/secret-or-certificate.yaml b/charts/coturn/templates/secret-or-certificate.yaml deleted file mode 100644 index a48eba9b499..00000000000 --- a/charts/coturn/templates/secret-or-certificate.yaml +++ /dev/null @@ -1,41 +0,0 @@ -{{- if .Values.federate.dtls.enabled -}} - -{{- if .Values.federate.dtls.tls.issuerRef -}} -{{- if or .Values.federate.dtls.tls.key .Values.federate.dtls.tls.crt }} -{{- fail "issuerRef and {crt,key} are mutually exclusive" -}} -{{- end -}} -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: "{{ include "coturn.fullname" . }}" - labels: - {{- include "coturn.labels" . | nindent 4 }} - {{- if .Values.federate.dtls.tls.certificate.labels }} - {{- toYaml .Values.federate.dtls.tls.certificate.labels | nindent 4}} - {{- end }} -spec: - dnsNames: - {{- toYaml .Values.federate.dtls.tls.certificate.dnsNames | nindent 4 }} - secretName: coturn-dtls-certificate - issuerRef: - {{- toYaml .Values.federate.dtls.tls.issuerRef | nindent 4 }} - privateKey: - rotationPolicy: Always - algorithm: ECDSA - size: 384 -{{- else if and .Values.federate.dtls.tls.key .Values.federate.dtls.tls.crt }} -apiVersion: v1 -kind: Secret -metadata: - name: coturn-dtls-certificate - labels: - {{- include "coturn.labels" . | nindent 4 }} -type: Opaque -data: - tls.key: {{ .Values.federate.dtls.tls.key | b64enc }} - tls.crt: {{ .Values.federate.dtls.tls.crt | b64enc }} -{{- else -}} -{{- fail "must specify tls.key and tls.crt , or tls.issuerRef" -}} -{{- end -}} - -{{- end -}} diff --git a/charts/coturn/templates/secret.yaml b/charts/coturn/templates/secret.yaml deleted file mode 100644 index 6dd55212066..00000000000 --- a/charts/coturn/templates/secret.yaml +++ /dev/null @@ -1,20 +0,0 @@ -{{- if or (not .Values.secrets) (not .Values.secrets.zrestSecrets) }} -{{- fail "TURN authentication secrets are not defined in .Values.secrets.zrestSecrets" }} -{{- else if eq (len .Values.secrets.zrestSecrets) 0 }} -{{- fail "At least one authentication secret must be defined" }} -{{- else }} -apiVersion: v1 -kind: Secret -metadata: - name: coturn - labels: - app: coturn - chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -type: Opaque -stringData: - zrest_secret.txt: | - {{- range .Values.secrets.zrestSecrets }}{{ . | nindent 4 }} - {{- end }} -{{- end }} diff --git a/charts/coturn/templates/service-account.yaml b/charts/coturn/templates/service-account.yaml deleted file mode 100644 index ec932539fef..00000000000 --- a/charts/coturn/templates/service-account.yaml +++ /dev/null @@ -1,42 +0,0 @@ ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: coturn - labels: - app: coturn - chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: coturn-{{ .Release.Namespace }} - labels: - app: coturn - chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -rules: - - apiGroups: [""] - resources: [nodes] - verbs: [get] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: coturn-{{ .Release.Namespace }} - labels: - app: coturn - chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -roleRef: - kind: ClusterRole - apiGroup: rbac.authorization.k8s.io - name: coturn-{{ .Release.Namespace }} -subjects: - - kind: ServiceAccount - name: coturn - namespace: {{ .Release.Namespace }} diff --git a/charts/coturn/templates/service.yaml b/charts/coturn/templates/service.yaml deleted file mode 100644 index f1420c44d62..00000000000 --- a/charts/coturn/templates/service.yaml +++ /dev/null @@ -1,33 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - name: coturn - labels: - app: coturn - chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -spec: - # Needs to be headless - # See: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/ - clusterIP: None - ports: - - name: coturn-tcp - port: {{ .Values.coturnTurnListenPort }} - targetPort: coturn-tcp - - name: coturn-udp - port: {{ .Values.coturnTurnListenPort }} - targetPort: coturn-udp - protocol: UDP - {{- if .Values.tls.enabled }} - - name: coturn-tls - port: {{ .Values.coturnTurnTlsListenPort }} - targetPort: coturn-tls - {{- end }} - - name: status-http - port: {{ .Values.coturnMetricsListenPort }} - targetPort: status-http - selector: - app: coturn - release: {{ .Release.Name }} diff --git a/charts/coturn/templates/servicemonitor.yaml b/charts/coturn/templates/servicemonitor.yaml deleted file mode 100644 index a21f0faea4e..00000000000 --- a/charts/coturn/templates/servicemonitor.yaml +++ /dev/null @@ -1,19 +0,0 @@ -{{- if .Values.metrics.serviceMonitor.enabled }} -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: coturn - labels: - app: coturn - chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -spec: - endpoints: - - port: status-http - path: /metrics - selector: - matchLabels: - app: coturn - release: {{ .Release.Name }} -{{- end }} diff --git a/charts/coturn/templates/statefulset.yaml b/charts/coturn/templates/statefulset.yaml deleted file mode 100644 index e33c8be7ae2..00000000000 --- a/charts/coturn/templates/statefulset.yaml +++ /dev/null @@ -1,218 +0,0 @@ -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: coturn - labels: - app: coturn - chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} - -spec: - replicas: {{ .Values.replicaCount }} - - # Allow starting and stopping coturn in parallel when scaling. This does not - # affect upgrades. - podManagementPolicy: Parallel - - serviceName: coturn - selector: - matchLabels: - app: coturn - template: - metadata: - {{- with .Values.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - - labels: - app: coturn - release: {{ .Release.Name }} - spec: - securityContext: - {{- toYaml .Values.podSecurityContext | nindent 8 }} - {{- if .Values.tls.enabled }} - # Needed for automatic certificate reload handling - shareProcessNamespace: true - {{- end }} - hostNetwork: true - serviceAccountName: coturn - volumes: - - name: external-ip - emptyDir: {} - - name: coturn-config - emptyDir: {} - - name: coturn-config-template - configMap: - name: coturn - - name: secrets - secret: - secretName: coturn - - name: coturndb - emptyDir: - medium: Memory - sizeLimit: 128Mi # observed size: 80 kilobytes - {{- if .Values.tls.enabled }} - - name: secrets-tls - secret: - secretName: {{ .Values.tls.secretRef }} - {{- end }} - {{- if .Values.federate.dtls.enabled }} - - name: coturn-dtls-certificate - secret: - secretName: coturn-dtls-certificate - {{- end }} - initContainers: - - name: get-external-ip - image: bitnami/kubectl:1.24.12 - volumeMounts: - - name: external-ip - mountPath: /external-ip - env: - - name: NODE_NAME - valueFrom: - fieldRef: - fieldPath: spec.nodeName - command: - - /bin/sh - - -c - - | - set -e - - # In the cloud, this setting is available to indicate the true IP address - addr=$(kubectl get node $NODE_NAME -ojsonpath='{.status.addresses[?(@.type=="ExternalIP")].address}') - # On on-prem we allow people to set "wire.com/external-ip" to override this - if [ -z "$addr" ]; then - addr=$(kubectl get node $NODE_NAME -ojsonpath='{.metadata.annotations.wire\.com/external-ip}') - fi - echo -n "$addr" | tee /dev/stderr > /external-ip/ip - containers: - - name: {{ .Chart.Name }} - image: "{{ .Values.image.repository}}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - env: - - name: POD_IP - valueFrom: - fieldRef: - fieldPath: status.podIP - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: HOST_IP - valueFrom: - fieldRef: - fieldPath: status.hostIP - volumeMounts: - - name: external-ip - mountPath: /external-ip - - name: coturn-config - mountPath: /coturn-config - - name: coturn-config-template - mountPath: /coturn-template/coturn.conf.template - subPath: coturn.conf.template - - name: secrets - mountPath: /secrets/ - readOnly: true - # > By default, Coturn Docker image persists its data in /var/lib/coturn/ directory. - # > You can speedup Coturn simply by using tmpfs for that. - # We use a memory-backed emptyDir here instead. - - name: coturndb - mountPath: /var/lib/coturn - {{- if .Values.tls.enabled }} - - name: secrets-tls - mountPath: /secrets-tls/ - readOnly: true - {{- end }} - {{- if .Values.federate.dtls.enabled }} - - name: coturn-dtls-certificate - mountPath: /coturn-dtls-certificate/ - readOnly: true - {{- end }} - command: - - /usr/bin/dumb-init - - -- - - /bin/sh - - -c - - | - set -e - EXTERNAL_IP=$(cat /external-ip/ip) - sed -Ee "s;__COTURN_EXT_IP__;$EXTERNAL_IP;g" -e "s;__COTURN_POD_IP__;$POD_IP;g" -e "s;__COTURN_HOST_IP__;$HOST_IP;g" /coturn-template/coturn.conf.template > /coturn-config/turnserver.conf - sed -Ee 's/^/static-auth-secret=/' /secrets/zrest_secret.txt >> /coturn-config/turnserver.conf - exec /usr/bin/turnserver -c /coturn-config/turnserver.conf - {{- if .Values.coturnGracefulTermination }} - lifecycle: - preStop: - exec: - command: - - /bin/sh - - -c - - "exec /usr/local/bin/pre-stop-hook \"$POD_IP\" {{ .Values.coturnMetricsListenPort }}" - {{- end }} - - ports: - - name: coturn-tcp - containerPort: {{ .Values.coturnTurnListenPort }} - protocol: TCP - - name: coturn-udp - containerPort: {{ .Values.coturnTurnListenPort }} - protocol: UDP - {{- if .Values.tls.enabled }} - - name: coturn-tls - containerPort: {{ .Values.coturnTurnTlsListenPort }} - protocol: TCP - {{- end }} - - name: status-http - containerPort: {{ .Values.coturnMetricsListenPort }} - protocol: TCP - - livenessProbe: - timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }} - failureThreshold: {{ .Values.livenessProbe.failureThreshold }} - httpGet: - path: / - port: status-http - - readinessProbe: - timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }} - failureThreshold: {{ .Values.readinessProbe.failureThreshold }} - httpGet: - path: / - port: status-http - - resources: - {{- toYaml .Values.resources | nindent 12 }} - - {{- if .Values.tls.enabled }} - - name: {{ .Chart.Name }}-cert-reloader - image: "{{ .Values.tls.reloaderImage.repository }}:{{ .Values.tls.reloaderImage.tag }}" - imagePullPolicy: {{ .Values.tls.reloaderImage.pullPolicy }} - env: - - name: CONFIG_DIR - value: /secrets-tls - - name: PROCESS_NAME - value: turnserver - - name: RELOAD_SIGNAL - value: SIGUSR2 - volumeMounts: - - name: secrets-tls - mountPath: /secrets-tls/ - readOnly: true - {{- end }} - - {{- if .Values.coturnGracefulTermination }} - terminationGracePeriodSeconds: {{ .Values.coturnGracePeriodSeconds }} - {{- end }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/charts/coturn/values.yaml b/charts/coturn/values.yaml deleted file mode 100644 index fc6fe3b2917..00000000000 --- a/charts/coturn/values.yaml +++ /dev/null @@ -1,117 +0,0 @@ -# The amount of coturn instances to run. NOTE: Only one coturn can run per node due -# to `hostNetwork`. If this number is higher than the amount of nodes that can -# be used for scheduling (Also see `nodeSelector`) pods will remain in a -# pending state untill you add more capacity. -replicaCount: 1 - -image: - repository: quay.io/wire/coturn - pullPolicy: IfNotPresent - # overwrite the tag here, otherwise `appVersion` of the chart will be used - tag: "" - -# If you have multiple deployments of coturn running in one cluster, it is -# important that they run on disjoint sets of nodes, you can use nodeSelector to enforce this -nodeSelector: {} - -podSecurityContext: - fsGroup: 31338 - -securityContext: - # Pick a high number that is unlikely to conflict with the host - # https://kubesec.io/basics/containers-securitycontext-runasuser/ - runAsUser: 31338 - -coturnTurnListenPort: 3478 -coturnMetricsListenPort: 9641 -coturnTurnTlsListenPort: 5349 - -# coturnTurnListenIP: "1.2.3.4" # can also be __COTURN_EXT_IP__, __COTURN_POD_IP__,__COTURN_HOST_IP__ -coturnTurnExternalIP: null -# coturnTurnRelayIP: -# coturnPrometheusIP: -# coturnFederationListeningIP: - -tls: - enabled: false - # compliant with BSI TR-02102-2 - ciphers: 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384' - secretRef: - reloaderImage: - # container image containing https://github.com/Pluies/config-reloader-sidecar - # for handling runtime certificate reloads. - repository: quay.io/wire/config-reloader-sidecar - pullPolicy: IfNotPresent - tag: 72c3c8434660bd157d42012b0bcd67f338fc5c7a - -config: - verboseLogging: false - -federate: - enabled: false - port: 9191 - - dtls: - enabled: false - - tls: - # Example: - # - # tls: - # key: "-----BEGIN EC PRIVATE KEY----- ..." # (ascii blob) private key - # crt: "-----BEGIN CERTIFICATE----- ..." # (ascii blob) certificate - # privateKeyPassword: "XXX" # optional, used when the key is password protected - # - # OR (mutually exclusive) - # - # tls: - # issuerRef: - # name: letsencrypt-http01 - # - # # We can reference ClusterIssuers by changing the kind here. - # # The default value is Issuer (i.e. a locally namespaced Issuer) - # # kind: Issuer - # kind: Issuer - # - # # This is optional since cert-manager will default to this value however - # # if you are using an external issuer, change this to that issuer group. - # group: cert-manager.io - # - # # optional labels to attach to the cert-manager Certificate - # certificate: - # labels: .. - - # # list of host/ip/cert common names / subject alt names, and optional issuer - # # names to accept DTLS connections from. There can be multiple entries. - # remoteWhitelist: - # - host: wire.example - # issuer: Issuer Common Name - # - host: another.wire.example - # issuer: "DigiCert SHA2 Extended Validation Server CA" - # - host: another-host-without-issuer.wire.example - remoteWhitelist: [] - -metrics: - serviceMonitor: - enabled: false - -# This chart supports waiting for traffic to drain from coturn -# before pods are actually terminated. Once in 'drain' mode, no new connections -# are accepted, but old ones are kept alive. -# If you have 2 or more replicas, it's recommended to set this to true, -# and if you only have one coturn replica you may want this to be false, as -# otherwise while the pod restarts, no new calls can be established. -coturnGracefulTermination: false -# Grace period for terminating coturn pods, after which they will be forcibly -# terminated. This setting is only effective when coturnGracefulTermination is -# set to true. -coturnGracePeriodSeconds: 43200 # 12 hours - -livenessProbe: - timeoutSeconds: 5 - failureThreshold: 5 - -readinessProbe: - timeoutSeconds: 5 - failureThreshold: 5 - diff --git a/charts/demo-smtp/templates/deployment.yaml b/charts/demo-smtp/templates/deployment.yaml index 1e132a72ee6..4cc1b36a363 100644 --- a/charts/demo-smtp/templates/deployment.yaml +++ b/charts/demo-smtp/templates/deployment.yaml @@ -19,6 +19,13 @@ spec: app: {{ template "demo-smtp.name" . }} release: {{ .Release.Name }} spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: {{ template "demo-smtp.name" . }} containers: - name: {{ .Chart.Name }} image: "{{ .Values.image }}" diff --git a/charts/fake-aws-s3/templates/reaper.yaml b/charts/fake-aws-s3/templates/reaper.yaml index 9d7759eaadd..0687875de32 100644 --- a/charts/fake-aws-s3/templates/reaper.yaml +++ b/charts/fake-aws-s3/templates/reaper.yaml @@ -17,6 +17,13 @@ spec: labels: app: {{ template "fullname" . }}-reaper spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: {{ template "fullname" . }}-reaper volumes: - name: minio-configuration projected: diff --git a/charts/fake-aws-ses/templates/deployment.yaml b/charts/fake-aws-ses/templates/deployment.yaml index 11ec6b5501e..43c48b98a01 100644 --- a/charts/fake-aws-ses/templates/deployment.yaml +++ b/charts/fake-aws-ses/templates/deployment.yaml @@ -17,6 +17,13 @@ spec: labels: app: {{ template "fullname" . }} spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: {{ template "fullname" . }} containers: - name: fake-aws-ses image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" diff --git a/charts/fake-aws-sns/templates/deployment.yaml b/charts/fake-aws-sns/templates/deployment.yaml index f93bfc62167..04ff1e083f2 100644 --- a/charts/fake-aws-sns/templates/deployment.yaml +++ b/charts/fake-aws-sns/templates/deployment.yaml @@ -17,6 +17,13 @@ spec: labels: app: {{ template "fullname" . }} spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: {{ template "fullname" . }} containers: - name: fake-aws-sns image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" diff --git a/charts/fake-aws-sqs/templates/deployment.yaml b/charts/fake-aws-sqs/templates/deployment.yaml index 39848020c90..c8e024632b1 100644 --- a/charts/fake-aws-sqs/templates/deployment.yaml +++ b/charts/fake-aws-sqs/templates/deployment.yaml @@ -19,6 +19,13 @@ spec: annotations: checksum/configmap: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: {{ template "fullname" . }} containers: - name: fake-aws-sqs image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" diff --git a/charts/federator/templates/deployment.yaml b/charts/federator/templates/deployment.yaml index 8b38aa22e71..e5faa860516 100644 --- a/charts/federator/templates/deployment.yaml +++ b/charts/federator/templates/deployment.yaml @@ -30,6 +30,13 @@ spec: {{- end }} fluentbit.io/parser: json spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: federator volumes: - name: "federator-config" configMap: diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index ea0cd15354c..7b790a8c596 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -83,6 +83,9 @@ data: {{ fail "settings.conversationCodeURI and settings.multiIngress are mutually exclusive" }} {{- end }} federationDomain: {{ .settings.federationDomain }} + {{- if .settings.federationProtocols }} + federationProtocols: {{ .settings.federationProtocols }} + {{- end }} {{- if $.Values.secrets.mlsPrivateKeys }} mlsPrivateKeyPaths: removal: @@ -96,6 +99,7 @@ data: {{- if .settings.guestLinkTTLSeconds }} guestLinkTTLSeconds: {{ .settings.guestLinkTTLSeconds }} {{- end }} + passwordHashingOptions: {{ toYaml .settings.passwordHashingOptions | nindent 8 }} featureFlags: sso: {{ .settings.featureFlags.sso }} legalhold: {{ .settings.featureFlags.legalhold }} @@ -106,6 +110,10 @@ data: fileSharing: {{- toYaml .settings.featureFlags.fileSharing | nindent 10 }} {{- end }} + {{- if .settings.featureFlags.enforceFileDownloadLocation }} + enforceFileDownloadLocation: + {{- toYaml .settings.featureFlags.enforceFileDownloadLocation | nindent 10 }} + {{- end }} {{- if .settings.featureFlags.sndFactorPasswordChallenge }} sndFactorPasswordChallenge: {{- toYaml .settings.featureFlags.sndFactorPasswordChallenge | nindent 10 }} diff --git a/charts/galley/templates/deployment.yaml b/charts/galley/templates/deployment.yaml index ebfb5582abd..26d4ab5568b 100644 --- a/charts/galley/templates/deployment.yaml +++ b/charts/galley/templates/deployment.yaml @@ -28,6 +28,13 @@ spec: checksum/aws-secret: {{ include (print .Template.BasePath "/aws-secret.yaml") . | sha256sum }} checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: galley serviceAccountName: {{ .Values.serviceAccount.name }} volumes: - name: "galley-config" @@ -126,5 +133,10 @@ spec: scheme: HTTP path: /i/status port: {{ .Values.service.internalPort }} + {{- if .Values.preStop }} + lifecycle: + preStop: +{{ toYaml .Values.preStop | indent 14 }} + {{- end }} resources: {{ toYaml .Values.resources | indent 12 }} diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index 1d170d39883..60122db2c23 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -15,6 +15,10 @@ resources: 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 @@ -29,7 +33,7 @@ config: # tlsCaSecretRef: # name: # key: - enableFederation: false # keep enableFederation default in sync with brig and cargohold chart's config.enableFederation as well as wire-server chart's tags.federation + 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 @@ -66,6 +70,16 @@ config: # 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: + + # To disable proteus for new federated conversations: + # federationProtocols: ["mls"] + featureFlags: # see #RefConfigOptions in `/docs/reference` (https://github.com/wireapp/wire-server/) appLock: defaults: @@ -80,6 +94,7 @@ config: conferenceCalling: defaults: status: enabled + lockStatus: locked conversationGuestLinks: defaults: lockStatus: unlocked @@ -88,6 +103,11 @@ config: defaults: lockStatus: unlocked status: enabled + enforceFileDownloadLocation: + defaults: + lockStatus: locked + status: disabled + config: {} legalhold: disabled-by-default mls: defaults: diff --git a/charts/gundeck/templates/configmap.yaml b/charts/gundeck/templates/configmap.yaml index 446fa7bab39..cf7c37e1a7c 100644 --- a/charts/gundeck/templates/configmap.yaml +++ b/charts/gundeck/templates/configmap.yaml @@ -68,7 +68,6 @@ data: settings: httpPoolSize: 1024 notificationTTL: {{ required "config.notificationTTL" .notificationTTL }} - bulkPush: {{ .bulkPush }} {{- if hasKey . "perNativePushConcurrency" }} perNativePushConcurrency: {{ .perNativePushConcurrency }} {{- end }} diff --git a/charts/gundeck/templates/deployment.yaml b/charts/gundeck/templates/deployment.yaml index 5afbdd9c4cf..ee67ba1ba43 100644 --- a/charts/gundeck/templates/deployment.yaml +++ b/charts/gundeck/templates/deployment.yaml @@ -28,6 +28,13 @@ spec: checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} spec: serviceAccountName: {{ .Values.serviceAccount.name }} + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: gundeck volumes: - name: "gundeck-config" configMap: diff --git a/charts/gundeck/values.yaml b/charts/gundeck/values.yaml index ea8b6406a51..9749dd94be8 100644 --- a/charts/gundeck/values.yaml +++ b/charts/gundeck/values.yaml @@ -56,7 +56,6 @@ config: # # tlsCaSecretRef: # # name: # # key: - bulkPush: true aws: region: "eu-west-1" proxy: {} diff --git a/charts/integration/templates/configmap.yaml b/charts/integration/templates/configmap.yaml index ca2d49f9bec..77177433dee 100644 --- a/charts/integration/templates/configmap.yaml +++ b/charts/integration/templates/configmap.yaml @@ -6,6 +6,12 @@ metadata: "helm.sh/hook": post-install "helm.sh/hook-delete-policy": before-hook-creation data: + {{- range $name, $dynamicBackend := .Values.config.dynamicBackends }} + {{ $name }}-mls-removal-key-ed25519.pem: {{ $dynamicBackend.mlsPrivateKeys.removal.ed25519 | quote }} + {{ $name }}-mls-removal-key-ecdsa_secp256r1_sha256.pem: {{ $dynamicBackend.mlsPrivateKeys.removal.ecdsa_secp256r1_sha256 | quote }} + {{ $name }}-mls-removal-key-ecdsa_secp384r1_sha384.pem: {{ $dynamicBackend.mlsPrivateKeys.removal.ecdsa_secp384r1_sha384 | quote }} + {{ $name }}-mls-removal-key-ecdsa_secp521r1_sha512.pem: {{ $dynamicBackend.mlsPrivateKeys.removal.ecdsa_secp521r1_sha512 | quote }} + {{- end }} integration.yaml: | brig: host: brig.{{ .Release.Namespace }}.svc.cluster.local @@ -118,6 +124,12 @@ data: {{ $name }}: domain: {{ $dynamicBackend.federatorExternalHostPrefix }}.{{ $.Release.Namespace }}.svc.cluster.local federatorExternalPort: {{ $dynamicBackend.federatorExternalPort }} + mlsPrivateKeyPaths: + removal: + ed25519: "/etc/wire/integration/{{ $name }}-mls-removal-key-ed25519.pem" + ecdsa_secp256r1_sha256: "/etc/wire/integration/{{ $name }}-mls-removal-key-ecdsa_secp256r1_sha256.pem" + ecdsa_secp384r1_sha384: "/etc/wire/integration/{{ $name }}-mls-removal-key-ecdsa_secp384r1_sha384.pem" + ecdsa_secp521r1_sha512: "/etc/wire/integration/{{ $name }}-mls-removal-key-ecdsa_secp521r1_sha512.pem" {{- end }} cassandra: host: {{ .Values.config.cassandra.host }} @@ -164,4 +176,44 @@ data: stern: host: stern.wire-federation-v0.svc.cluster.local port: 8080 + + federation-v1: + originDomain: federation-test-helper.wire-federation-v1.svc.cluster.local + brig: + host: brig.wire-federation-v1.svc.cluster.local + port: 8080 + cannon: + host: cannon.wire-federation-v1.svc.cluster.local + port: 8080 + cargohold: + host: cargohold.wire-federation-v1.svc.cluster.local + port: 8080 + federatorInternal: + host: federator.wire-federation-v1.svc.cluster.local + port: 8080 + federatorExternal: + host: federator.wire-federation-v1.svc.cluster.local + port: 8081 + galley: + host: galley.wire-federation-v1.svc.cluster.local + port: 8080 + gundeck: + host: gundeck.wire-federation-v1.svc.cluster.local + port: 8080 + nginz: + host: nginz-integration-http.wire-federation-v1.svc.cluster.local + port: 8080 + spar: + host: spar.wire-federation-v1.svc.cluster.local + port: 8080 + proxy: + host: proxy.wire-federation-v1.svc.cluster.local + port: 8080 + backgroundWorker: + host: backgroundWorker.wire-federation-v1.svc.cluster.local + port: 8080 + stern: + host: stern.wire-federation-v1.svc.cluster.local + port: 8080 + integrationTestHostName: integration-headless.{{ .Release.Namespace }}.svc.cluster.local diff --git a/charts/integration/templates/integration-integration.yaml b/charts/integration/templates/integration-integration.yaml index 56dbf2bf8e7..3fe4284dc5b 100644 --- a/charts/integration/templates/integration-integration.yaml +++ b/charts/integration/templates/integration-integration.yaml @@ -328,3 +328,7 @@ spec: key: uploadXmlAwsSecretAccessKey {{- end }} {{- end }} + - name: ENABLE_FEDERATION_V0 + value: "1" + - name: ENABLE_FEDERATION_V1 + value: "1" diff --git a/charts/integration/values.yaml b/charts/integration/values.yaml index f1310f8fa4e..36305b2be75 100644 --- a/charts/integration/values.yaml +++ b/charts/integration/values.yaml @@ -17,13 +17,96 @@ config: dynamic-backend-1: federatorExternalHostPrefix: dynamic-backend-1 federatorExternalPort: 10098 + mlsPrivateKeys: + removal: + ed25519: | + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIJrGRHzIwjc5byivY2l+/MqbH3ty1yetYG8d5p4GGHhk + -----END PRIVATE KEY----- + ecdsa_secp256r1_sha256: | + -----BEGIN PRIVATE KEY----- + MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgaeLidXfwi/RVvWZ4 + OHoQhicePLIfyDZI7gMVsyXtec6hRANCAARM6EWywmjaCXtvsQ1M2edrbMescC+j + GSIhBrlE7igzhookThDBvOGAL67vf8xz+hw7tE8NqfzbdJQBL8NQik2L + -----END PRIVATE KEY----- + ecdsa_secp384r1_sha384: | + -----BEGIN PRIVATE KEY----- + MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDD1rK50pFsZmOomBiNQ + QFRRwAmed8Ox+nFseYbKzjLIAgWH0sMQ5DU8SAK8ks+GROShZANiAATyX0XQ6x6A + pi+HKz+ReWV9iIUOttxJv9u2aTY5ZrQ42IJs3fV1AGz1BE52uDvhbILOD9WfqZ9d + 6MqCjF6OqYT9nmnPkQ+CKC2XPzSVBpqJtuHXiMfFrc7n05E8CdIHOkI= + -----END PRIVATE KEY----- + ecdsa_secp521r1_sha512: | + -----BEGIN PRIVATE KEY----- + MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAQnVcp85/mC6r91yB + XjhiHOp4j450UcThSmDBdva4Pj9ihXvAScEFabus7CeECvUT3auqXKY9iSR45vQq + JuFI/0uhgYkDgYYABAG1paU01rRuYG4K2PWaIIbB9RuiYg5GVsu5mu6VHjYEH+7c + 1AGuCPEsUoM542cn3T1utv0EMtoj4yFPvf0xBs7AowHW04JsgMFzpWm8T1e/91n1 + IEkT5xOnq8obn7p4je9Ui95ojEA/n49gsTKsuO1qv2n79PnStLfn2yT5lAtcTcva + 6Q== + -----END PRIVATE KEY----- dynamic-backend-2: federatorExternalHostPrefix: dynamic-backend-2 federatorExternalPort: 11098 + mlsPrivateKeys: + removal: + ed25519: | + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIDgG4Dhqfq6KRyGKtEFiPeP+Nq1DBsTY31q3f/tC/lnk + -----END PRIVATE KEY----- + ecdsa_secp256r1_sha256: | + -----BEGIN PRIVATE KEY----- + MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgz0IEyU4GYrpkVH2y + iR87BMD1VAfBgl69WedewvA/Vl+hRANCAASTYYD2BF2E1zqPKYZtpHW1quo+YBsv + SAeznMX1bOeoOLD8zyFDHEGb3I9S90iGjYKTUogY+QfbbiqAiBIuSig7 + -----END PRIVATE KEY----- + ecdsa_secp384r1_sha384: | + -----BEGIN PRIVATE KEY----- + MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDA43epgPhtj4s0G3aXQ + TPXjnQHhiQ7Hfze+K8HgDSUL+Ds31v+g+Ko/OZrAA7povdWhZANiAATXd/dKoFvA + wlISC4MAbBsDV6g2oezzZt0nXUq4uysANJ24s+BNey7tYpB36qAOUhqmCzJW5IFJ + 22ttorUXSTaJeUIUdRiwD7xJ54z3NV5Wj8CUskvp0DIf/ILkOpbxdQY= + -----END PRIVATE KEY----- + ecdsa_secp521r1_sha512: | + -----BEGIN PRIVATE KEY----- + MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAjGOdWinAUUopQCYW + 6Ch4UuwdHhTERbUS90bQiQyoPdnTrTT8+NsYsB8DmPLltxls6h28q0IGCKUmO9ph + 8gFT0l6hgYkDgYYABAD3l73lFiVckI4V8BhR2x83o44dhjZA26d8SVSUBt9iuRbR + Lh0vP+zghhDQZLFLpfcL0Fo0K9H4HdQwe2cMxbOyQwDUC76ot9BdZjfsjKiRK6+k + ZNlnHSWx15yg8gF0dpt2eVn1LBLB0JvRcauYVMfKNox1IU8DY0ZiuO4DJNXRDVEI + 7w== + -----END PRIVATE KEY----- dynamic-backend-3: federatorExternalHostPrefix: dynamic-backend-3 federatorExternalPort: 12098 - + mlsPrivateKeys: + removal: + ed25519: | + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIB/Jddpef01pYWQXUEFmJ+k6dDQE7fVSKfk7/AyQaOnU + -----END PRIVATE KEY----- + ecdsa_secp256r1_sha256: | + -----BEGIN PRIVATE KEY----- + MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgIkeTuHoMtzsuaN3f + zug+mp/IlejrG1W4z/lOU9yGNqKhRANCAASLReHnUMJfSs0pDFxVYIgCOThRsiCD + Fq/6oKzWYnvX+taJgNUCVm7QND7Q9ll+Vy4ymZmE9YH1QuNW4FbVe1X6 + -----END PRIVATE KEY----- + ecdsa_secp384r1_sha384: | + -----BEGIN PRIVATE KEY----- + MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDB0dp/epJB3XhCeRcYJ + C76Ll50HHb/H2GR/UBFyAWYtQ8mbaXWis8NPjvYmqrqd5VWhZANiAARoOjIYWdAP + Y910LsLGcihvmnoFx7atJbOhaGTem57P/DOkYqcYohUcz6WaCuqzk/ZEj8NZtdvF + 4AYt0mnxkl9L5pt2a6i2HWW+4puR+JMmWD9qj0lRc5AQeEtmbuohIfg= + -----END PRIVATE KEY----- + ecdsa_secp521r1_sha512: | + -----BEGIN PRIVATE KEY----- + MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBfyz5LCeeA0seQo1O + jlQiUKxL4tWX23mD5G5Y5nra3Ju/7mNYp/sIX5BS81iWno5N6KfEdgUtgEffa4Xj + nuyF2QqhgYkDgYYABAF3eFOMjqpO7hDdVua9WgquGdFRRd3LWLhY0fyeyiQn/7yr + vLIb01f8dX9UVFKMxw77ZMMcfF+uW5Enxa8kadDcmwHIiIh/6jW0oGlFxkmwmecr + MwfpR6lZMbtQMD4rm8AwQAsFCBCRyPyK8bWanzMYusbnCdS/nBB5YB8x0ejjYFlU + RQ== + -----END PRIVATE KEY----- cassandra: host: cassandra-ephemeral port: 9042 diff --git a/charts/legalhold/templates/deployment.yaml b/charts/legalhold/templates/deployment.yaml index 51036248390..7f6b2c320aa 100644 --- a/charts/legalhold/templates/deployment.yaml +++ b/charts/legalhold/templates/deployment.yaml @@ -17,6 +17,13 @@ spec: labels: name: "{{ .Release.Name }}-hold" spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + name: "{{ .Release.Name }}-hold" restartPolicy: Always containers: - name: hold diff --git a/charts/nginz/templates/deployment.yaml b/charts/nginz/templates/deployment.yaml index fd9a0f3cd06..d04610f6020 100644 --- a/charts/nginz/templates/deployment.yaml +++ b/charts/nginz/templates/deployment.yaml @@ -29,6 +29,13 @@ spec: fluentbit.io/parser-nginz: nginz spec: terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: nginz containers: - name: nginz-disco image: "{{ .Values.images.nginzDisco.repository }}:{{ .Values.images.nginzDisco.tag }}" diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index c3db69f37fc..b853e1b2cde 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -156,6 +156,10 @@ nginx_conf: envs: - all doc: true + - path: /handles + envs: + - all + doc: true - path: /list-users envs: - all @@ -410,6 +414,9 @@ nginx_conf: envs: - all disable_zauth: true + - path: /teams/invitations/accept$ + envs: + - all - path: /i/teams/invitation-code envs: - staging @@ -476,7 +483,7 @@ nginx_conf: - all max_body_size: 40m body_buffer_size: 256k - - path: /conversations/one2one/ + - path: /one2one-conversations/ envs: - all # During MLS migration, this endpoint gets called _a lot_. diff --git a/charts/openldap/templates/openldap.yaml b/charts/openldap/templates/openldap.yaml index 28ed001aa62..3a0fdb9f08b 100644 --- a/charts/openldap/templates/openldap.yaml +++ b/charts/openldap/templates/openldap.yaml @@ -8,6 +8,13 @@ metadata: release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: openldap securityContext: fsGroup: 911 volumes: diff --git a/charts/outlook-addin/templates/deployment.yaml b/charts/outlook-addin/templates/deployment.yaml index a9679ab816b..3a0ab24413d 100644 --- a/charts/outlook-addin/templates/deployment.yaml +++ b/charts/outlook-addin/templates/deployment.yaml @@ -15,6 +15,13 @@ spec: labels: app: {{ include "outlook.fullname" . }} spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: {{ include "outlook.fullname" . }} containers: - name: {{ include "outlook.fullname" . }} image: {{ .Values.containerImage }} diff --git a/charts/postgresql/Chart.yaml b/charts/postgresql/Chart.yaml deleted file mode 100644 index bff81e0d6fb..00000000000 --- a/charts/postgresql/Chart.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -description: Wrapper chart for bitnami/postgresql -name: postgresql -version: 0.0.42 diff --git a/charts/postgresql/README.md b/charts/postgresql/README.md deleted file mode 100644 index 84f4ee02a57..00000000000 --- a/charts/postgresql/README.md +++ /dev/null @@ -1,4 +0,0 @@ -This is the wrapper for PostgreSQL Bitnami chart. - -Configure the values.yaml file to create the database, username, password and other configuration. -List of parameters available - https://artifacthub.io/packages/helm/bitnami/postgresql#parameters diff --git a/charts/postgresql/requirements.yaml b/charts/postgresql/requirements.yaml deleted file mode 100644 index c1d2a1f639b..00000000000 --- a/charts/postgresql/requirements.yaml +++ /dev/null @@ -1,4 +0,0 @@ -dependencies: -- name: postgresql - version: 11.9.8 - repository: https://charts.bitnami.com/bitnami diff --git a/charts/postgresql/values.yaml b/charts/postgresql/values.yaml deleted file mode 100644 index fa2230183f7..00000000000 --- a/charts/postgresql/values.yaml +++ /dev/null @@ -1,5 +0,0 @@ -# Configure the parent postgresql chart -postgresql: - fullnameOverride: postgresql - volumePermissions: - enabled: true diff --git a/charts/proxy/templates/deployment.yaml b/charts/proxy/templates/deployment.yaml index 63239a5d413..02676553a1b 100644 --- a/charts/proxy/templates/deployment.yaml +++ b/charts/proxy/templates/deployment.yaml @@ -27,6 +27,13 @@ spec: checksum/configmap: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: proxy volumes: - name: "proxy-config" configMap: diff --git a/charts/rabbitmq/requirements.yaml b/charts/rabbitmq/requirements.yaml index 1742b3e8641..6ac9220940f 100644 --- a/charts/rabbitmq/requirements.yaml +++ b/charts/rabbitmq/requirements.yaml @@ -1,4 +1,4 @@ dependencies: - name: rabbitmq - version: 11.13.0 + version: 14.6.9 repository: https://charts.bitnami.com/bitnami diff --git a/charts/reaper/templates/deployment.yaml b/charts/reaper/templates/deployment.yaml index 89b581b0941..a63cdc42fef 100644 --- a/charts/reaper/templates/deployment.yaml +++ b/charts/reaper/templates/deployment.yaml @@ -20,6 +20,13 @@ spec: release: {{ .Release.Name }} spec: serviceAccountName: reaper-role + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: reaper containers: - name: reaper image: bitnami/kubectl:1.24.12 diff --git a/charts/restund/templates/statefulset.yaml b/charts/restund/templates/statefulset.yaml index 87fa6571c21..97f09a60aae 100644 --- a/charts/restund/templates/statefulset.yaml +++ b/charts/restund/templates/statefulset.yaml @@ -26,6 +26,13 @@ spec: labels: {{- include "restund.selectorLabels" . | nindent 8 }} spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + {{- include "restund.selectorLabels" . | nindent 6 }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} hostNetwork: true diff --git a/charts/smallstep-accomp/Chart.yaml b/charts/smallstep-accomp/Chart.yaml deleted file mode 100644 index 6dad899102f..00000000000 --- a/charts/smallstep-accomp/Chart.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -description: Accompanying chart for Smallstep E2EI support -name: smallstep-accomp -version: 1.0.4 diff --git a/charts/smallstep-accomp/README.md b/charts/smallstep-accomp/README.md deleted file mode 100644 index ad57924296c..00000000000 --- a/charts/smallstep-accomp/README.md +++ /dev/null @@ -1,113 +0,0 @@ -# smallstep-acomp - Helm chart accompanying smallstep - -This Helm chart is meant to be installed alongside the [step-certificates Helm -chart](https://smallstep.github.io/helm-charts) in the same namespace. It has been tested with Helm -chart version `1.25.0` and image - -``` -image: - repository: cr.step.sm/smallstep/step-ca - tag: "0.25.3-rc7" -``` - -This Helm chart provides: - -- A reverse-proxy for Certificate Revocation List (CR) distribution endpoints to federating smallstep - servers -- Smallstep server configuration for the End-to-End Identity setup - - -## Reverse-proxy for CRL distribution points - -This Helm chart installs a reverse proxy that proxies the Certificate Revocation List (CRL) -Distribution Point of the Smallstep servers CRL Certificate Authority (CA) from federating domains -and the own domain. This reverse proxy is required for a working End-to-End Identity setup. - -The Helm chart deploys a nginx server that reverse-proxies -`https:///proxyCrl/` to `https://{other_acme_domain}/crl` -as well as an ingress for the `/proxyCrl` endpoint. For example if `upstreams.proxiedHosts` is set -to `[acme.alpha.example.com, acme.beta.example.com]` and the host for the Smallstep server on the -own domain is `acme.alpha.example.com` this helm chart will forward requests - -- `https://acme.alpha.example.com/proxyCrl/acme.alpha.example.com` to `https://acme.alpha.example.com/crl` -- `https://acme.alpha.example.com/proxyCrl/acme.beta.example.com` to `https://acme.beta.example.com/crl` - -| Name | Description | -| -------------------------- | --------------------------------------------------------------------------------------------------------- | -| `upstreams.enable` | Set to `false` in case you want to write custom nginx server block for the upstream rules | -| `upstreams.dnsResolver` | DNS server that nginx uses to resolve the proxied hostnames | -| `upstreams.proxiedHosts` | List of remote (federated) step-ca hostnames to proxy. Also include the own step-ca host here. | -| `nginx.ingress.enable` | Set to `false` if you need to define a custom ingress for the /proxyCrl endpoint. Make sure CORS is set. | -| `nginx.ingress.hostname` | Hostname of the step-ca server | -| `nginx.ingress.extraTls` | The TLS configuration | -| `nginx.ingress.annotations`| CORS config for the ingress, set the hostname of the step-ca server here | - -For more details on `nginx.*` parameters see README.md documentation in the `nginx` dependency chart. - -## Smallstep server configuration for the End-to-End Identity setup - -This Helm chart helps to create configuration file for step-ca. If `stepConfig.enabled` is `true` a -configmap that contains a `ca.json` will be created. The name of that configmap is compatible with the -step-certificates Helm chart, so that it can be directly used. However since step-ca is deployed -from a seperate Helm release updating and deploying a configuration won't cause an automatic reload -of the step-ca server. It is therefore recommended to manually restart step-ca after configuartion -changes if this Helm chart is used for these purposes. - -For references see: - -- [[1] Configuring `step-ca`](https://smallstep.com/docs/step-ca/configuration/) -- [[2] Configuring `step-ca` Provisioners - ACME for Wire messenger clients ](https://smallstep.com/docs/step-ca/provisioners/#acme-for-wire-messenger-clients) - -| Parameter | Description | -|------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------| -| `stepConfig.enabled` | Create a configmap with configuration file for `step-certificates` Helm chart. | -| | If `true` then almost all `stepConfig.*` parameters are required. | -| `stepConfig.configTemplate` | Template for the configuration file. Overwrite this if the default value is not generic enough for your use case. | -| `stepConfig.address` | See [1] | -| `stepConfig.dnsName` | Used in `dnsNames` config entry (See [1]) and to define the CRL URL. | -| `stepConfig.additionalDNSNames` | Optional. Additional entries to `dnsNames` configuration field | -| `stepConfig.root` | See [1]. Public key of the Root CA | -| `stepConfig.crt` | See [1]. Public key of the Intermediate CA | -| `stepConfig.key` | See [1]. Private key of the Intermediate CA | -| `stepConfig.federatedRoots` | See [1]. Add all cross-signed Intermediate CA certs from federating domains here. | -| `stepConfig.db` | See [1] | -| `stepConfig.tls` | See [1] | -| `stepConfig.logger` | See [1] | -| `stepConfig.authority.claims` | See [1] | -| `stepConfig.authority.jwk` | JSON string of the JWK provisioner to use. A JWK provisioner can be created | -| | by running `step ca init` then copying it out of the generated `ca.json`, discarding the `ca.json`. | -| `stepConfig.authority.acme.name` | Name of the ACME provisioner. Default: `"keycloakteams"` | -| `stepConfig.authority.acme.claims` | See [1] | -| `stepConfig.authority.acme.dpop.key` | See [2]. Public half of the DPoP signature key bundle configured of the Wire deployment. | -| | Use the same value as `brig.secrets.dpopSigKeyBundle` value of the `wire-server` Helm chart. | -| | Base64 encoded string of the PEM encoded public key. | -| `stepConfig.authority.acme.dpop.wireDomain` | Set this to the federation domain of the backend | -| `stepConfig.authority.acme.oidc.clientId` | Name of the OIDC client. Default: "wireapp". | -| `stepConfig.authority.acme.oidc.discoveryBaseUrl` | OpenID Connect Discovery endpoint. The OIDC provider must respond with its configuration when `/.well-known/openid-configuration` | -| | is appended to the URL. For Keycloak this URL is of format `https:///auth/realms/`. | -| `stepConfig.authority.acme.oidc.issuerUrl` | For Keycloak this must be of the format `https:///auth/realms/?client_id=wireapp` | -| `stepConfig.authority.acme.oidc.signatureAlgorithms` | See [2] | -| `stepConfig.authority.acme.oidc.transform` | See [2]. Has sensible default. There shouldn't be any need to customize this setting. | -| `stepConfig.authority.acme.x509.organization` | Set this to the federation domain of the backend | -| `stepConfig.authority.acme.x509.template` | See [2]. Go template for a client certificate which is evaluated by step-ca. | -| | This string is evaluated as template of the Helm chart first. | -| | Has a sensible default. There shouldn't be a need to customize this setting. | - -| Parameter | Description | -|-----------------------|-------------------------------------------------------------------------------------------------------| -| `caPassword.enabled` | If `true` generate Secret with a name that the `step-certificates` Helm chart will automatically use. | -| | The Helm chart will mount this at `/home/step/secrets/passwords/password`. | -| `caPassword.password` | Password that decrypts the intermediate CA private key | - -| Parameter | Description | -|---------------------------|-------------------------------------------------------------------------------------------------------| -| `existingSecrets.enabled` | If `true` generate Secret with a name that the `step-certificates` Helm chart will automatically use. | -| `existingSecrets.data` | Map from filename to content. Each entry will be mounted as file `/home/step/secrets/` | -| | Add the private key of the Intermediate CA here. | - -| Parameter | Description | -|-------------------------|-----------------------------------------------------------------------------------------------------| -| `existingCerts.enabled` | If `true` generate ConfigMap with a name that the Helm chart will automatically use. | -| `existingCerts.data` | Map from filename to content. Each entry will be mounted as file `/home/step/certs/` | -| `existingCerts.data` | Use it to make public keys of the Root, intermediate CA as well as the cross-signed certs available | -| | to step-ca. Each entry will be mounted as file `/home/step/certs/` | diff --git a/charts/smallstep-accomp/requirements.yaml b/charts/smallstep-accomp/requirements.yaml deleted file mode 100644 index e9d0780c6e9..00000000000 --- a/charts/smallstep-accomp/requirements.yaml +++ /dev/null @@ -1,4 +0,0 @@ -dependencies: -- name: nginx - version: 15.10.4 - repository: https://charts.bitnami.com/bitnami diff --git a/charts/smallstep-accomp/templates/_helpers.tpl b/charts/smallstep-accomp/templates/_helpers.tpl deleted file mode 100644 index fb5cb93c9ce..00000000000 --- a/charts/smallstep-accomp/templates/_helpers.tpl +++ /dev/null @@ -1,3 +0,0 @@ -{{- define "fullname" -}} -smallstep-step-certificates -{{- end -}} diff --git a/charts/smallstep-accomp/templates/ca-password.yaml b/charts/smallstep-accomp/templates/ca-password.yaml deleted file mode 100644 index cd1bdc962a9..00000000000 --- a/charts/smallstep-accomp/templates/ca-password.yaml +++ /dev/null @@ -1,12 +0,0 @@ -{{- if .Values.caPassword.enabled }} -apiVersion: v1 -kind: Secret -metadata: - name: smallstep-step-certificates-ca-password - labels: - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - release: "{{ .Release.Name }}" -type: Opaque -data: - password: {{ .Values.caPassword.password | b64enc | quote }} -{{- end }} diff --git a/charts/smallstep-accomp/templates/certs.yaml b/charts/smallstep-accomp/templates/certs.yaml deleted file mode 100644 index c9ef0ce45a9..00000000000 --- a/charts/smallstep-accomp/templates/certs.yaml +++ /dev/null @@ -1,13 +0,0 @@ -{{- if .Values.existingCerts.enabled }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: smallstep-step-certificates-certs - labels: - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - release: "{{ .Release.Name }}" -data: - {{- range $key, $value := .Values.existingCerts.data }} - {{ $key }}: {{ $value | quote }} - {{- end }} -{{- end }} diff --git a/charts/smallstep-accomp/templates/secrets.yaml b/charts/smallstep-accomp/templates/secrets.yaml deleted file mode 100644 index 8448fbc7f8f..00000000000 --- a/charts/smallstep-accomp/templates/secrets.yaml +++ /dev/null @@ -1,14 +0,0 @@ -{{- if .Values.existingSecrets.enabled }} -apiVersion: v1 -kind: Secret -metadata: - name: smallstep-step-certificates-secrets - labels: - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - release: "{{ .Release.Name }}" -type: Opaque -data: - {{- range $key, $value := .Values.existingSecrets.data }} - {{ $key }}: {{ $value | b64enc | quote }} - {{- end }} -{{- end }} diff --git a/charts/smallstep-accomp/templates/server-block-configmap.yaml b/charts/smallstep-accomp/templates/server-block-configmap.yaml deleted file mode 100644 index 59c423d3345..00000000000 --- a/charts/smallstep-accomp/templates/server-block-configmap.yaml +++ /dev/null @@ -1,39 +0,0 @@ -{{- if and .Values.upstreams.enabled .Values.nginx.existingServerBlockConfigmap }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Values.nginx.existingServerBlockConfigmap }} - labels: - chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -data: - server.conf: | - resolver {{ .Values.upstreams.dnsResolver }}; - - server { - listen 0.0.0.0:8080; - - {{- range .Values.upstreams.proxiedHosts }} - - location /proxyCrl/{{ . }} { - # This indirection is required to make the resolver check the domain. - # Otherwise, broken upstreams lead to broken deployments. - set $backend "{{ . }}"; - - proxy_redirect off; - proxy_set_header X-Forwarded-Host $http_host; - proxy_set_header Host $backend; - proxy_hide_header Content-Type; - add_header Content-Type application/pkix-crl; - # Prevent caching on client side - add_header Cache-Control 'no-cache, no-store, must-revalidate'; - add_header Pragma 'no-cache'; - add_header Expires '0'; - - proxy_pass "https://$backend/crl"; - } - - {{- end }} - } -{{- end }} diff --git a/charts/smallstep-accomp/templates/step-config.yaml b/charts/smallstep-accomp/templates/step-config.yaml deleted file mode 100644 index 0cb957fa88c..00000000000 --- a/charts/smallstep-accomp/templates/step-config.yaml +++ /dev/null @@ -1,9 +0,0 @@ -{{- if .Values.stepConfig.enabled }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: smallstep-step-certificates-config -data: - ca.json: |- - {{(tpl .Values.stepConfig.configTemplate .) | fromYaml | toJson }} -{{- end }} diff --git a/charts/smallstep-accomp/values.yaml b/charts/smallstep-accomp/values.yaml deleted file mode 100644 index e4e3ad18437..00000000000 --- a/charts/smallstep-accomp/values.yaml +++ /dev/null @@ -1,212 +0,0 @@ -nginx: - existingServerBlockConfigmap: "smallstep-accomp-server-block" - - service: - type: ClusterIP - - ingress: - enabled: true - # ingressClassName: "nginx" - - # hostname: "acme.alpha.example.com" - path: "/proxyCrl" - pathType: "Prefix" - - # extraTls: - # - - # hosts: [ "acme.alpha.example.com" ] - # secretName: "smallstep-step-certificates-ingress-cert" - - # annotations: - # nginx.ingress.kubernetes.io/cors-allow-origin: https://webapp.acme.alpha.example.com - # nginx.ingress.kubernetes.io/cors-expose-headers: Replay-Nonce, Location - # nginx.ingress.kubernetes.io/enable-cors: 'true' - -upstreams: - enabled: true - # dnsResolver: 9.9.9.9 - - # Note: include the smallstep host of the own domain here as well - proxiedHosts: [] - # proxiedHosts: - # - acme.alpha.example.com - # - acme.beta.example.com - # - acme.gamma.example.com - - -caPassword: - enabled: true - password: "...." - -existingSecrets: - enabled: false - # data: - # ca.key: foobar - -existingCerts: - enabled: false - # data: - # ca.crt: "-----BEGIN CERTIFICATE-----...." - # root_ca.crt: "-----BEGIN CERTIFICATE-----...." - # ca-other2-cross-signed.crt: "-----BEGIN CERTIFICATE-----...." - # ca-other3-cross-signed.crt: "-----BEGIN CERTIFICATE-----...." - -stepConfig: - enabled: true - - address: "0.0.0.0:9000" - - # dnsName: acme.alpha.example.com - - # additionalDNSNames: - # - localhost - - root: /home/step/certs/root_ca.crt - crt: /home/step/certs/ca.crt - key: /home/step/secrets/ca.key - - federatedRoots: - - /home/step/certs/ca.crt - - # federatedRoots: - # - /home/step/certs/ca.crt - # - /home/step/certs/acme.beta.example.com-xsigned-by-acme.alpha.example.com - - db: - badgerFileLoadingMode: "" - dataSource: /home/step/db - type: badgerv2 - - tls: - cipherSuites: - - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 - maxVersion: 1.3 - minVersion: 1.2 - renegotiation: false - - logger: - format: text - - authority: - claims: - maxTLSCertDuration: 87701h - - # jwk: |- - # { - # "type": "JWK", - # "name": "..example.com", - # "key": { ... }, - # "encryptedKey": "e..." - # } - - acme: - name: keycloakteams - - claims: - allowRenewalAfterExpiry: false - defaultTLSCertDuration: 2160h - disableRenewal: false - maxTLSCertDuration: 2160h - minTLSCertDuration: 60s - - dpop: - # key: - wireDomain: alpha.example.com - - oidc: - clientId: wireapp - # discoveryBaseUrl: https://keycloak.example.com/auth/realms/master - # issuerUrl: https://keycloak.example.com/auth/realms/master?client_id=wireapp - signatureAlgorithms: - - RS256 - - ES256 - - ES384 - - EdDSA - transform: | - { - "name": "{{ .name }}", - "preferred_username": "wireapp://%40{{ .preferred_username }}" - } - - x509: - # organization: alpha.example.com - template: | - { - "subject": { - "organization": {{ required "stepConfig.authority.acme.x509.organization is missing" .Values.stepConfig.authority.acme.x509.organization | toJson }}, - "commonName": {{ "{{" }} toJson .Oidc.name {{ "}}" }} - }, - "uris": [{{ "{{" }} toJson .Oidc.preferred_username {{ "}}" }}, {{ "{{" }} toJson .Dpop.sub {{ "}}" }}], - "keyUsage": ["digitalSignature"], - "extKeyUsage": ["clientAuth"], - "crlDistributionPoints": {{ tpl "[ https://{{ required \"stepConfig.dnsName is missing\" .Values.stepConfig.dnsName }}/crl ]" . | fromYamlArray | toJson }} - } - - configTemplate: |- - address: {{ required "stepConfig.address is missing" .Values.stepConfig.address }} - - dnsNames: - - {{ required "stepConfig.dnsName is missing" .Values.stepConfig.dnsName }} - {{- if .Values.stepConfig.additionalDNSNames }} - {{- .Values.stepConfig.additionalDNSNames | toYaml | nindent 2 }} - {{- end }} - - crt: {{ required "stepConfig.crt is missing" .Values.stepConfig.crt }} - key: {{ required "stepConfig.key is missing" .Values.stepConfig.key }} - root: {{ required "stepConfig.root is missing" .Values.stepConfig.root }} - - federatedRoots: - {{- required "stepConfig.federatedRoots is missing" .Values.stepConfig.federatedRoots | toYaml | nindent 2 }} - - crl: - enabled: true - generateOnRevoke: true - idpURL: https://{{ required "stepConfig.dnsName is missing" .Values.stepConfig.dnsName }}/crl - - db: - {{ required "stepConfig.db is missing" .Values.stepConfig.db | toYaml | nindent 2 }} - - tls: - {{ required "stepConfig.tls is missing" .Values.stepConfig.tls | toYaml | nindent 2 }} - - logger: - {{ required "stepConfig.logger is missing" .Values.stepConfig.logger | toYaml | nindent 2 }} - - authority: - claims: - {{ required "stepConfig.authority.claims is missing" .Values.stepConfig.authority.claims | toYaml | nindent 4 }} - provisioners: - - {{ required "stepConfig.authority.jwk is missing" .Values.stepConfig.authority.jwk | fromJson | toYaml | nindent 6 }} - - name: {{ required "stepConfig.authority.acme.name is missing" .Values.stepConfig.authority.acme.name }} - type: ACME - forceCN: true - challenges: - - wire-oidc-01 - - wire-dpop-01 - claims: - {{ required "stepConfig.authority.acme.claims is missing" .Values.stepConfig.authority.acme.claims | toYaml | nindent 8 }} - options: - wire: - dpop: - key: {{ required "stepConfig.authority.acme.dpop.key is missing" .Values.stepConfig.authority.acme.dpop.key }} - target: https://{{ required "stepConfig.authority.acme.dpop.wireDomain" .Values.stepConfig.authority.acme.dpop.wireDomain }}/clients/{{ "{{" }}.DeviceID{{ "}}" }}/access-token - oidc: - config: - clientId: {{ required "stepConfig.authority.acme.oidc.clientId is missing" .Values.stepConfig.authority.acme.oidc.clientId }} - signatureAlgorithms: - {{ required "stepConfig.authority.acme.oidc.signatureAlgorithms is missing" .Values.stepConfig.authority.acme.oidc.signatureAlgorithms | toYaml | nindent 14 }} - provider: - discoveryBaseUrl: {{ required "stepConfig.authority.acme.oidc.discoveryBaseUrl is missing" .Values.stepConfig.authority.acme.oidc.discoveryBaseUrl }} - id_token_signing_alg_values_supported: - {{ required "stepConfig.authority.acme.oidc.signatureAlgorithms is missing" .Values.stepConfig.authority.acme.oidc.signatureAlgorithms | toYaml | nindent 14 }} - issuerUrl: {{ required "stepConfig.authority.acme.oidc.issuerUrl is missing" .Values.stepConfig.authority.acme.oidc.issuerUrl }} - transform: {{ required "stepConfig.authority.acme.oidc.transform is missing" .Values.stepConfig.authority.acme.oidc.transform | toJson }} - x509: - template: {{ (tpl .Values.stepConfig.authority.acme.x509.template .) | toJson }} - - {{- if .Values.stepConfig.extraConfig }} - {{ .Values.stepConfig.extraconfig | toYaml }} - {{- end }} - - - diff --git a/charts/spar/templates/deployment.yaml b/charts/spar/templates/deployment.yaml index c09fc2beacd..5176bf3ebb2 100644 --- a/charts/spar/templates/deployment.yaml +++ b/charts/spar/templates/deployment.yaml @@ -26,6 +26,13 @@ spec: # 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 }} spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: spar volumes: - name: "spar-config" configMap: diff --git a/charts/wire-server/requirements.yaml b/charts/wire-server/requirements.yaml index 2d1fafb9674..60ed93fcbe0 100644 --- a/charts/wire-server/requirements.yaml +++ b/charts/wire-server/requirements.yaml @@ -96,7 +96,6 @@ dependencies: repository: "file://../background-worker" tags: - background-worker - - federation - haskellServices - services - name: integration diff --git a/charts/wire-server/templates/NOTES.txt b/charts/wire-server/templates/NOTES.txt index 2f93387327c..f6f59e0eda2 100644 --- a/charts/wire-server/templates/NOTES.txt +++ b/charts/wire-server/templates/NOTES.txt @@ -1,2 +1,5 @@ -TODO: write nice NOTES.txt +{{- if not (index .Values "brig" "config" "optSettings" "setRestrictUserCreation") }} +⚠️ ⚠️ ⚠️ User/Team creation is possible from outside the cluster, via Internet ⚠️ ⚠️ ⚠️ +To disable, Set brig.optSettings.setRestrictUserCreation to true. +{{- end }} diff --git a/charts/wire-server/values.yaml b/charts/wire-server/values.yaml index f0488133713..55900d494fc 100644 --- a/charts/wire-server/values.yaml +++ b/charts/wire-server/values.yaml @@ -7,7 +7,7 @@ tags: legalhold: false - federation: false # see also galley.config.enableFederation and brig.config.enableFederation + federation: false # see also {background-worker, brig, cargohold, galley}.config.enableFederation backoffice: false mlsstats: false integration: false diff --git a/deploy/dockerephemeral/coredns-config/db.example.com b/deploy/dockerephemeral/coredns-config/db.example.com index a458686bca7..46df3527882 100644 --- a/deploy/dockerephemeral/coredns-config/db.example.com +++ b/deploy/dockerephemeral/coredns-config/db.example.com @@ -18,3 +18,4 @@ _wire-server-federator._tcp.d1 IN SRV 0 0 10443 localhost. _wire-server-federator._tcp.d2 IN SRV 0 0 11443 localhost. _wire-server-federator._tcp.d3 IN SRV 0 0 12443 localhost. _wire-server-federator._tcp.federation-v0 IN SRV 0 0 21443 localhost. +_wire-server-federator._tcp.federation-v1 IN SRV 0 0 22443 localhost. diff --git a/deploy/dockerephemeral/docker-compose.yaml b/deploy/dockerephemeral/docker-compose.yaml index 58ff49b4c30..13061660d8c 100644 --- a/deploy/dockerephemeral/docker-compose.yaml +++ b/deploy/dockerephemeral/docker-compose.yaml @@ -58,9 +58,11 @@ services: basic_smtp: # needed for demo setup container_name: demo_wire_smtp - image: ixdotai/smtp:v0.5.2 + image: inbucket/inbucket:latest ports: - - 127.0.0.1:2500:25 + - 127.0.0.1:2500:2500 + - 127.0.0.1:1100:1100 + - 127.0.0.1:9000:9000 networks: - demo_wire @@ -262,7 +264,7 @@ services: rabbitmq: container_name: rabbitmq - image: rabbitmq:3.11-management-alpine + image: rabbitmq:3.13.7-management-alpine environment: - RABBITMQ_USERNAME - RABBITMQ_PASSWORD diff --git a/deploy/dockerephemeral/docker/elasticmq.conf b/deploy/dockerephemeral/docker/elasticmq.conf index 7cd41d7317e..77cdd11e4b4 100644 --- a/deploy/dockerephemeral/docker/elasticmq.conf +++ b/deploy/dockerephemeral/docker/elasticmq.conf @@ -43,6 +43,7 @@ queues { integration-brig-events4 = ${queues.default-queue-template} integration-brig-events5 = ${queues.default-queue-template} integration-brig-events-federation-v0 = ${queues.default-queue-template} + integration-brig-events-federation-v1 = ${queues.default-queue-template} integration-brig-events-internal = ${queues.default-queue-template} integration-brig-events-internal2 = ${queues.default-queue-template} @@ -50,6 +51,7 @@ queues { integration-brig-events-internal4 = ${queues.default-queue-template} integration-brig-events-internal5 = ${queues.default-queue-template} integration-brig-events-internal-federation-v0 = ${queues.default-queue-template} + integration-brig-events-internal-federation-v1 = ${queues.default-queue-template} "integration-user-events.fifo" = ${queues.fifo-queue-template} "integration-user-events2.fifo" = ${queues.fifo-queue-template} @@ -57,6 +59,7 @@ queues { "integration-user-events4.fifo" = ${queues.fifo-queue-template} "integration-user-events5.fifo" = ${queues.fifo-queue-template} "integration-user-events-federation-v0.fifo" = ${queues.fifo-queue-template} + "integration-user-events-federation-v1.fifo" = ${queues.fifo-queue-template} integration-gundeck-events = ${queues.default-queue-template} integration-gundeck-events2 = ${queues.default-queue-template} @@ -64,6 +67,7 @@ queues { integration-gundeck-events4 = ${queues.default-queue-template} integration-gundeck-events5 = ${queues.default-queue-template} integration-gundeck-events-federation-v0 = ${queues.default-queue-template} + integration-gundeck-events-federation-v1 = ${queues.default-queue-template} "integration-team-events.fifo" = ${queues.fifo-queue-template} "integration-team-events2.fifo" = ${queues.fifo-queue-template} @@ -71,10 +75,11 @@ queues { "integration-team-events4.fifo" = ${queues.fifo-queue-template} "integration-team-events5.fifo" = ${queues.fifo-queue-template} "integration-team-events-federation-v0.fifo" = ${queues.fifo-queue-template} + "integration-team-events-federation-v1.fifo" = ${queues.fifo-queue-template} } # Region and accountId which will be included in resource ids aws { region = eu-west-1 accountId = 000000000000 -} \ No newline at end of file +} diff --git a/deploy/dockerephemeral/docker/elasticsearch-ca.pem b/deploy/dockerephemeral/docker/elasticsearch-ca.pem index f17e9cb41ac..6511f688a73 100644 --- a/deploy/dockerephemeral/docker/elasticsearch-ca.pem +++ b/deploy/dockerephemeral/docker/elasticsearch-ca.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDLzCCAhegAwIBAgIUMGKU64YSPkGrWyHiXiLsuoKC/9owDQYJKoZIhvcNAQEL +MIIDLzCCAhegAwIBAgIUUOLn63PL3FEyGdhOK1ocDAn8dC8wDQYJKoZIhvcNAQEL BQAwJzElMCMGA1UEAwwcZWxhc3RpY3NlYXJjaC5jYS5leGFtcGxlLmNvbTAeFw0y -NDA2MTcxMzE1MzFaFw0zNDA2MTUxMzE1MzFaMCcxJTAjBgNVBAMMHGVsYXN0aWNz +NDA5MDMxMjAzMzhaFw0zNDA5MDExMjAzMzhaMCcxJTAjBgNVBAMMHGVsYXN0aWNz ZWFyY2guY2EuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK -AoIBAQC/oFJpJMdyG9FTpNw4K9f1pdkNikwbQsx4dokiQBMTu89IMTnNfsHz2IDr -xCKTCKC3oPupniaEPNprYpV6RMz1UPvUYu/IpvOXGeIGlVd9ixcoYN6763R2nZhM -lFS8Tma9mV+e/B0jr9DbV1pUWIPufuPrYXcOotxDO/W5I+GpKVTz/ZGD//O5odX1 -mJzkwqjeqGa1WNdg+/ALiDtVZ/YAKGdfjx81uqc16fYuYRDw3BYImBp5MyNu/jxd -gNxFB1edcVowvcKXVs5pSlay2ad0eQSa0Ux8n3RjfisjTLAHks/4dkPa3hQyBYzm -xwBhMcMDc06yxiCkXsVnlXRn9nf/AgMBAAGjUzBRMB0GA1UdDgQWBBSGMhy1Uvrs -lmdHKAGQ9avMSWhz2jAfBgNVHSMEGDAWgBSGMhy1UvrslmdHKAGQ9avMSWhz2jAP -BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA4vndI6NRcMgzba1y3 -lUPxy40bs/jQajR3A5fmCCX4c0ZeRc4YqE9cdYgeGffCZvPogyYjWDlavOma2uAQ -+3lZ35k0wG9GsU2g3fDIXpUuoSUjfYRLBQ3oqD7VRKYs1rDD87c+91DrsfIVZKF1 -W1RzOOvcW9QX2RHghFS4IluX6LEboo48cKtycA/nfmYDT/L9I4oYjaxc9l+HMUSH -gkQUU1xZnQ9GCqdhL3+2dmn0jvdgJLiFuefMGkE0oP/kFD/bhuOmDhpIDb10Cuck -Nw/nOSbBLINx2qDOa1f3Kox/PesQO4tp0dMp6XqZCOPTQ95vHsIOxuX1d+pxhX2V -ToWP +AoIBAQDhfp2hokSJ88qb6Gl8BBwg4jMbkt4l2ynOa/lO6DZFRheFlWZBaUZAcj1o +jBgbd3EoOXygZReL9TGBRrc5iACzytlOJkwMgNUUIq1KsPwWII41VJw3h3NO07tM +Tsf0kFvH1pEllJqorhQ1eZnU1SISyQcjk0oRQEyWd6arF7TLHna69OHF2ybYYXMD +MFWNr+O6t8RUfYs4kb7z1Nx3OnUKrUhIyaYeoyvBBOdXA5/G5GenDu/G4iVozsuL +gofYWu+77EnpProF0KRK+XlKakvF7bD26Qm1ol5qsXbdXOXKYq/c4KDdC938HNd/ +FNPz7EsnW0brBXqtz8TeQmxHix7DAgMBAAGjUzBRMB0GA1UdDgQWBBTWzp/VRTGq +s/IwlrROQGDwavTpyzAfBgNVHSMEGDAWgBTWzp/VRTGqs/IwlrROQGDwavTpyzAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBFue/SGpAVZ0TOVp8l +bdaaY/e9wUBSdCH5b39Nzd3rKmH3lIHqcafLlFCx+scKUIlFbohJr0aTK339wFfl +L0LzBJVUT9JzeDNPhk8pl/jBJk+eGP3fiykFMCgxxGvHtccHu8E/y8U0SeEtKqDn +Xy0ZbC3M54UedhDpHMovfHEsfN24Ev0DK13sBR2T8fmXCyCrfq887cCqJyP2ODgb +xAY/R4F8Ueadn0ywHYSY3MqmDsvDul0QlaOu2J5A0+k5oy4hAfFB8PzPYZrmPkeU +N5oxudTTihIZ+0JiL2JmWGBzMGzgtmD1rHC6lugUlWq+BoPu2+/+hn8RcVHBCFDk +WMSU -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/elasticsearch-cert.pem b/deploy/dockerephemeral/docker/elasticsearch-cert.pem index 3a9d4ad013f..99fe3c464aa 100755 --- a/deploy/dockerephemeral/docker/elasticsearch-cert.pem +++ b/deploy/dockerephemeral/docker/elasticsearch-cert.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- MIIDMDCCAhigAwIBAgIBADANBgkqhkiG9w0BAQsFADAnMSUwIwYDVQQDDBxlbGFz -dGljc2VhcmNoLmNhLmV4YW1wbGUuY29tMB4XDTI0MDYxNzEzMTUzMVoXDTI0MDcx -NzEzMTUzMVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAvWOmaFQEjlt8yqmMtpKFyoFoaGYfsGX5YhNoZOMtEtKX -F6ct1nIcJA9h5awgAlivRKeAkySUZSsWKCibaeNGneN9XTcrhedVpEtcz3js2CbB -1MDyfS9mrgt78uv4zQ5ZHY3wh6LC8k5Aj0aK2PJMNjJogIksO7zKBBGU/L+IMglU -j0kPIn8qiIxgNYRhqxQ0iQpiD065PrjU+jfwz7/Q1Oslq+Xxa9fY2+yYG1XMVdC8 -s2waBl953qv3gNtWZ3O+O3cS5egH/HiNKSWRmaoFebuI3RPAORbRVgHe1k/xTI7V -VE9A2IvHkETmd0Kx4qh66tAc2qayX4c979I7oA382QIDAQABo3oweDAdBgNVHSUE +dGljc2VhcmNoLmNhLmV4YW1wbGUuY29tMB4XDTI0MDkwMzEyMDMzOFoXDTM0MDkw +MTEyMDMzOFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAwDpnRRMmtz2g/5RBTO7FcuOsy3ss5g3E1npSOyQ4MebM +vM3fIZd+xVPNj6ZDxIHotLS17av41YmHCEtSgu0BtOda9hvn62HBR3Gyl6TDEvvC +ptHqxc+5ttrdKc5XqpI2lJsNUOUQhwQjYyTvbGwF5YxtHGc0mtUEJ7d/1qoT7A/y +W0KxTfclo7LPPBMiIF6Qzjn5iEguVPQm7jvQs9UbHad/ffMKDEBkBNs5joYTzLil +NFrnxxQKpxf4qS/cA62zeBS2dVvgZHqNeHfOF+v1DHItB/uoB9zRKFPvArI+Y+RR +uPsXuPNZgN210V55iDyDdU31Eh78ndShCSg6X3rfgQIDAQABo3oweDAdBgNVHSUE FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwFwYDVR0RAQH/BA0wC4IJbG9jYWxob3N0 -MB0GA1UdDgQWBBQDv9kWb35hEik7oNPxU62c6mt6UzAfBgNVHSMEGDAWgBSGMhy1 -UvrslmdHKAGQ9avMSWhz2jANBgkqhkiG9w0BAQsFAAOCAQEASMywZx+iTfpXH4Tu -C9261lD5Q2HZ3NtNRjGiImRjLhPQUt+5gLwwca0oiHBFa+xIt5MVwhnatJ2x8IZ1 -8ttQiqJUhXC8k62DVq1oMsgIusf+FaVxRQO5uCp5erroeUqJWvumC8013lNDjXHW -/X9PiouUTSndGI/pv6RokK+8VCT8mv7DvwhsTRyely51o7tCqHp6VjtD2wpm9ApW -qpySHKwEdwRSMvOIH2+x/Qa0ykFPKV1T+oqF4xM1x6ob06z3rS74uSK825g7Kyqd -zcjImK2DCVIkA3bSGxONQ/APTNd0TwAw9khhncjLJWjk1as6tuQGtpKWRA/01z+M -KHyT2Q== +MB0GA1UdDgQWBBTxqOE025egTOExeT139PqO6dxGKDAfBgNVHSMEGDAWgBTWzp/V +RTGqs/IwlrROQGDwavTpyzANBgkqhkiG9w0BAQsFAAOCAQEAWyJLKHLcz3oKVZnH +KP7AR0ty0m9H4yeHVPT7/IjfUsemDkFhk9xcSHlqVEqNu7CHL/VjZ6wke79yGm4L +zBIqiTGKgHmFRTn+19bNg/K/IodAXaTWayEzAwrJmEU2W6aarxhL6IiyHHnDba2J +u9h/cVV2OGODdg3+QuEr/3UV5XQX6X3hVGa3YUb/sTt1tuj4Rs9e1UCoSL2+4NtM +20De1G5zF0z05SP5z9H98sryf69PysJjmSWc601S4iR22o2nGDA/JrPBnVHfL0Cj +Aah5YYqY4m2llOwTGTrQdrzX2Oe2Qwcm1ofmn0P8Y4uYvqg9sUXKR1yf92PjytoE +/ZIJPA== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/elasticsearch-key.pem b/deploy/dockerephemeral/docker/elasticsearch-key.pem index 0f15c75e114..b4346d3579c 100755 --- a/deploy/dockerephemeral/docker/elasticsearch-key.pem +++ b/deploy/dockerephemeral/docker/elasticsearch-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC9Y6ZoVASOW3zK -qYy2koXKgWhoZh+wZfliE2hk4y0S0pcXpy3WchwkD2HlrCACWK9Ep4CTJJRlKxYo -KJtp40ad431dNyuF51WkS1zPeOzYJsHUwPJ9L2auC3vy6/jNDlkdjfCHosLyTkCP -RorY8kw2MmiAiSw7vMoEEZT8v4gyCVSPSQ8ifyqIjGA1hGGrFDSJCmIPTrk+uNT6 -N/DPv9DU6yWr5fFr19jb7JgbVcxV0LyzbBoGX3neq/eA21Znc747dxLl6Af8eI0p -JZGZqgV5u4jdE8A5FtFWAd7WT/FMjtVUT0DYi8eQROZ3QrHiqHrq0BzaprJfhz3v -0jugDfzZAgMBAAECggEAKZ8z3CvS0IJ0u4llnl43PxkPnBoNjtPqac6AG+P9bOyR -PiaEoWN0ocwrpLEeW8WnxzvUuwHIBy/f77V06mGDjIGJdKoCS6xamv/hBsu5qYti -/+HjqPV46HknpWyMwmwL0731BaoUk/H0qEhFjYY6j5KmetEqwnosH5bJmn5xbSVU -yrXSoWYX5yX9e2gL2QD3IyVdlIzbRnWwxaHhSUSo4jIlw7t/oaLL2gurzYQVpI6R -a+0HsQ81IulEIMH6iWZCyKn3NCcB/5TifA3e3DwjiYxYxGC2JmxRBb84F0pV8DhX -vETgIhG8vrkz8h2coCzYe7XIwiklMpbijMREpC6QnQKBgQDrfD0JjHWhQ9u1qCAb -E1vN+xVAZ9LUJVFoex+BeOjU0JkcM7i1tQy4mEcq8LGjpPCX7k5XqtMo2UUPDhLf -bppuNWCmFeDJjetj3b/zxEe0UMz4+Z8anW9AZpIJYkeRN6R4/ptiErbygxqr0Wus -inT+qRvjZuSz6ajj8qdeZun9NwKBgQDN42I84JYtViVptJeIG2TcJVSVRq24ADNy -w9V/y53Nc4zRCfR5Yz9pw0pRuFaSgaDZvKFGwU51j/8/t/nDyO7+Y4fFziiDpsFP -LBKc9fI4UTpPP8QEPBxQ7gK4vTN0ouziqQ1bA7kXF3mPh/g1rRBesEEFtnu+lcoR -nnz5HtlebwKBgHH30PqcFhoUY3NJiTBRcC8Cg8iF9w1hekLcw+S/hb/prRBvH8gh -daSpXlgz4WVX4HFHjnbzX/r3HGsq3otwViFciAgZso8ZtoDAw7PQnPtx16Hv/ca9 -xygd/DO6cvSfP2SnpMAUWqKIPRJG6pu47uKJKcwm8iz4uxqHR+VyXXCFAoGBAMPv -jlEDJshUgFxdigv0jgLX3+wEDFTclBm29xqcizu3qJ5TS/6tje639KVaucDJbmto -kU8FrgZBmJdqHV7OfWtJCzAa5wGLE9KlzbzkbrRb0RMUSxYAoq3+JEbtf+eTGb8H -RPeFzoKES6JlsrhaUAbc07R9GrygTmKAIszuJ80vAoGBAIudK5mEcqD8VNMnMAo/ -atWoImkCKLNDkAxr1E34BCorQ3ZvJZ5k+vjxTtiaOIzo6/qj1MAzfHBx22sCyJ36 -4RhCfk56XiAzZiwTRALDcd0l40Z6OoitwsXdXazeG6PMPleZmV+t7lejYfGokeI6 -6jRKZxwsF6kSk7XNnHmeB9qX +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDAOmdFEya3PaD/ +lEFM7sVy46zLeyzmDcTWelI7JDgx5sy8zd8hl37FU82PpkPEgei0tLXtq/jViYcI +S1KC7QG051r2G+frYcFHcbKXpMMS+8Km0erFz7m22t0pzleqkjaUmw1Q5RCHBCNj +JO9sbAXljG0cZzSa1QQnt3/WqhPsD/JbQrFN9yWjss88EyIgXpDOOfmISC5U9Cbu +O9Cz1Rsdp3998woMQGQE2zmOhhPMuKU0WufHFAqnF/ipL9wDrbN4FLZ1W+Bkeo14 +d84X6/UMci0H+6gH3NEoU+8Csj5j5FG4+xe481mA3bXRXnmIPIN1TfUSHvyd1KEJ +KDpfet+BAgMBAAECggEADItTJWgSSPdx2/PcDg3v3yc55b54R9wCqh9p4dejfigq +WLDTnJDTEkP9gGAQgJCcs7wuOiAUmTTEFde6fvZB/AD0B+b6y7rBnuytw6UaINFC +mtnMiROc8jCWqa2APY6Ulr6GkC6elT0BJS1qHWhwOxJepXGbtnXrsz7Pjh3jtm4X +8tZ6/msANBKGzaEKaIg41YxUbr4tWr1G73IlnWkJwVrfLTHm1sAjDTqoYFo3xdlK +Ow3vm5+Zscv6sZmRzZDUuEdaBuyqkHr5X3L9lncfxG6WdpCi5JUFfihxegnV5yi6 +5dL3Spu+ZdOrnHsQ/leaSEwzJOWAQlYvCrYoiOSKxQKBgQDuYeyff9JQkOxSlnzs +UO849VFQceltgjpiNF8D3O4bF6eE09qPljs0zWiGBvpwNLX+L/xJTEVv1625IB8I +W2SvBu+WhfwqlThwNBW0HWAn6V0ehEgu7dWaU2X5cNdyGdr8wqkLyZhFi8BepOyR +ytvO7Azeks/XnpJtZwhJzy1qFQKBgQDOb0MDwnEdYMmEZ3aDSvXO8xoifblrCTBl +ysCtj1jmplp0FV9VeWsuJawovzF8DaDtuCUOLBSimRU/56BflWRPivwSy2b70gJr +LXcvN1Wws2zBd6YJjqNCyu/d3eqLV8+YTL/ZpjyI5uEREfoR6mK3ze2S4KkOxPW1 +snvtcq7WvQKBgQDPfnMtvl/9ergJhy4DsMsZpAb8Y7rQdDuHgZh2z1Z+RI+vAYzL +0POGGYlyqB5Tjr4fG/uYfYgvOuffLQN2Db9Mzle7iLKfCjYPDHcbyToKY4mHZ5NB +Lgnwg8lOXxdZHQJNYs8sEHS3jFaMyzeUC6Rar4LgNaAuSbug+L7xKCGapQKBgHF1 +jWufjvQKojd3dherN3bK/m4+k45Uupj32vaJdt8uR0DODlu4JER0yC6NBvGbu/tr +3lHvwFets5QwBmEChuOBDBJ4YN2/Cz1E++CjlSFNPFUJIeTW1Lx9NWDH+4UieiLG +7Br/1v2Xh9QOAVefbyp+sDit6b0IW9PFiX90LMwxAoGBANorN+N/857GHDfZNadH +3z1TI3vFQCC99OZr98IUVJ2KR5/bxhnlknzY1BOqX0KVZxCelXUlyc1bPc9NZvDv +dpE2tuMf9Yi3QqUDMAz06NsBbJ07b/7Te+nPFFWnts6MLApM8BPDyvkqpWAiWMdd +5kIXKnYdDAyHqhfz46+yzxQa -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-ca.pem b/deploy/dockerephemeral/docker/redis-ca.pem index 85d169823c6..1bfe054b3ba 100644 --- a/deploy/dockerephemeral/docker/redis-ca.pem +++ b/deploy/dockerephemeral/docker/redis-ca.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- -MIIDHzCCAgegAwIBAgIUba2QSPicJVmpvwiyTu4YRiUzi5QwDQYJKoZIhvcNAQEL -BQAwHzEdMBsGA1UEAwwUcmVkaXMuY2EuZXhhbXBsZS5jb20wHhcNMjQwNjE3MTMx -NTMyWhcNMzQwNjE1MTMxNTMyWjAfMR0wGwYDVQQDDBRyZWRpcy5jYS5leGFtcGxl -LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALm/Ta6NRzTQLyTJ -PbktHqHLhRnlrtsCp3IfJ7JGiuc5HLJL1NbNbLw+XgZwjiVwmeQeZMjH8Sa7tRay -9OtqeP55zbgww4nZtTFKH6AdDVVJDg5X10xghijqhjRRUSh1dRxI4q4f/bjKhvc9 -Uk9B6gMIIS5gzS+XCf+WxQ0Zp2Zr11wbFlQ2ynp8Bb1k1Fyao3e0cHzIRrCn0qbv -VNOtNwDL5/M6sJyu3gvuxOGKhFJ9GzPtSYjTSIkQnddmoMQuDT6GZMo9RkcWTRFh -6f0EDan1iAIWNcK5NrHZKA/L3gPLIb+d29HuKbZcdgcQLfMkfgX49cTDcSv3XI90 -Fz1IAVMCAwEAAaNTMFEwHQYDVR0OBBYEFIeV2duiox4T4NjZWcFgRiS44y44MB8G -A1UdIwQYMBaAFIeV2duiox4T4NjZWcFgRiS44y44MA8GA1UdEwEB/wQFMAMBAf8w -DQYJKoZIhvcNAQELBQADggEBAG1E1db7eaoS5OW+7XQcXHPpqvIP1GRPnsetN+L/ -1fc5lH0lzRyiY2BHNJUMsENiDXMbgPzuVR0Eks8i8goSM9F9rZK7znpnesgS3ec9 -alTIDHIgsgSrRTJWXsGFq4GH1atcjX3nkxETx/o4sV9MC2h5SrfiKnO7nc+/LUDC -hxrGLQikDmt+thygMG8LguCtEAVr8QghbAGxPyKKCLai4S8w+Mo1YtQYLLKhSeWl -Wmf+IpdLXZy1MS/G3b0Wy5py8ZkYQORL0UQMk2kCFj3J5m2N1xo0KsiXY7yZE9Wr -XNeZPtygtjDqTME+GvPB6vQloizMom8E3p40vdSx3Rsr+wc= +MIIDHzCCAgegAwIBAgIUUXTxTo88ZrFDrX/oilOFIxNMKggwDQYJKoZIhvcNAQEL +BQAwHzEdMBsGA1UEAwwUcmVkaXMuY2EuZXhhbXBsZS5jb20wHhcNMjQwOTAzMTIw +MzM4WhcNMzQwOTAxMTIwMzM4WjAfMR0wGwYDVQQDDBRyZWRpcy5jYS5leGFtcGxl +LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK3RiwFgh+OKAzEj +1N4q4L00E1mz8my/SG4aANMYh2CklukXyWqCkmMxi7nTgoLGO1Y3uWHfOOEjMZqV +VYQ6bWRTpcYwurhNKbAJX52mRveBpBnLVH4cjtxbDC3rfTKAec71/3f3V+xk66dC +obT7cjEm4ogAnF5AAIRruD5UUGBBwins5Ao8hz1BtOW9RgR0Emtq3Evz4SNeNV4z +M3+kAOKRI4DeTx8H8GEEo04pkWHABxBHife2JhlznbHukiaKK8znTqOvpLbpqmZm +QqjpEGabgjtVLCV9RzHqg7i4iEWcBmRAsNLOtgf/BBIzbLhOPLA34mIJaD8q0bAs +UemvCFUCAwEAAaNTMFEwHQYDVR0OBBYEFI1yBFqrix10ZHSpYPd5VPXrxxQsMB8G +A1UdIwQYMBaAFI1yBFqrix10ZHSpYPd5VPXrxxQsMA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQELBQADggEBAJJqSvqx9iO4FALty9cOBo6TM96FqsgGbKVH+U0w +2C1su2utw1HrmWBcMI8lOlPEC0dDwpyTZgOq835tEafzisa+y79s97jz2WjFpdvN +jExEzY06zZbV3AGHo+u/TwLBABCVU7NGlN1pnCJj8SqAkXkKE9E4jzTUc57BqZTi +jkQSy0kx3lDpJpd26oGwm91sJaCQfD/54SUs7Ev5BLTAZL0XjXdORLn1Y5vxTqUr +e2ydqDIn9lnBapgBA5ceifmj44Xbq7NCiLZPy+tDi0uR+i12kFAr1SymY9tXan+2 +pq5p4t2O2CLIHgvVWaKZ/IKi2YKP1JwVGC3EN6B0KoCZce4= -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-1-cert.pem b/deploy/dockerephemeral/docker/redis-node-1-cert.pem index 37bd4bc75dd..7756f82bbd0 100644 --- a/deploy/dockerephemeral/docker/redis-node-1-cert.pem +++ b/deploy/dockerephemeral/docker/redis-node-1-cert.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA2MTcxMzE1MzJaFw0yNDA3MTcxMzE1MzJa -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCkyI311U2ZCCKvnPqv -K0y5A8CI8A3W146s2490ReOZp1xA+l1bPcVJan2N0Na/kLNYo9Lm1xbuNWxadllq -0DnhYTMzP48Rlh69lPL1GjWWI0vZjC4qcv0r6k4DrKVn6yvzs8jQDiykvsIXHcPi -OovQ+ol30xd01KV8k7CsAgFpDON9PgaLKTV5S9I2R+zfTGWHWZCfJlJea2fbf6Ui -O4VwNCO62C46aRLUh0qgdkqvjts1BV9/rzeLQ6UQBU3o4h+9obTOI56ZaUk5fU5v -mb9P0Fj+NLlEqIb2Zl7IopwiIBQSzA+3USFYMQl/fppyCm5X7OrQ1tjJNZ3Tpm+K -7CflAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy0xhwSsFAAfMB0GA1UdDgQWBBT7rOsZpR0sBNmrAIIj -raJniMK0FTAfBgNVHSMEGDAWgBSHldnboqMeE+DY2VnBYEYkuOMuODANBgkqhkiG -9w0BAQsFAAOCAQEAcYPqHdms1aYR5aWdqJYPRgydaAdTp14J6jXNQh8NU9jMIV0S -CTVuZwuSMoiMzQXezicHJjMc5YZTvB6SHNi0bidvx48Xuw/JUvlDHVysZgPZYR11 -diAsp+iD0+EB2hR5vHseehwTmyfyVIbFvWXNDNvRrU628gzWUlC4adsUVue8xsfp -dzQQUJKizO4sBM9hpxjF2iWnRDsE/QZPmPpuRD3ys8ym08zUH+R3dLFbNuDkWb9t -mr8IQJI6eALdbcn9vVHlGIluRni4Oe9d/lZ+adbLvbwsZsyeUldn/VzPUIAFE1P9 -HqWg9/JFnc3EQeuLGEqea+nk6WCHJyU5w7GETQ== +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTtp3U0VPwBVJNULQF +S4BBlWBNf/8NMidOq23IsTcjkIFWO1XL+HFZoa1AArUSA/TaLBYyz9WmX7eLWvAU +ADM6mfAf2V6whmIs2H9ZRnY89bFWO2hzLWWp1qq3dXK1ywTLpw7DqU4OT0rtYZbp +QHeVY0mKKspF+YJTZzWB1hs8IX9355wXRlYBLPNQ5oHRb4/16J/UUFPIJjpUyHsq +T1LWmVREqisrq9u50FnNPeLXE6SDnHGRkYGQXzQOM/yAI75/QUOOqo5rt3Et52t5 +pkOT45R0PbAC2UpR1usew0zVjRoQfFk9n38tXUSHKw/tW+ZY1xJqEKEiLGfnhhza +t4kjAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy0xhwSsFAAfMB0GA1UdDgQWBBQsOxsq4X8dS/Ddl9l1 +TWDb8Q5KKzAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG +9w0BAQsFAAOCAQEAaByeD08xOZCV0ZKsx7lHtiem5/XG01rMcDxNVrVguSD+mqhR +/j8ciTW2CruJ2X8ReTjNrI4X1nWLbh4rsrA56q4xkjkgJIfWQAdKCibXTrHOWfk5 +dcYG1pqVdpD5bvsxAsY95jxqoVJHXHGN8ynC+lV39HbDJQFOdHLAP66NUrphp76a +OZKiuzUS6naeiHWoA9eIANFRz/JoQvyp109gdce5MH0iFwGFqNJU2rwilOpzQVc7 +qldx7MHMnW5UYSTqryTOr8PS+xo24TSdHjIXmnOO3Ov0Pw7iPpGVGj56dAKgEisG +yGOAWYto8UBWKLox1vSSlfdkhAoDXluvE8EwRw== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-1-key.pem b/deploy/dockerephemeral/docker/redis-node-1-key.pem index 024e676a48f..6d8b29bbdee 100644 --- a/deploy/dockerephemeral/docker/redis-node-1-key.pem +++ b/deploy/dockerephemeral/docker/redis-node-1-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCkyI311U2ZCCKv -nPqvK0y5A8CI8A3W146s2490ReOZp1xA+l1bPcVJan2N0Na/kLNYo9Lm1xbuNWxa -dllq0DnhYTMzP48Rlh69lPL1GjWWI0vZjC4qcv0r6k4DrKVn6yvzs8jQDiykvsIX -HcPiOovQ+ol30xd01KV8k7CsAgFpDON9PgaLKTV5S9I2R+zfTGWHWZCfJlJea2fb -f6UiO4VwNCO62C46aRLUh0qgdkqvjts1BV9/rzeLQ6UQBU3o4h+9obTOI56ZaUk5 -fU5vmb9P0Fj+NLlEqIb2Zl7IopwiIBQSzA+3USFYMQl/fppyCm5X7OrQ1tjJNZ3T -pm+K7CflAgMBAAECggEAKKucSBbVoGXe47+nqrjR5p8rs9Cl5ccNnpHRQgYm4uto -7Fuu04B3M0POicRH4H+XGFNU0Cc5sGDspZYswx1yD7O1FprDFbjazPlYjtChdbUv -+RltYoo/fMmHaEZCC9hCIJPYxisdbyrqzhhJWsqO7C0N5U5rLWl3j7wHAKk9Dl9b -lIdn+AlEiA2cpAk/5rqSZysOv0+v56jh1ay6Hqsp7jm9NAdEoSpJMDwZ/FJxu4PZ -vvxxpACJhyVZkJuuJPc68HbwXIPhEImPzc2TA2Zok+PdgEpnNWpiSGainsaDY6l3 -9XQabptbwhHke0eikQQ+27PXFl5XuvU6qHBEXnFXQQKBgQDdDRILeIUyZHFRJCaJ -uaK9/0IXgTD2p5/ef6a5RcoSEDda+fvKUbd7MlLnLkSOWko27uheVwnpVC3RtgQf -XmMenbAcsh+3a6bDHBK1VwJZAHTb11aLOVzBiof16FTr89OO5gu4WEM2XedOW+Lz -o6QJQsoJfR/6Q7jfwSwNQR54pQKBgQC+1hRuRJsJiaDRIq/T/9dmNhMdPMw1ojM2 -i4EeS1wYUnDr2D/KfpDFKZ/uVobFekH1eqKOQhaUkND9/kHyx/4XBZYcabxxyC2w -SoY26G6ha/gKNlgKhioqAtMc8f7caNwZADrggYIilfPc2uQAaAtKbWb8TuJlGDmF -WcxPRvSOQQKBgQCEZo3GXRu6wTq2VSbYG16E2t1lYrZHJsO061SbaFfOVfQyA8Vy -u1tg6RWK7sWVVjNZj+OSjiObpBYFpDX36/sGnYCcz3v7yvkJqEj0YPdBA+r6upJV -tbf/HNCu08f5xAOVdejTM9qeN8SRxKu9LujTuzN0V4PNzL5xFy0hiz2LGQKBgEO1 -CMKmrKsRnXEV8XQyDWZCQT3aWEmfJrRvgnwRGLe4aEAFFXzussaBIjEZme9ulQBX -Zl06rXBAgSXck+Fje48HeF7UVPu5nhwyFLReewHioLpe1ZXGTCdjoStf4KCqw4xL -PJhy2o0SztbJAqPyRi896ZATHNfpZF8foRFvh00BAoGAIrvHzt0EBLUaquJEKotX -b56F7s7uhgoc/ugcHgAK/b7K82B4/3K8lg3naynmabkU0/rdmcnyVfyXnjVFevpe -szUiOX+zG20LJnN9G589fRxFJM9Vny2WLV+7y7VoDoLl5BkjR9VBUo11aYVW97QH -Vr72lA4ZaymQK2MtMWlTXsQ= +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDTtp3U0VPwBVJN +ULQFS4BBlWBNf/8NMidOq23IsTcjkIFWO1XL+HFZoa1AArUSA/TaLBYyz9WmX7eL +WvAUADM6mfAf2V6whmIs2H9ZRnY89bFWO2hzLWWp1qq3dXK1ywTLpw7DqU4OT0rt +YZbpQHeVY0mKKspF+YJTZzWB1hs8IX9355wXRlYBLPNQ5oHRb4/16J/UUFPIJjpU +yHsqT1LWmVREqisrq9u50FnNPeLXE6SDnHGRkYGQXzQOM/yAI75/QUOOqo5rt3Et +52t5pkOT45R0PbAC2UpR1usew0zVjRoQfFk9n38tXUSHKw/tW+ZY1xJqEKEiLGfn +hhzat4kjAgMBAAECggEAFqMmoixVxMrU34Z7ETve9WRC/VZrz53mvQ8weG6WfjuD +0NQcWuhwOkzCyR7g/JGmuzNOllVJu3Xtmr15ATJ6R9BQ8B7edJKR6cimaUXS+7ar +pRRKGVKn1a6p517sCoswMpRkzEAMpBQPZ21xZPRrNPJ+WQM1SKEiscdN3dmmZNng +MvroH1dPVbyZ49xkjMQ0NaOtk4rvopzdKKZea2qz41w/vXR9hShnfVDs/q86clmx +5mnEvXcEdguioAfUWz+qQ7dXlWsASKa/gAMjUN9GW9uOn4LclFsVCD2MW+IUJMxe ++JtFM0xiQ3HaK0Fem8+XR8mG3BB5a/06ZHBfcv/lsQKBgQD4WSJjZwMBG80uGidR +ls+VhhFjysxm5qrF34MWziLczi1nAStc/PzVcA7tHapKX5JiKYT6d8Ptngz6FLIo +/72OshmLzctxRprlpihWxMIYOqwb2PLB0//ghuUE81Zbxj1MQ6k9WbTGHBwUbaiv +PSzclhmMubypfLLcmMEnHeZFswKBgQDaPIWmyax3Eft8DzC3Om7X3WMN0NXE96z2 +6hUAon5tqinMuWUWa2cyWzPsdBgFM8mCynoiIu08YFpZQivoB6QSal4x2mLg4R+u +aLm3h9f6NS4/VvpWPL5wMUAqeCCbP/2PVKk///0mtQGixUOxeQftTncQeLtfXOXd +4gDJHjfW0QKBgQDND7xnW42Ngsk+wfWpVt981UDSp4dziA+GZ3I0iG0c6Vlv7fVC +SNrz2h1ZCN+tnZCfYS0eK3oqYBDTBfe+Br0ccE7Ls1fC5svLyBES5FBn9TpbnB2G +kmh7mqbMGak7CktfB5dcww+TbW56J7nbSKYcVgwuuMbhI8gEglUq2XNkJQKBgQDV +VojIzSmdlKSlWCwlUif9OdyVKutuizg4gAhcAH1bMxd9nFbnncLaBTIzGiJJI6EA +DHNsX3xOo1pvGzLUtnN71SOT1IsIjsprstCqS0+ktswo+xvppaP9BQhW++vUGLAE +p5x0hgixCA07U1+jZE+NekEGhx+UT7oeN8rQ0IuBoQKBgQC4PF4WwqashYHkYW2j +4LaMu5kWY/0OI9Vh/h1iOcKPzVUn61aabjsx1wF9rummIdxP03/bs7ZpkwPypcVR +v7XnNbi+hDZFEN6s/+Gl4S6RfAbWXs3sgnhVlctlkzzwG8UHCef4DWMPxFI1JQI8 +X+SdDfpmB/ayQb8TlYvke/s8cQ== -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-2-cert.pem b/deploy/dockerephemeral/docker/redis-node-2-cert.pem index 4681392ce71..ea4b4507d6b 100644 --- a/deploy/dockerephemeral/docker/redis-node-2-cert.pem +++ b/deploy/dockerephemeral/docker/redis-node-2-cert.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA2MTcxMzE1MzJaFw0yNDA3MTcxMzE1MzJa -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDusjRUdb9cOIGhRXVG -WqsawsdIqljT2r2G7wTtSPfTPpLqMY1rcw1VJdHKG4Kx8SYEZZ02IOjHkV8Ik7/m -0kiKUNo+nRURvNkWsedoQt8/5NvL6O0d15BoHMSjJMkQYKDew2pEbcR3YyLrndjl -qRv4QSaESA1c854IejG1V91Tvk7KC4jqmisZEz6hrHg5XBPGk0cTb3rAQhFgpZo1 -tpjhc9CHQzNv+FM6lgy/n53kpTDYpGJgYN1I+lqU1qN29WHKaMNHXBj3GT9uZ64e -4IPmbCIn+U3+KWZACaD2HbDq8QLcTFxT3kyETH27UPkETDa56pPHgqRWziaNbwgS -cZx5AgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy0yhwSsFAAgMB0GA1UdDgQWBBRd+gHa2Eis8uVk//hq -jqoBINup4DAfBgNVHSMEGDAWgBSHldnboqMeE+DY2VnBYEYkuOMuODANBgkqhkiG -9w0BAQsFAAOCAQEAV8hQotbxJAdXEyZQjQPmG8AmUZSi4U8LnMDe9od58sD59J7o -m26WbNvq7tDRVpBsrUCk/rfVT8I26h1ImS2tlZtyW5LiKOi9t3I3W1s2fnWk6GBg -tH3SKf0aZRw96RSoYa7DNp4MilRtF3pQF8rg78b3BYaAUezCe2KO9Ddlym2YhAth -rzSY4cek3Gsd62SFyq8ufFq54Q3pImcVF6shGidmqfeAgRRXumekdDSr7rkvEJ+5 -6fAVMhLRu4YGGH5SPF2dauFgpMCgMFHp5uUmAcq4sLbnLZhSckuzaagoaQRZAALT -NdJ9nFHupmUGV/dagYSx+cFccEVJ0nl29YF4qg== +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4mm9GWOVh0ttlzXaJ +11/rQQX3vYQ3zyeMdz/KGTKArOF+pxVCxbETlI3ZufO8Ht9zqa7Doh5R86iNtVMR +LoQZVWeXjQsMATwNUZT3lEOezpDE0ZI8d5JyU946Z+7s0VjIMbXOzjTSjTNSi57N +li59/1NTG5CW9EtgnnYoP5SOrYTpK+fzawXD18tD8kq/VBLt8OoG7xn6DIpGsFr9 +h1Ot/yrUejvrHg2KIi3av/cnqA8twzFpkdvGSEarjRuYG6fHGL67dgSpLvzh/v7h +QiJDFFB8fHnUc5ioZXFw88P4Oq7UlzBhnkC8nhUi1X1vWoF9Xz4FXXJ1P4WkZfWB +Vui7AgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy0yhwSsFAAgMB0GA1UdDgQWBBQQK2od431iWKznJEQz +zy5GXgt1DDAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG +9w0BAQsFAAOCAQEAhvNbLzlY4sS/xmn8alzIYjY/uIc5c0PaUaXc7SSjeoChRfNQ +tE5YLmOo86WYNThtaNmiRLFv3yNBXCcqdVgNdL78EIQlKvPxHwzZXxkKDmOcfIZS +nUa4w+OmKJLsdNjphBGmR94h8WycwoFMThw55vnTJ2+AnCFPsLDfjtHiKB8AsW8u +gtSTtVyu+QyvGTDxEFDgqFgyFjJpVp37bOakRuzuZZ8VUssQbb11YHyhnNGTcL3a +hLXeGVSRA7SyDXxxRs5PmmJVsUOWkgbIjguvZK5APpqaGEYwBYo036DFSgt6DTOu +8YsCTeSOmue0xNlPDiVPSP8HUGfq3tTBKMXbUQ== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-2-key.pem b/deploy/dockerephemeral/docker/redis-node-2-key.pem index ab7fffcf64c..fba9118998e 100644 --- a/deploy/dockerephemeral/docker/redis-node-2-key.pem +++ b/deploy/dockerephemeral/docker/redis-node-2-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDusjRUdb9cOIGh -RXVGWqsawsdIqljT2r2G7wTtSPfTPpLqMY1rcw1VJdHKG4Kx8SYEZZ02IOjHkV8I -k7/m0kiKUNo+nRURvNkWsedoQt8/5NvL6O0d15BoHMSjJMkQYKDew2pEbcR3YyLr -ndjlqRv4QSaESA1c854IejG1V91Tvk7KC4jqmisZEz6hrHg5XBPGk0cTb3rAQhFg -pZo1tpjhc9CHQzNv+FM6lgy/n53kpTDYpGJgYN1I+lqU1qN29WHKaMNHXBj3GT9u -Z64e4IPmbCIn+U3+KWZACaD2HbDq8QLcTFxT3kyETH27UPkETDa56pPHgqRWziaN -bwgScZx5AgMBAAECggEAGaBQNfEeRkxavnGykYcSb6ERvB9twfDuABqRMNhwouFI -7JO9VxfXCpkw2L3zXh9BsZ8nLbSCyUo2JbmXFLTmzNK5W5eJt4nK1MDs0yi6xyVO -46lyK44FFuhfxBQi8fstyjy4n/gY66hdC2a67o0lT5XPCMyjgqM1CDv2Mj3oqSDE -QXxQtT3sSVLWGT+ztQhcLBdUpIG6Q3qaXr/JTLDDNn3kIAB9XOw21tDibRPGdw/P -54b4fx7x9K/0bYZg0STh6xWUBTM3geUEW3tRUMkaqtbjXltu12j6rf6FfBOip397 -pdER/YCFn23nIHAn44jp60S4eT2p9QYPxPcAeFnIcQKBgQD/VYO+G3peH7fKI9FO -kY8bLIr9aiF4F8AzPBeUgJjK3MrhkV2wZULT/VD0JMJubAICfSwYgBvQXiDNwD82 -CeMasapzlolnMTa7zYyjIYZSeBXvJpOcSyuPNy8DCPp5mwfEXeoGcpbuUjPBIBsD -rHO45gFS75kf6YBO8/h3AuJNOwKBgQDvUZSqTDDF3sF/Z2C7Kn2cBgoB6tsUqTQV -ZdKRjSoIjSI3XoPzyCQdLrnq1bn7aUXt6IlQySZNjJ4hXr/yduf141l8j4XTNMPe -kisPNNwIPvsDOVO/27+emn44Py4IMIr3kdwoO7YnVHXu4IM6DEhbVC3Pi0glSxol -ydODQh/x2wKBgQCsog6+vClR9jP3MZxUeMm+37DhgZ47aiODAIAY4ZFspzdspzIn -D2/NkJnpV+k1a0U4lZT4w7UKfnnDYtXaHXk1FSZfnEouQPH2rBUIPqRoodSCqxxm -MdSzseXRMYLYMV9g/vY5gcRWQbHIQ4LASxq6ypfekSyAjQk5WG6HWKXU/QKBgQDl -ejqtmWVjNxggDIbKshHEHF5YPFVa2Gyi4AIro0rc7EgVA8JPbmiCux13Ov2dP/LY -EBQrrNXXorC2mt4/pxkBxME4GX9faMcwksRLTop2Nb4H916BKDvz33yMfrirDbET -d3+97JPb3rc/GXV7oe93854B1zKU4BDwjzkMMcnj5QKBgHsko7YzcZKjmaEV9ecr -/9wrBA5OkckoxeJo5qlqxg7p63V7gEZ6/QjfJcuCvDMEYMKjhgb8bbq+JgCyjCHB -0dll5cH6Foe7RTePT07zhAEutLxUU32XiwKtN9dyBQlXoMmJl8o+G+pfcWb59jJx -Rv22/ufIlLl6Z4JZ9RWM3/Ka +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4mm9GWOVh0ttl +zXaJ11/rQQX3vYQ3zyeMdz/KGTKArOF+pxVCxbETlI3ZufO8Ht9zqa7Doh5R86iN +tVMRLoQZVWeXjQsMATwNUZT3lEOezpDE0ZI8d5JyU946Z+7s0VjIMbXOzjTSjTNS +i57Nli59/1NTG5CW9EtgnnYoP5SOrYTpK+fzawXD18tD8kq/VBLt8OoG7xn6DIpG +sFr9h1Ot/yrUejvrHg2KIi3av/cnqA8twzFpkdvGSEarjRuYG6fHGL67dgSpLvzh +/v7hQiJDFFB8fHnUc5ioZXFw88P4Oq7UlzBhnkC8nhUi1X1vWoF9Xz4FXXJ1P4Wk +ZfWBVui7AgMBAAECggEAONB+8r2lSygkEf7cPqwkfzjx5z9SlAKTf22sGj0LCAMt +G1e8+WHyj74msh3C3+D4kJZmjRs2Da7Z71MhD6arTUi1qzTjc3xlyQuUt2XQMe4N +LCX7xdRfJASf3oXiSMxdcK+r7swUAcEnTH5gD5HrGSgdsvRG2c6x7DiY0OZQiGBk +2rPHQDUKeb7Z0YLWc8nldzlnOe2OeWpFVEraAOzmANnV5FVJZP8RNoiKviZnB77O +qbA6Xtpg4ytVhMaymUmjkjdFuxm5XcMCOIz4W9SZVJ9uSzjZqATzgjsiOWYozB7q +2xb1yOyCVPgf+dZj32D8DvqSrwwRBR3LcNhnj2wUIQKBgQDqL4VNp54Lrf+oZ0ZF +h3s6lL2NquY0xHs91YvoO187VetyUlNjOcGXt8ROhSSAf6qTLQvrreVdjYHr52xr +smCohhQ9QDm3d+Inh3ARgr75O577aPwJHBmo0fnu9h6OkDr8nx05SthW4XenHqoE +iWQ9FnibAFz5KLBSYC7x9wfGaQKBgQDJzI3UC6AqQS8ILbcqHm7ZmnpUUjn7vPUm +lkB3/YtV7ewWJhFzdPdaKHKe2YO9WXQTCF7iPRK3+gWt8uh4DWCrSObBkmSUlF66 +wbRof3lsYiWDPed9OTgoDHRwbMPeYrJ3A0TMrGJQsbedljneaat+DM3kNgjgChfW +JiL0g9c5gwKBgQDBi8zMRT/lv0SQVepKBJLf85ZFw3zHF6wTiq46nPcz/uq8bTXl +yBIr5gEkM/3bBahgQtabTflGvHEoGvgMejxQi5+mj7Ij47zRlqoUjs5vBct7VWUX +0lWSpRe/W0Id6S4XIxnwA9+Qzn8pa7pwTWy+4BeFY2NzuSEgs8WYzOVsIQKBgHbI +IPOfpDc7ByQZRKdWIomTlE3t2JOFNgfwiSIX69w4n66p2bvMLYy0IkO+ZP0fmmNZ +mgAxUsNYN9+cC5oexbgMwUdPlESg0OG9AyQ/ZImXe900ov3ioFtyeVdzrhdIoSPM +mMKg9X3qHdp0gruYF4mqn8akx7SYPE+hQxIKSLVhAoGAJP+TshJj8xAeE1Uroyc/ +yIWThbp0Q/EFaXkpS6aJqBjdcLfh2U+Zo9ZaTn9OBlzXHk9WttzeWuMY9PrINodJ +8DSg5f0PslYxJ5DQuKnDWUeqX3zCnXkgnymlvh78t6wWp+BUAEjI8qH5IgKVwKd+ +VJbPX4mzhAl/0kIablU6SqM= -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-3-cert.pem b/deploy/dockerephemeral/docker/redis-node-3-cert.pem index f676a744e30..e550d0e30f9 100644 --- a/deploy/dockerephemeral/docker/redis-node-3-cert.pem +++ b/deploy/dockerephemeral/docker/redis-node-3-cert.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA2MTcxMzE1MzNaFw0yNDA3MTcxMzE1MzNa -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDYU44MmK1l+2zjBrHY -/ORzWNWNsM9cMuh31KuNBDf1yD8Wg4YxfRgrqI2la88qRVNz3bUr5P8P/ubk1UH2 -agK5Drta7fYkvDPhveeTIOKz9l/ojxb4mXQ5aNmRZThtftokSbnPj8rCLRTvwpxW -wtZGBPAOTHcIRAZs3w8XPIFY/2FnOILHMpGUD6MrG0texcV05GLi10ZEevVb4tPl -1QF4dvyQdGjpOZ9qVn27xl2GAxX7yOlxC5AgLS7HuzLyCP5eyB4i7hRK06XjrVu+ -VUi1nzrOneDJzBFZLhcY1ktEKnmqvZ9Wh6eZGepXo8lV6QCH77OJ1TNSL2ke8qUb -IMJHAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy0zhwSsFAAhMB0GA1UdDgQWBBSq82oTIR7xwMx2Cfim -DbOoj8FPajAfBgNVHSMEGDAWgBSHldnboqMeE+DY2VnBYEYkuOMuODANBgkqhkiG -9w0BAQsFAAOCAQEATue+pKPQpi+RUUNsxi7REOmKjVwvEOUePqsovmXzE8aC3P8v -akpVDDggA7JeAgWcFfFng8SimNTq+TqfNRx06E7MYc0Fcekqa1wfTe7eEdaHrekd -vR/HvKONaenxQ0jDD7PLQi+8dZAvValb9avw8howkrQlt0lLt3KVUHepO9ErDJ3P -ymhAl1Dc/8PiwH0wicXmJSnxSpIttv1WHW1wj31G8f6D8W8k6i8fQYPeMI/OyGu6 -tpSe8SXZ/P/trEVFCWISYaq861jufkaTdHGKVMv+rL0E+Ow+zmLmeBRLq+rrmTT7 -gWCV/wl41D9nrzWYtSmBEnwcDHK7eRqb5NiGmA== +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCd72omHFn1mEFw/GBp +gkPM5BkF7giGx7GOLyijCoi4NLNVKJn6mOJt9vX2PbBYedy1OcskObLbEwqUwcZr +7fVim34xrE4AmdJqBWTkcMFnhbjzYIynfvejej/05kWlzp3JuhTpi7i2W+nnZjqb +S6UHgeTwF/iENA1oysuq0jC4oaVGNa2ZCoz3W+uAEbpUYNjN7/uQeEwRyZjSEJUY +KyG69Wrl9KnzBX0mkltq8rJiCqaG+qOZwP+XH7TxjYM1SlAxLHrnjDQHWyZXJzPY +fikRk2Zf8nDobA5thXVR/2PicDxUs1VyGYSg/vK1EMwOIHIZdxalo0x75vFjBJ9T +l+HFAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy0zhwSsFAAhMB0GA1UdDgQWBBQyljx2OR3L7yZLVax4 +MLTDhj4xPjAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG +9w0BAQsFAAOCAQEAUv3JN0ip/LWmtWHyqzPuq9tbVFs2M5waRO2ZZtEp6Pzudr9x +JKrmtz7IlnwK2E3eqw1Hh3kZYiM5XT2GzqFjPn+Na32i3IsR/S1Y4ZDq6T1WOjht +u+3EjrUvpTXAcLfaO60gJ7DrfC4PsuNuaRr23BiF3lIb7A693hnESg3EnUqGvAvA +ikR/Cv48kAvxpFlXZfnGApFEP49svj676emodRUlk4aCOjIniPByLF318Dl+MwzW +KbnjynzjnOqfcXeD67axFqIBAhZPBDWIDOLNo/ASAROkPntycBGFPUL+Wgdq75vs +8WnftwfCzYtKcASNVSeoSFtJhVy2cAqHK1bd/g== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-3-key.pem b/deploy/dockerephemeral/docker/redis-node-3-key.pem index 0264fce611d..d7be5cf147d 100644 --- a/deploy/dockerephemeral/docker/redis-node-3-key.pem +++ b/deploy/dockerephemeral/docker/redis-node-3-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDYU44MmK1l+2zj -BrHY/ORzWNWNsM9cMuh31KuNBDf1yD8Wg4YxfRgrqI2la88qRVNz3bUr5P8P/ubk -1UH2agK5Drta7fYkvDPhveeTIOKz9l/ojxb4mXQ5aNmRZThtftokSbnPj8rCLRTv -wpxWwtZGBPAOTHcIRAZs3w8XPIFY/2FnOILHMpGUD6MrG0texcV05GLi10ZEevVb -4tPl1QF4dvyQdGjpOZ9qVn27xl2GAxX7yOlxC5AgLS7HuzLyCP5eyB4i7hRK06Xj -rVu+VUi1nzrOneDJzBFZLhcY1ktEKnmqvZ9Wh6eZGepXo8lV6QCH77OJ1TNSL2ke -8qUbIMJHAgMBAAECggEAMnVG2GRSacu8CbZZjHHsfYU2hq67p1dOhwjpnOJjhSZY -pNE331o85Y4SwAeGEmeKQCfyJtNqtRnxVGXz1VzD1tN7WwnPVKE7fsezeMt+ZZit -pUqfAoyUogF1YicYgt3IVxeFSkdRdXpbfFNJ8SjQHxPuxH8McrafQwzCcdqQlyfI -fnhNYxHt9lL262lywRLkuAwXB69cUdLXaemfvNVcTW9+QUnz+Emx3KnlhRyRlMNn -hgkwsp4NB4nElHKHntYaoVlEqrDRJyz7mCXNviHb/WC7kNznLEArzPJJa4YjAedy -P9kXTlYEkUcmrv/furc7wrAYeJ/+qQ27ToGk+JI9UQKBgQD0LsPKfD/ep8Y7Qcvl -VOSYqUrjQ9azW2IXkK8L5U5IOZbXScsB3gdxhLhSk2MDz2R8n0BUlYEihdzRBA4K -aH+4qW0OipwjQD+qUU5oW9SJ6SaHRp8Mpq7d/mSR7HnNfXCz60YjYPID+71nK+6q -FcBvFvLxopQt7ZDFaONHMWNodwKBgQDiy6wP5b3ncfG3LzapKM73X765scTUXuZa -ow0aHMJ9nRi0aiLnlGzIPwh3QU5L5mpG+gAIUlaguI29x08BtkeorKhOJ0M/taT1 -nbf0FLIQBm2uzpm4ICTlGEi/drUwssw5kFou4AA3pscdY7dam7BRb7eMcaABECz9 -WlJldDC4sQKBgA4vNUJq607k0hgZH14IC2tu0iHXi/5JPa5+whxfyqdZaRDCgZ9v -JWGLwyVQ2HydLIosuhDvyluWCRi/Mo2aOmkgtmwU0zMdBVXAeVyIkRUdzRYonQ6g -FCJjJ7ZuVTkBo21gKmfdttFSa1M18xxAPTh2zdAJkLAGT9WX3TQCg3LLAoGAXQKE -LPzeNdXP+H0/YH5g6qh0cnlKLIJC3Db0P5o91QAhSpQgfnKrbjATi7zXnF8BhNww -OTlzV3R4hLUBXMVhe/ZbC7okZTNcVHJ7J3l5UQMh5kfKWO2t09pyszq+shsRkCX4 -JjMtQ6V9ETt8zYb991fmoY1TvjvhB4IMOpk9BfECgYBZ6TaBR81zK0bX2dJpvWBu -ECwG9vg80NuM8UIjeQ7iCzfSD/6MsYDedU8+u7seV7LZx01DISUFu6q4rWpncJ/1 -W3+LU/apgy7+jeJmUjN0lFCEbf7f0h4x4GGXYbDCiPdzcrL7g5gcwaaf0+0PO4bZ -t7SfNCX7wVFT8ipYG2vTNQ== +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCd72omHFn1mEFw +/GBpgkPM5BkF7giGx7GOLyijCoi4NLNVKJn6mOJt9vX2PbBYedy1OcskObLbEwqU +wcZr7fVim34xrE4AmdJqBWTkcMFnhbjzYIynfvejej/05kWlzp3JuhTpi7i2W+nn +ZjqbS6UHgeTwF/iENA1oysuq0jC4oaVGNa2ZCoz3W+uAEbpUYNjN7/uQeEwRyZjS +EJUYKyG69Wrl9KnzBX0mkltq8rJiCqaG+qOZwP+XH7TxjYM1SlAxLHrnjDQHWyZX +JzPYfikRk2Zf8nDobA5thXVR/2PicDxUs1VyGYSg/vK1EMwOIHIZdxalo0x75vFj +BJ9Tl+HFAgMBAAECggEABYejI9UiS+MaMiaOtE2x/16NMb6f4Hg600umFJoDJ3qm +PM5rIHHHRn+7JPVhU00RA+y+HB/uZJVKGDigsJloWhzaUkrs1ZXiiYEe2JDKH3cj +KVexamabrRxUA53RxSMdizlPZM4A7axSMvP1YV1IrfadBCW9Ydj2DzvqiFShDWst +asKPAa6MAU63zfZZaBQvicswd1nJUvc8ZNp1p0JiVcwWPWVTYH9d2c+0WZLlfCHm +GxUurHwyVc6b7T4OSrsiDaQN0kdLJDAYowp+T94JDBCH3m4e/NF9W6gkoO2UGXTH +6A9HVDI3FwUBzXdT9rL/Wmp4kKXB4xO2TU/yeZoYAQKBgQDK2Q2vG+BucY2aJxGw +7HNeXov2lLma2Vn4TRr+cyzcXH7Jmc8J/h9RMU7AEfg3CQwMbXE60P561/1q1e0Z +fD55x9ka3FZ2dG+a5CDzjkqnUgnLYOK1bxx5UUq+Sf6IeNjGPikejRPcPBmvFVuu +NvoPU0HwWLm67BnantJIpUFvRQKBgQDHUaPa6SIMGAWHasI6EvZMGgBAy5iJa4s3 +o+DuESF+6lD989ZnOltsPFeYhwbIzm14EzhK/y4MVR46gXLMZ9FwlGCGdXE7LWiN +VKCm9kRcxcH9Sak70LkZ9yv08Nl45f9vTOzBcKzu6bgZ2LOeSJ0oTmiVEb98pL7N +w6XxD2iQgQKBgAVVPYncBsOAksN5wXpQTRwvCij6cgLDMh1YEZyc9JH6kI7GT24o +0zP0QujD0C3KPBnbir2MHxSltxDm/OvNm2riOS/+mPtWRlThKIiethG+E2nYaz1v +5WS/IWLtWRbHbpOPsM8P0HTa06YJvrZO1bYvby1dd8yVRny77jVgut6tAoGAHpMK +ZHkgjORebMBWnNvtxgyy/z1735CMoXNU/I/KKJK+68WsnNcZ0QeMlEwaIVFw/1tL +Zk2wfZnM8kKLHonKWc+Y4uc/AEnd4NgbcKEUKXr4X+cdu5wv2KjOqFsNsPru7N7K +7n1fOaLGZ8iS/PO8j8M/TaaUTgVjc2LQoKKxcoECgYAtPzq1Y0yc22M+m1m6nK/W +L7rsUI0zDs0VZcJ5mrJg8nahOM/f+BsFYN5oAHYxuXPUyynZyD2nPtdsES75DGOH +PEqr9DhgSig4JmHS/6SEBnWql+zyNdn1/FaYOkKRHiY7jNhjTayiDObJrXg0g4OT +BmzY39BABb52ogQbjWslow== -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-4-cert.pem b/deploy/dockerephemeral/docker/redis-node-4-cert.pem index e8ebd88bce0..185f8f97014 100644 --- a/deploy/dockerephemeral/docker/redis-node-4-cert.pem +++ b/deploy/dockerephemeral/docker/redis-node-4-cert.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA2MTcxMzE1MzNaFw0yNDA3MTcxMzE1MzNa -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvitE2tBIZ5zco7Acg -zZXive6YAUJTrsyfLAFWWJo7rOZJ3vlVvKlGgRLcDRTqS7Rjs5ZiKKjO+vbCo9GH -akHz1hzDQpXUoSeyRlu/AausQbBB1ZhDOzlcg2bmhhG3CYswrSSXM6GdV7C1J4wS -0pmXoZJO9QKvgdskkgHn4I8RGGIshtKRA040yoRQPNJeC7QUOZj94YC+pyR0Yb0e -pawKDllkhpCIosg9TWqEeOFTdm4ibN+g4M5xihqsHTkBlQjvb4U45ybZyJdT3bw2 -inN2f/FZsyOp8as4JoMbXnp/Bwd01Ze/cQ5pVVz4pnvW9jaUzJ5yRi4du3v70Ft0 -mP4TAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy00hwSsFAAiMB0GA1UdDgQWBBR2P9NFa+sluP6Ic0su -lqWSIP2i4zAfBgNVHSMEGDAWgBSHldnboqMeE+DY2VnBYEYkuOMuODANBgkqhkiG -9w0BAQsFAAOCAQEAjInARY3/TdhAT0RJDdDLyxfx/NF3VD+L0GlA5YGAqj9lLyr9 -rE96N7y6/imhc8r+zecHKcJVNZ+NkA9YHhK0NqkC8UXcV/te6KWxe8KbvrFfuhep -BlWQ0RhQYUDDoFuyZ9FoH+gdynz3OU1J1LyGZG280O5/QQL+ON5t9rD1wYCcTRM6 -zlUyWtbUWxrGVvGClRn6lrTNphOTxBtKM0cqXD6jnnGUqLhCY1y801HHzfJ07jIY -b5iLW+kRiwdnDIvuiJ8gRmqHgr+rHwpv15HfumedQBUuZsVTPpcFiAqs1wrT8BFn -EWPtolHtCOwd57X4UP/LpPnUAHwQJPmloDaL8g== +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDKxOmFo5c4ae38qC3z +I89R1F6xjaMyR6jjd6k5qsW7eU/y8+trgY4HV/jbzD3CDOjMkj70la5EiV+GTA0i +GeJH/BkKjqEPsIy/vAPux9xt2ZpIO9ieO2BF75ojrcM7tAbeOLQNAgYA7zAyIpQk +J2P8IyOYSJ31ujLJCR7d0zudAbXJXfAAyPUWqUrmmRHIY7hRi1tUv74JARqnU2tH +ZhFgGyBCaLROK69S/Wy+xPKo5w9Ol5L9eIccrK2/JwNpfsFAxJqXawNm1l1M9gGk +2MpQXzZeTg/hlusqCtPieOPUQKoEDXAgYArQy8iYkLuZzOtg2WwcPOhtfsgVRLNE +wXihAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy00hwSsFAAiMB0GA1UdDgQWBBQrI/peejY55qjXOc6W +XUU+/q6R+TAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG +9w0BAQsFAAOCAQEAdf1N+gPpnkEHzDAMnK4kUCHq2ymLBBWJVAPDcmmtcMjEiEVC +/9BU+hcdqgLXxonEqiA4kEs9Mkj8AcUk0Dzl5Gfk2haZO6yzVEp97zwto+3Tgzya +0l6bvRv4OSfdVeSTYx8T48h23O8FBD/Gp9l5sFOZgc1TCWrb7ReJQS+XThAksIdW +DLvwbOU1I2qRL3ZbT49FAhmVcrMkHJjzkugXDoGG3Rgdzx/HePUjXWdWC1L+7/Kn +U/7w72ymW1mC5PbjoW9zzkVKesj++mhzSb5+sXa/is3hUJ17zy4Bqc71Mb2q8tqM +G/uMrdwfPeoad3qRVPRsK8QlVnJ0eIpiUDk+Ow== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-4-key.pem b/deploy/dockerephemeral/docker/redis-node-4-key.pem index 5da453ec076..355661d6a99 100644 --- a/deploy/dockerephemeral/docker/redis-node-4-key.pem +++ b/deploy/dockerephemeral/docker/redis-node-4-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCvitE2tBIZ5zco -7AcgzZXive6YAUJTrsyfLAFWWJo7rOZJ3vlVvKlGgRLcDRTqS7Rjs5ZiKKjO+vbC -o9GHakHz1hzDQpXUoSeyRlu/AausQbBB1ZhDOzlcg2bmhhG3CYswrSSXM6GdV7C1 -J4wS0pmXoZJO9QKvgdskkgHn4I8RGGIshtKRA040yoRQPNJeC7QUOZj94YC+pyR0 -Yb0epawKDllkhpCIosg9TWqEeOFTdm4ibN+g4M5xihqsHTkBlQjvb4U45ybZyJdT -3bw2inN2f/FZsyOp8as4JoMbXnp/Bwd01Ze/cQ5pVVz4pnvW9jaUzJ5yRi4du3v7 -0Ft0mP4TAgMBAAECggEACYE2L8STQFTNH0mcXzHSfkrzasaSrU5HJQ0wa1jzzOxh -MbnBfVtwPPGLMGAC9Gax9z4Hk/wIm+Bp0QMmurLNrGK4/veRfkhVimkV2aNBBNwv -q3jhvC4uPmyc+zliJyt8nl+Znhg9FXRkjIJ+Kpy9lUC5182bXh5lW7cOJFx70pyM -5+UZ3+ogNnGqEtTSRSEKR0TzCLC6hORmlWCbnyYCaVG/H3tKAjg8Mwb7vFpreXSQ -QXqeRT8i/wVcWGvnuYyhkIfuLWOFXMswazLPyf1H8pF6xAxqeaX5QKhv19mE6xf3 -A7ZdBgsCigWcKuNDb9Fx2s/5xZM5SygzbStOvH27xQKBgQDbcZr3mZx2NaYLbDWX -Rk9b/plm7sYBCbenUXHZVE4px2DJ+r4V5XjVvzHM8cLRcNmSPhZJjamhVNsLNUOS -L39ouxuSqarLd7bxB5i2vCUEqnqVs2JhgVAjucIuOhtEGhk1yR9gGIgadXYhO93Q -bdHbop2qgxut7XWPWhuqe/u5zwKBgQDMyP0dOpElgmpzJ9mzgNNvwtxzA4uqllEH -Mst1I+8mQRRVEaqmB+TpfeoR/OUKKKk3XcziDTLCCkwyplahDW7AHbYeizevg++6 -G/09Z0XyZZ51L7LxjVi1ORWyCDrTFMASjPzUGedcIANKkCHZ5q3971iSyjFmXrKa -G/i6pyF8fQKBgQDIuDQf7/CuK0oyvoqSUOx73/ger56LCoFi2NtDB5rrGgRNGz3N -N3T8RgLeS/B/tDI+Uu3930beW4hzyweAalOmzyZcUzb3HwxFkUY9NwDBMNIppcgC -Gc7crqePsvSHqTuP9+Pr+ORdFz2zDlhIsnq25BpFAeFKiJ30Pl555SgN/wKBgGV/ -UISGHJ58rwn4PFxNg34nFGAk57pa2jo5IMIkV0mcg9lN8khsLTbU44ia0WJhmM0K -Ppvjcr7dn7qS2ujj4Xpyv2sQET96ovyZFsCySObFGu51/7jdF5RqgKhGj/FCnZgU -LNNrK1Jrw3XXTg/T13S+hiXq9OUKFndvWa4ZW+15AoGABGPWL6H0hQWyzYq1CZl6 -kdCW0b1cZeuJyl8C3MCFq5f20myvTehua0DdROT+MaLJQ+20etJ9TRHpKiUKhcgq -gJ9y/8tcMG0hMPjQkFzFsHuDiIE3UOtJd9kUwLbl97WUZX1EZ5jmIVdCpcsx6WcU -PFoetX5NdFoY2jhfWym5WcY= +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDKxOmFo5c4ae38 +qC3zI89R1F6xjaMyR6jjd6k5qsW7eU/y8+trgY4HV/jbzD3CDOjMkj70la5EiV+G +TA0iGeJH/BkKjqEPsIy/vAPux9xt2ZpIO9ieO2BF75ojrcM7tAbeOLQNAgYA7zAy +IpQkJ2P8IyOYSJ31ujLJCR7d0zudAbXJXfAAyPUWqUrmmRHIY7hRi1tUv74JARqn +U2tHZhFgGyBCaLROK69S/Wy+xPKo5w9Ol5L9eIccrK2/JwNpfsFAxJqXawNm1l1M +9gGk2MpQXzZeTg/hlusqCtPieOPUQKoEDXAgYArQy8iYkLuZzOtg2WwcPOhtfsgV +RLNEwXihAgMBAAECggEABRxG5XEc0dVro9tKQy9DHaUcWN3Av/bp5QfCSluJPcMe +Nnma1JwQjBNVyJZidRZVtLg34Xq3SG9s6qnWh+Y+m4FZUTiMiyRwO7HdqII9hkA+ +gPUPLdfBwql6CU2rFsFgDfBAa3aCV7ovjQftk2axwKxTDJbB8mxFtObnsgANp9SU +c+MTlNTs1IQ4ev4u1i9ntR8SlFMcYQUA2AxvOiEDu7b4x/Ph9TEGuR6wLxdImRq/ +7hXcPtGAJKYgZLzAwCrZrjGjHILSskTxdii+Tr52Aq75SA3tLYGkJfSxHTJjFe0u +1k4Ot4uSEjRf4DIwohbSFFbK/ZXG2uscn36OphtbUQKBgQDwQY263RPJ/M5mKvME +15DK1JW3DOLWCBiV0XzwXsS+QpE8pKs2YLeyrY7sV/w1tdnfNdfINCknuzC4tG7Y +I+QzCQGhyKrP2nj4K3SsKUcFk6OWxgiPF5CRmlWySJ+H6+yITKcSJt/ZjUvvGQyQ +TV+IQ8s4RbKII9Pvifai6SLJ2QKBgQDYDn4bqIfZKR0I46//AycGXAUl55Yfgeog +8CR5MatNz26crrmDzjnDgsRbKUxK+UZLl/zEXY5Npn06sOG1G0bO/t7wQqcPsXZt +rZTx58lKvW7LQhEBAz48y9QeK3WUvT1E3JMJ6rt+6IfHvbvCLIu9DwyGJ7Zc7N+6 +k5GduC9gCQKBgQC4Zdfd3+hcUwgnKjezM7ARvO/buqwvEa+s7UgzRMlELdtC7C/s +YHcdUFAt3anZn2VFCBJBuqcLs4RFf1bD1WhEM1lpTparSUcnUlMN//Beu14HTp8r +FC8FUasMVuj6bXzxb8ObDvMoCmaJcHRQHNKBx2amHfhUvQrhAsalasIkoQKBgAFo +XsP5XiE5FlpXeW8U6y0sblAn6R99bjQWvHYZr78LCfJ1ZPoJ3vB6KqNZaojWhPG7 +JMd2wJWa7xfxzRar/dMdcABqvsHoaxgd2GmXFAWrpEwouwmhpscooNItgE+eyAZp +1X9sCxqxkyjnAJEsTyDFN1Ssb5C9blu92GYJrC1ZAoGASChIICMp0HWrDSRxRCen +Fddf993aEI4e46NTWY54u2p0Ga62XUcaw5eND9QX6craD8nd7mMhwvdvQ5vuORBk +m+dqt0oU5cloVp0srHDA861CO8topJFaNGWdF4wDgLU8YKRzd6hNX8X0/CCRl1vd +z/YmtxfgU56SaqExe0X65eA= -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-5-cert.pem b/deploy/dockerephemeral/docker/redis-node-5-cert.pem index d771a21b461..e1221b9df77 100644 --- a/deploy/dockerephemeral/docker/redis-node-5-cert.pem +++ b/deploy/dockerephemeral/docker/redis-node-5-cert.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA2MTcxMzE1MzNaFw0yNDA3MTcxMzE1MzNa -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCiYYwsg3+qF8hXrp0A -ni/h4oySwTN7JBsElWzipzoY5k5VLZWdYITaYc+mypys1OSHiVsDff12FGWAsKMD -ItoFC2jnMGx9FQcXMokNRmEdmvcOMEx6Y314U/63HzAKYC7XCrV6TdK12zmVxiCc -pZ7Iz8Ch7bzeFTQQY4cdvA9sJJeJ5oJ5Tm/JJGgSzNPBOHbdeDuprQayihA3Hfac -19oM7tZGEqvjk/otzxmi0X7qMKFO43cxD4URqHWa6So2T2g//HvwMoq0AUajmXUI -9DYgownjfZNJ0ISEouuPLHe4C3jdG8ku24r25cugpkY57zN7BlDU0trOFk4TeeyM -HXD1AgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy01hwSsFAAjMB0GA1UdDgQWBBTM1a20hY1IgokeC3tT -zg6shjIA7TAfBgNVHSMEGDAWgBSHldnboqMeE+DY2VnBYEYkuOMuODANBgkqhkiG -9w0BAQsFAAOCAQEABtQ69VGkEHPkIotuB6kqtz7LtDAf4D7N1lIE3pib02n+5wHi -ITzzv0uuNpdzAPfvaR8OU3/8uNzA6GvrspNLaDbhRnXdTI4eDpFro+vRGvsqaLPa -FWpooa+zNgoIqPzQ/3exN6nA0APYqvxRUcAdsaP3C4clecBvHWOpZya3Q1sdvCH9 -b0Fidfb24D0B6arHrx3hEwufmamkMxOnvUFh7mqyEwuyb2lF2x0VKT5/u8+rfOSj -+xmv0A5gsc/Q1jIkzdfGco7i+BWbINS8dj77ajDykpxvbdP1mGzXCqJdBkxaFhO+ -iLUAqbXLDw47wCi3Pe7wDaWFqQfgs97j585lUA== +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCSXrnTzrNfHudXjC0A +h0CiFRe3yp4j6cN3Hfv4snS6tPWXM4MH9Dka8zMLZvzRVQZK3PxDh/R/DQYBZhpy +LEvT7wYCDsS+F+tie2sPjzSAbdM5dolD8fGwACOqobI4vPz0QrwDqHde/OdVWAZl +h5Pzw5rDUu84CdfPSWRN1pomCFWG7gVkpuFzIcBfz+smPodyw3BfU8969q6tFACE +pjGPF/RufmHoIaHe2q/c+3HBY06ro0oTqTtRe36v4Jp2HLE/jE8wc+YggTmHE670 +uEXIR9N3fF3AbPVnhimEwcQ5fpJtMonUvfj5Z/4KfKo/0Yrh0wljeRiz/tZgTwwb +h5ATAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy01hwSsFAAjMB0GA1UdDgQWBBRkbb1LScfQthztQJ3l +R+QFCKjXUTAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG +9w0BAQsFAAOCAQEAn8TOFqomU30SmIDIHYBKMRGq3bVDLkDDC2yy6LCCwwG2rpoO +UtnUMig2w3iNQ6nvqR4LJB1ha0hLK5FP3iX/JcqZiO0NaucOTe7aJlt9taCADgAw +4vRW/pDuxtq7H1hc2pOue6i05UtGqy2E12jYowQc8a/5hylfEO3b5t5Z7xoQzyAZ +1ov7sYatBinwhqyDI5qNvZCuyT7SMx7H10T7cPrEec4uq55AJ0ReXnAAy1MLhpGd +nW5FX3F4gnyJcK2xL/V+ScL4NTzA8qWT+qOK33KxU1qrGripAkFaF6Z110nuIDiP +Z2tneIovCKKChgFsmZjy2spRpDw6R3Am6rXjpA== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-5-key.pem b/deploy/dockerephemeral/docker/redis-node-5-key.pem index 6167a8e8275..467778629cd 100644 --- a/deploy/dockerephemeral/docker/redis-node-5-key.pem +++ b/deploy/dockerephemeral/docker/redis-node-5-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCiYYwsg3+qF8hX -rp0Ani/h4oySwTN7JBsElWzipzoY5k5VLZWdYITaYc+mypys1OSHiVsDff12FGWA -sKMDItoFC2jnMGx9FQcXMokNRmEdmvcOMEx6Y314U/63HzAKYC7XCrV6TdK12zmV -xiCcpZ7Iz8Ch7bzeFTQQY4cdvA9sJJeJ5oJ5Tm/JJGgSzNPBOHbdeDuprQayihA3 -Hfac19oM7tZGEqvjk/otzxmi0X7qMKFO43cxD4URqHWa6So2T2g//HvwMoq0AUaj -mXUI9DYgownjfZNJ0ISEouuPLHe4C3jdG8ku24r25cugpkY57zN7BlDU0trOFk4T -eeyMHXD1AgMBAAECggEAHJX6IIr8wkOiDgeMFaQDb2tbzmkLKFI8nGO20bbZRDFl -GFsoQ9aORMijzuvLxaRL3+1nE5gOMweXr95IsEBmK62sx8hPTPzS7PtFQ8xAQ/84 -H2wSxpf1qmV1CaVIpob0sAAvXwrMvZ2Mh2ij7Je+eoESWx9YWKtYaUswKeSlvWZy -2OQVQ72eX3MTMTjwXBTqo7cVC+j0/yMxAha6lRRd4BlWDuUi9VZIRS2LC934sPbx -dDDm3qkP9zXF4xg/8MrWe5BTA9pxVcAT+RMIhnk5g+mjHxKL3B7Klbhzn+SLWc9A -1TWRntwbUC+8RYWqCNesAdoLIlEJrNUncOhY+CrPgQKBgQDVFO9yUbp0uMeAJFD2 -qQEwnybIWyB0SWUEq91kFQ3cSFoVSiIvaLYZM6n9lSRwxdXS/KtLnhdyUbkOR0rD -VOJmPf136MwlTHMud3YPxku7YWEDH40BIbFvBNUYj9y4GpPGANen7NgF4Vnn8T6G -7SInfx79y+JhQ+Oxd2DFikQXgQKBgQDDFljiqPQeGUz6B/y4CHBfc+e8eagdt3km -NXsgFuGmu3ksy5uBcgCebXthh3coKPeP2cIwob4sfnjq8vuOWSsIqquLZmidyyb9 -ARBQ/CtSnBXAyakoOnI4usPQmrbFq58xIh6I0MRk1N1L1D2MTz4i6QZzRekxAFdc -nBpaclAzdQKBgFfvadm9zLr6vqotUpRYrrsIExNAOCaFW4EQBC+XWL79xN9gVrdF -+VBxN8gE0qMPoeyOhYqRVY/CFiLEXSA7WatkDcR8eDM0V5xnhHuCFCLiTwzg6mn7 -I6RzVBXs2OPJZA6krlsIrSXQGDBWKL26AwxVs859Y5FMWR0V7QPYyb0BAoGAJZ4A -g6wqbkdYpXm2zFGsQWubCqe2uAwxyyFS3Ywr9Ld/lRipops17VaVDOhPHKpRmiZW -IIR/pBq6/CrgQMGG38PxEg8sKwkKOozi9Yq6W9KHC0aXXI9wiOnSaj368kC2kIXQ -t3bx97Nn/IAvYgfBpn+iY8XeQjmbntrm5fvW5SUCgYBmqOIT2yiFGjJybTvwcDr8 -Lz9QpCnP2mHYiTmzXnPZGCaLIeSmXxCr4YDTgKajKcx0NVZmX/iGm9VDrfZn593w -EVy/oxg+vpME2RnnBLDstO8dVMOuSs8/ao0PZWylkuC+5bMvYO8iaGk3EZSX2fmY -AH0a1dtdGMveeGsFqnQyjg== +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCSXrnTzrNfHudX +jC0Ah0CiFRe3yp4j6cN3Hfv4snS6tPWXM4MH9Dka8zMLZvzRVQZK3PxDh/R/DQYB +ZhpyLEvT7wYCDsS+F+tie2sPjzSAbdM5dolD8fGwACOqobI4vPz0QrwDqHde/OdV +WAZlh5Pzw5rDUu84CdfPSWRN1pomCFWG7gVkpuFzIcBfz+smPodyw3BfU8969q6t +FACEpjGPF/RufmHoIaHe2q/c+3HBY06ro0oTqTtRe36v4Jp2HLE/jE8wc+YggTmH +E670uEXIR9N3fF3AbPVnhimEwcQ5fpJtMonUvfj5Z/4KfKo/0Yrh0wljeRiz/tZg +Twwbh5ATAgMBAAECggEAFEPciJsaseBueUGQItLsZkRzVzVMtdOHW4uhjOpFnRVc +LLCrbe4opeGRaf0P+HpEIm38LewPNDP9ETPYv4FV3PmVTwhKbGNAFLovtXocnmzA +4jjWLRESEaMYombmwlJFghq8kJPCNeWKsIHnyNDU8YVd0mM+JE0V6GjUjq5YA0x4 +co87wiNxAtjdNuAmI8elOqH3YhCwCQjYO1NeEJwIhWz5tgb2J9Rvn85LOh65cU17 +FaxiTBNSrMW44yk+uhEyj8IDbcZax0s8gLbCSLIj/MnuSbm74VkGKXme0U3mJSmn +dY2tpO3DnNZ+qvakpUk50e/LXYFofsJH9cs1BlZ7AQKBgQDJufGOp07KcIG4N3ei +YxH1IRZ8vThOHksbKVnzQLRcJcEY6SHrL3DgxS+kO3IfjkKZGw5vZgV4/jfTfWQP +eDXwIl0t/YVCEDECppfAIN7fyvIVI14quRogbIrn0jn5ijhVzPI8SWvi/viFbFvn +2O/8KUaHudv9yQ6zKItZ1zHAkwKBgQC5wBLKYdeQT5EfvfXT+rHoioUyywFxTpOF +em14JfNwKLdhqEVB99MzGEdRs6HNz88YbhKQpuEQjwJkbBXZUpAXyYPLDN5uQtV7 +Xw1MY7d8O7U5qNevos+Yti8rrv4w8Cb8ppOX0DJ2SD7J4OQjuyiRYx6sE+tQH6p+ +6N2Gt9YigQKBgQCqpnt7s3uK9Aw42+t/2xFo7lnIooYMR8I/swaeKsGpJmMpAKep +/pMeApHf/E359e3O+b2HbaX5ig2OAwhvscDnaRqsekiN74aWeHntlaEVbujGCwpx +V++LOGd13zkeKdiodN0DNRVojUuOC3HgO3whNIWu8gLxuXGPDCB+mvZCswKBgH+I +vh4QgZYG22iE37U0ylQUT5HpSktGnQknXuQAgp1+hzJY+3xosKzDPax9/lk2FkX6 +xWpl+d+JoSXcBEBbbK24YXHXmxzvbG4xfAr36DI3OJ2nLLfdvFVouQhwNPza1pnf +sTSp8Qu/XMT1UQ6rYRY5jQSvBIDVzRUnw3nM3QyBAoGAXs5Mg1jcQme6X56e0Db0 +zDCcEJuYL+nWXSkClsQCaDwafi4PQVP/V351Qruw0n98grD5vacz1HdXosvCaACJ +8P8e4sFJmSGu8SQt4zbReq8DHNTWZyPC8muurnMSKtfg3XulY8SFsoog7dlMzGGY +IMDiEb5jIb6DFcpNxjigXsM= -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-6-cert.pem b/deploy/dockerephemeral/docker/redis-node-6-cert.pem index 9e323b2b5cd..c176eae043d 100644 --- a/deploy/dockerephemeral/docker/redis-node-6-cert.pem +++ b/deploy/dockerephemeral/docker/redis-node-6-cert.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA2MTcxMzE1MzNaFw0yNDA3MTcxMzE1MzNa -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6YjcRhT/CWKdXQXIZ -+qee0UPul0gw+QHPcc8MHsO06bDN8y40//sa2/5fhp80SkFCZSlT99tCBBO+M8Nx -ALADOGvl01aOL9LY9O2nXkya6/6DkIsV+GssBtC1OIBOiSrHfHy+C7ICbV1Ax5Nk -HWEXpkKc8kAZo3ETDqXzCoYq+01qgb12RBBwQxz0yxHDOZcfXFaffIM3+Wv7XnHp -RT22tWJuw7h5TTxx9u1dhZKBWERa2kVUhA6/Ihk/zCpASWbRwOf355jTNAuO+pQT -mXFDwr+/JcenBiwCQzxaTFkUDPwy0UvhrKvK6WsXuySNO6QNFZXPyuus2IOlnXd0 -8+vBAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy02hwSsFAAkMB0GA1UdDgQWBBR6J2P3AJJ/LRcvVhcE -nrAvvFDyqTAfBgNVHSMEGDAWgBSHldnboqMeE+DY2VnBYEYkuOMuODANBgkqhkiG -9w0BAQsFAAOCAQEAtt28vPDd4DnT9/vIfIAR0xdLVrw6EGBqrsDtGHbF+0SJ0GJA -+2DxQazJGGgBYXqfjZz1+yBImHP3Rio8+gdU84C+K3CKsa6k4N76f2Ym85FrOOjY -nMNVhdPSdlFptq67euCbSJc9fzXE6Aq73Zm9dRtsLQVmYOAMOkw6EPXNLrwVRaCP -FUswJctD0RcRareRsgiDVgRXfPBzfuxMYMYNwWNcQ6R9dL1r0db4O3Py4L2GkB3o -ukPcoemA2FA1ExA+shzzXBIBr2aK79VkaWPzoUuY/TeRmqdxKeDiaFT9F2eUCdez -FbId94n/8E69dSSCtmbEwwQMsxgMxALZEFustA== +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCmN9ktdBsuxTOPFUsU +qAjMnQSyBz/BpYDGMagy9e7PbtniVGTHHOvGgoq5VvPdtiVTerwefNAQaL3nLLvg +24hOEWBlQuBgK0gW48NPZJAbzYvNdF2jOzIzsDu8edEz4TcI8oKvw2WS5HQGl213 +06f2tMN1Ng0O07WoW8cxOYISsKVT9EyQJX4M/Oq5/nzHkXvS97ayFT0OvVdIRzPU +A6VsSyr/X1LgVmZEGfWcdv+cxJGBiXRsiWdW+Y+n6qvRBC2WpTEhCXomtbbDtuSH +e+8EXk9eKSc5QYFNCDWEMk25JuEQpXIMfdiHbMmK+9BgdRTUh8Pm94yD3hkMO6z5 +N5e3AgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy02hwSsFAAkMB0GA1UdDgQWBBRyt96xEM6o5VkG9JV5 +vLVxnBELSjAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG +9w0BAQsFAAOCAQEAmxFmsjenSgrrI1sE7DJahX1CaNVGodx4CwVc2etEq5PBWC6r +DpfCcYDW+Hg64Ac+NiPaLxFaG/8aM7JSePbAa71AQN+2hJpsV3/ANvSUaJfbHSFx +xfTRr8m5l33IV7ynjvZCPXWK4Gc5o7/shPKObHjwb03DLJjW0rvD5SYIjfCLjlOk +na2ufQnrmEP0XO77EvP4G/sHBjUaXrthsYTISO3lBTnGoKWNj8YwTFtXILC3O1to +sKWKYe5A6FB6xathUVBfS+Drp0PIYdAU9N3adymv4tZf52ofMsbJNkDqY3JaWmcO +dYHuYTeYg6ZiVhzZeasd3V+wc/CKAD8U5UfD5A== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-6-key.pem b/deploy/dockerephemeral/docker/redis-node-6-key.pem index a214d2d810f..0bc3f366189 100644 --- a/deploy/dockerephemeral/docker/redis-node-6-key.pem +++ b/deploy/dockerephemeral/docker/redis-node-6-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC6YjcRhT/CWKdX -QXIZ+qee0UPul0gw+QHPcc8MHsO06bDN8y40//sa2/5fhp80SkFCZSlT99tCBBO+ -M8NxALADOGvl01aOL9LY9O2nXkya6/6DkIsV+GssBtC1OIBOiSrHfHy+C7ICbV1A -x5NkHWEXpkKc8kAZo3ETDqXzCoYq+01qgb12RBBwQxz0yxHDOZcfXFaffIM3+Wv7 -XnHpRT22tWJuw7h5TTxx9u1dhZKBWERa2kVUhA6/Ihk/zCpASWbRwOf355jTNAuO -+pQTmXFDwr+/JcenBiwCQzxaTFkUDPwy0UvhrKvK6WsXuySNO6QNFZXPyuus2IOl -nXd08+vBAgMBAAECggEAEori36NaDIO1YkDokR0Wv/4hvALg875SJ8kyyAnnfoAh -Ttv6pNsyqCFq1SYXgKRCidB2pBvsfEzbifisYPmoiSl70omL+ulXGK6FVjlTdbY0 -w/IFZFIql162NNFCMo4C64W/A0k2lHc858zzJOqnVir8RZD0P5i7DyJN8DgD0RKz -uDpulugDgHgWuyfhkve3rmN8RAJkFiSlyJJCKPA1YoSdKrUmqwjZ9WJfV8UrUPPC -PoEksVEiLB0NrE2X0CtcSRuSZV0JDDciCLiCHkwDSWRgLzWnE7ECa8BNgdz+MyUH -WQjAoG5ZNP/9pPtVb2yyHqrC7ekc0wZyzahgiKb+gQKBgQDp8cMdHxf4kBwcJIXq -69OB1/0BwnXs0nXVaWQycy2pEFRC6CGfXkUEG9nT/nnHiQyD2ENcjgR9ATFe4Tx0 -CRfB6LQBmpsBK3fidYyBIUqDypMmrzFfc0Kj00o2v8TlO6bj2cFxYwWiu7VQ4E6i -ACmWfi5Aww+yCVPxzKgbAdkULQKBgQDL9JOjvkFbtFmbOn5yvoiIAOMnVlM6BvAd -vzUxg9Hp+xxYaBLPSEhqqc+kcCUdVzGOKeCZheB+D6OzZ9tAMqv46qBk6gsBdhk0 -uieaD5gNnVJm+l2ziTLNGdIF1StqaqXC/GU58BYqiajZABoePdS9pGK3BprDr1NW -8pcy8laOZQKBgGa3fuq/Zz/8zkrRAnemOcSt9+mY3zwvIAum7ZZ1Gdw8TjLeRzz5 -ICZwsBCzj/a7RuJwxwrRVEkqh+nXzTpJb8P1D2wQ3PQDiOzGnf1oh5YcEMYQcAYv -zleuAszNIH9h1KIATz4gsy3DaxXqlrvshFYOavKGctLB47isGjdZdV21AoGANh2d -8utvUhLHV82scWumtFdv7icUjCf9HBd42Lt+PhQX0ElE/GTUeiC2bI4o+uEA0BTC -eFmyWCB0Mg0TerQ3NyOiDUSgSPH5/CiMi28pzCr7C0HRDOsRZKQ+Orf1/hVwCA2K -GlZeu0itWW6Sf4WuZecxHhkNhXCGr2JMxgLQ/pUCgYAYw3Zvvs1C88geKyugdjtA -RIHPrkU5iPk7N+lr3Fb7HQft063f+ejuUIR6RJUUQsAf1OCsYK2AT9xd6JqAfpZA -AhDHPd8lMy1mepqG7MscICH31pFdLjfyBP9z/aktVgzDgQ5c/VbTxrW/+Zm6vQUC -JAeAbzN4IggEBDj6higxWg== +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCmN9ktdBsuxTOP +FUsUqAjMnQSyBz/BpYDGMagy9e7PbtniVGTHHOvGgoq5VvPdtiVTerwefNAQaL3n +LLvg24hOEWBlQuBgK0gW48NPZJAbzYvNdF2jOzIzsDu8edEz4TcI8oKvw2WS5HQG +l21306f2tMN1Ng0O07WoW8cxOYISsKVT9EyQJX4M/Oq5/nzHkXvS97ayFT0OvVdI +RzPUA6VsSyr/X1LgVmZEGfWcdv+cxJGBiXRsiWdW+Y+n6qvRBC2WpTEhCXomtbbD +tuSHe+8EXk9eKSc5QYFNCDWEMk25JuEQpXIMfdiHbMmK+9BgdRTUh8Pm94yD3hkM +O6z5N5e3AgMBAAECggEAQ088YB6zX0Y2McvyooPFRG6VVy5+UAGgWyICtdhHg7Kl +AvUf9k2s4K8+U/11NaQsC1kZUtNCQlLYDARedJkR4mNBAOCLEgaU48gJ8F2NyeR7 +p5Bm1tIC61GDbzh5UiPycGocJ+bdfBWNMpohlzObwdjDifSAZy+uUWYRDMr39G7x +9SH6aLL7ZHBg0Oc4dw6K4GQMrU7sdomQcSqNyi5sn6PN8FsuO1wMp8C8V+U6y5sb +36Y1rz90ZOFqmOBnG/IdPR8tFbdql1Yy31tzy/I4thK+1v4QN6JVLPvyw4H0RzFe +j347k5IsNehRdwltplhckeAUzWGGNiTx0zhQPAchuQKBgQDmyGB055GCtRoEIpqN +ANNa8PxTp2sCH+/J7KZma6gSJ9WY73xtGSVXX/Ubz4l8FHiGoA0CQCElARJ9zff/ +tAiNXqvcQeBPVC23CMJL3hxeHLNs0ipoD8qvdQpGit3DAZMjdjtt5jd48CulEmfP +/rVmeHKChZaPPR1EgrMnIytaCwKBgQC4YWygHnDjW9zekpsDRMKkvK5QMIey9ygB +LqXlXw6GANhVDGSr7zOHBtF1aBc6FA1FKlVRXz3Fag4pPZLd2HbEaKnzfCNPH5PL +UTX8fukftrzY03bvpYcr+/YabPO8H5hkeUqHyH9EyIgdj5hOhKEVj9kJkqENt3el +GvohkgdwhQKBgG0itPqTx6wYGIV8F7o2eby32Zt1wJTwpWTIFKi6oHB1hf0cw6qU +CaSYLEFKk6mpxJVlesFlskbdivETRgQWDzVLX9p5DKp3FGdKLRfToXaf+/mqKYOs +dB0lLAbQBK8DP6G1d8Uw6Wq3qOwXGCC0QvSCYSR4KAr0y7JqXG5Vo1qhAoGATLCh +GNxwgfDEpoL+HNbtys18B3iYCLVKm2tGr2fhR5V0ZbOY7/a3TPNmDdp0xsBuYJVi +FU1zCPi62SZ2PvX5OGp8Pf0lRpTQyWGG/fXfi0RbuigCsVz9IytSyt0EZ/wQS8Iz +YNThMr/h9cGzTP1Xbvt8/8FQYb8s8ayN24a8t20CgYBDyjVifJHw6iVl3vu/O+R9 ++AdSe5bEGGDuIKZRDJbEj2ScgD3Nwqdst7X5wC+rcUuyJNW22GihyLiC/+OCaJPl +9fyaRpWWjEUkzpvR+3GhzzykDnemw1z39AJrg3ewSaBdbw9Bvq0ebrGaDF+uCReY +V+yVEYFsBaK0JrbkIffXbA== -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/federation-v0.yaml b/deploy/dockerephemeral/federation-v0.yaml index 28e50750273..d34be166d50 100644 --- a/deploy/dockerephemeral/federation-v0.yaml +++ b/deploy/dockerephemeral/federation-v0.yaml @@ -2,8 +2,11 @@ networks: demo_wire: external: false + coredns: + external: false + services: - brig_schema: + brig_schema-v0: container_name: brig-schema-federation-v0 image: quay.io/wire/brig-schema:4.38.51 command: --host cassandra --keyspace brig_test_federation_v0 --replication-factor 1 @@ -15,7 +18,7 @@ services: condition: on-failure networks: - demo_wire - brig: + brig-v0: container_name: brig-federation-v0 image: quay.io/wire/brig:4.38.0-mandarin.14 volumes: @@ -27,7 +30,7 @@ services: healthcheck: &haskell_health_check test: "curl --fail localhost:8080/i/status" depends_on: - brig_schema: + brig_schema-v0: condition: service_completed_successfully aws_cli: condition: service_completed_successfully @@ -41,7 +44,7 @@ services: - RABBITMQ_USERNAME=${RABBITMQ_USERNAME} - RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD} - galley_schema: + galley_schema-v0: container_name: galley-schema-federation-v0 image: quay.io/wire/galley-schema:4.38.51 command: --host cassandra --keyspace galley_test_federation_v0 --replication-factor 1 @@ -54,7 +57,7 @@ services: networks: - demo_wire - galley: + galley-v0: container_name: galley-federation-v0 image: quay.io/wire/galley:4.38.0-mandarin.14 volumes: @@ -65,7 +68,7 @@ services: - '127.0.0.1:21085:8080' healthcheck: *haskell_health_check depends_on: - galley_schema: + galley_schema-v0: condition: service_completed_successfully aws_cli: condition: service_completed_successfully @@ -79,7 +82,7 @@ services: - RABBITMQ_USERNAME=${RABBITMQ_USERNAME} - RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD} - cargohold: + cargohold-v0: container_name: cargohold-federation-v0 image: quay.io/wire/cargohold:4.38.0-mandarin.14 volumes: @@ -98,7 +101,7 @@ services: - AWS_ACCESS_KEY_ID=dummykey - AWS_SECRET_ACCESS_KEY=dummysecret - gundeck_schema: + gundeck_schema-v0: container_name: gundeck-schema-federation-v0 image: quay.io/wire/gundeck-schema:4.38.51 command: --host cassandra --keyspace gundeck_test_federation_v0 --replication-factor 1 @@ -111,7 +114,7 @@ services: networks: - demo_wire - gundeck: + gundeck-v0: container_name: gundeck-federation-v0 image: quay.io/wire/gundeck:4.38.0-mandarin.14 volumes: @@ -122,11 +125,11 @@ services: - '127.0.0.1:21086:8080' healthcheck: *haskell_health_check depends_on: - gundeck_schema: + gundeck_schema-v0: condition: service_completed_successfully aws_cli: condition: service_completed_successfully - redis: + redis-v0: condition: service_started environment: @@ -134,7 +137,7 @@ services: - AWS_ACCESS_KEY_ID=dummykey - AWS_SECRET_ACCESS_KEY=dummysecret - spar_schema: + spar_schema-v0: container_name: spar-schema-federation-v0 image: quay.io/wire/spar-schema:4.38.51 command: --host cassandra --keyspace spar_test_federation_v0 --replication-factor 1 @@ -147,7 +150,7 @@ services: networks: - demo_wire - spar: + spar-v0: container_name: spar-federation-v0 image: quay.io/wire/spar:4.38.0-mandarin.14 volumes: @@ -158,10 +161,10 @@ services: - '127.0.0.1:21088:8080' healthcheck: *haskell_health_check depends_on: - spar_schema: + spar_schema-v0: condition: service_completed_successfully - cannon: + cannon-v0: container_name: cannon-federation-v0 image: quay.io/wire/cannon:4.38.0-mandarin.14 volumes: @@ -172,7 +175,7 @@ services: - '127.0.0.1:21083:8080' healthcheck: *haskell_health_check - federator: + federator-v0: container_name: federator-federation-v0 image: quay.io/wire/federator:4.38.0-mandarin.14 volumes: @@ -189,10 +192,10 @@ services: healthcheck: test: "true" depends_on: - coredns-federation: + coredns-federation-v0: condition: service_started - background_worker: + background_worker-v0: container_name: background-worker-federation-v0 image: quay.io/wire/background-worker:4.38.0-mandarin.14 volumes: @@ -201,6 +204,7 @@ services: - demo_wire ports: - '127.0.0.1:21089:8080' + healthcheck: *haskell_health_check depends_on: init_vhosts: condition: service_completed_successfully @@ -208,7 +212,7 @@ services: - RABBITMQ_USERNAME=${RABBITMQ_USERNAME} - RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD} - proxy: + proxy-v0: container_name: proxy-federation-v0 image: quay.io/wire/proxy:4.38.0-mandarin.14 volumes: @@ -219,7 +223,7 @@ services: - '127.0.0.1:21087:8080' healthcheck: *haskell_health_check - nginz: + nginz-v0: container_name: nginz-federation-v0 image: quay.io/wire/nginz:4.38.0-mandarin.14 volumes: @@ -230,26 +234,28 @@ services: - '127.0.0.1:21080:8080' - '127.0.0.1:21443:8443' depends_on: - brig: + brig-v0: + condition: service_healthy + galley-v0: condition: service_healthy - galley: + gundeck-v0: condition: service_healthy - gundeck: + cargohold-v0: condition: service_healthy - cargohold: + cannon-v0: condition: service_healthy - cannon: + spar-v0: condition: service_healthy - spar: + federator-v0: condition: service_healthy - federator: + proxy-v0: condition: service_healthy - proxy: + background_worker-v0: condition: service_healthy # We have to run a separate redis instance for each version of wire-server we # want. This is because gundeck just assumes the whole redis is for itself - redis: + redis-v0: container_name: redis-federation-v0 image: redis:6.0-alpine networks: @@ -257,7 +263,7 @@ services: # This coredns serves slightly different SRV records, so federator running in # a docker container can talk to federator running on the host. - coredns-federation: + coredns-federation-v0: image: docker.io/coredns/coredns:1.8.4 volumes: - ./federation-v0/coredns-config:/coredns-config diff --git a/deploy/dockerephemeral/federation-v0/coredns-config/db.example.com b/deploy/dockerephemeral/federation-v0/coredns-config/db.example.com index 448d8b5f594..407f4770916 100644 --- a/deploy/dockerephemeral/federation-v0/coredns-config/db.example.com +++ b/deploy/dockerephemeral/federation-v0/coredns-config/db.example.com @@ -17,4 +17,5 @@ _wire-server-federator._tcp.b IN SRV 0 0 9443 host.docker.internal. _wire-server-federator._tcp.d1 IN SRV 0 0 10443 host.docker.internal. _wire-server-federator._tcp.d2 IN SRV 0 0 11443 host.docker.internal. _wire-server-federator._tcp.d3 IN SRV 0 0 12443 host.docker.internal. -_wire-server-federator._tcp.v0 IN SRV 0 0 21443 host.docker.internal. +_wire-server-federator._tcp.federation-v0 IN SRV 0 0 21443 host.docker.internal. +_wire-server-federator._tcp.federation-v1 IN SRV 0 0 22443 host.docker.internal. diff --git a/deploy/dockerephemeral/federation-v0/integration-ca.pem b/deploy/dockerephemeral/federation-v0/integration-ca.pem index 304fc892245..6a33fa9e2c3 100644 --- a/deploy/dockerephemeral/federation-v0/integration-ca.pem +++ b/deploy/dockerephemeral/federation-v0/integration-ca.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- -MIIDEzCCAfugAwIBAgIUQ35aUV70pJjvDTbfgFUj5YmchHQwDQYJKoZIhvcNAQEL -BQAwGTEXMBUGA1UEAwwOY2EuZXhhbXBsZS5jb20wHhcNMjQwNjE3MTMxNTMxWhcN -MzQwNjE1MTMxNTMxWjAZMRcwFQYDVQQDDA5jYS5leGFtcGxlLmNvbTCCASIwDQYJ -KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJQlUOLNmd7Ll7iskcSnsv9xcx/+TnMw -qtqkK17w54/Kto+NJJAkD1L+X5EkSPZ7FDKqt2bGfoETWGnlpH/zsUTUpchlf6Jf -w6TJOejQer5FQNLCtQSnOIchlAFKzFxhGSvcOrRWiBAPjTVIkv9eiCNXcJ5PE9Sk -8+bmn2ztz7LVHcv46PmT/+ihRxKJ01T5CsXWPUHOZQRfGvKZmyGf+iTBuhcxMPYC -nXb7/M3rYCQXL8FQZiaqbIVMqNRpMBVkAqU3l2JnSrlNIjIh6Nqowjog8QYGuIz6 -fxwWkw6EU5ZBwHIr2rOakCnQoKeXVqBJdWZNRMX1Vtqeh7O9zDoW4/0CAwEAAaNT -MFEwHQYDVR0OBBYEFHNgZ4nZQoNKnb0AnDkefTXxxYDqMB8GA1UdIwQYMBaAFHNg -Z4nZQoNKnb0AnDkefTXxxYDqMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL -BQADggEBAIuLuyF7m1SP6PBu29jXnfGtaGi7j0jlqfcAysn7VmAU3StgWvSatlAl -AO6MIasjSQ+ygAbfIQW6W2Wc/U+NLQq5fRVi1cnmlxH5OULOFeQZCVyux8Maq0fT -jj4mmsz62b/iiA4tyS5r+foY4v1u2siSViBJSbfYbMp/VggIimt26RNV2u/ZV6Kf -UrOxazMx1yyuqARiqoA3VOMV8Byv8SEIiteWUSYni6u7xOT4gucPORhbM1HOSQ/S -CVq95x4FeKQnbEMykHI+bpBdkoadMVtrjCbskU49mOrvl/pli9V44R8KK6C1Nv3E -VLLcoOctdw90aT3sIjaXBcZtDTE6p6g= +MIIDEzCCAfugAwIBAgIUEfjIXW9tD1WgwNHJ+kC3r6Cmv5swDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAwwOY2EuZXhhbXBsZS5jb20wHhcNMjQwOTAzMTIwMzM3WhcN +MzQwOTAxMTIwMzM3WjAZMRcwFQYDVQQDDA5jYS5leGFtcGxlLmNvbTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJkd+ihX0Tp6RG4Ue9kQFvTHFy2DL8rC +8u956hy17/9sTyIcMkTOtZMew+ua+ZrNzX1x1WP4BT5RZQA/wy6ioEpP5BVFxe9s +dmVqYawD9C5+XKTekEFF3gI0MIGZ4Vum2hdUOHTwDatAgdiqsBwuCxHM5terItzZ +SsrkYFlAhISmM9CsUFOR+1rqpMNWRQD5zzZ0Sk5HeyM1/9sHvkrq24rqKqkg0/uR +OTHMydrmuSArG5tKqkdb2zoJwOs8somraRzB1JOm5/i/3pCT5iBr5gf8eLGtb2Gu +XgmnTI129R2a+LbifqTC31jIMg/yNfFuU4MdUG9wE512ghhXZI+Hoc8CAwEAAaNT +MFEwHQYDVR0OBBYEFFqlhDsVqlH8UUKGOtCDE9xmYL3hMB8GA1UdIwQYMBaAFFql +hDsVqlH8UUKGOtCDE9xmYL3hMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBADQyh0k3xhcrNOqhvzAK1/A3TY5Hq1FE/a17Yiq8DIiLCJG/nAN60GBx +m7zuL8xoRJ4ylwIswa4z4rHj9p6M6tIbi2tTfJsbyB+FjyFRWoBmTngqNCiw7QUR +/ofSliuEu/YIjphR8LmTBvy4fVccTwXDaBPEGf2iN+DFmryLHxVpsVh3AA0uUSy0 +e2bZJLRwv1z0saC5KGHpWb6RJbAP2nRw5omcorMtP1KW8XyVESiJm7hDZAx6VLgD +k4GcEOUEq9CJs9UVAkIIDS87CfppHZEGPDK3Ufro5AhIwA3hpSJkTgzkf0TcQKIr +4E/zJSeTld4rMC26ghWodIwGRyofTP0= -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/federation-v0/integration-leaf-key.pem b/deploy/dockerephemeral/federation-v0/integration-leaf-key.pem index 1e7a83068de..e4ee0a09ab1 100644 --- a/deploy/dockerephemeral/federation-v0/integration-leaf-key.pem +++ b/deploy/dockerephemeral/federation-v0/integration-leaf-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCZjOHeUnlauuxD -WgrRnh3hj5Fs+uh9vyddMX8rSWJIbWFw4QuYzYKY8CQa3MBb6qK1uUwoJ0W1w47I -RgA5VLvGxI+T1wX8E5vljVgfT3CAXHKRB88NrT8A1urQnWpzlq5sNerL6dqgBrjG -QBmFF7NxrvjGgerC2D8+srWfpQ6Jbl9by8c3JDu+T79PM+pW9ycUgdF1AJQBTz9K -zNQ7ZTlBQvJG8WhTMKioJgQsE60oEXD0C8M5yKBBb7DrqkeZInXqCw2y7DZLWzog -D+jgoAD5/9sk3d/gGNqDibzjjwMiJnH/IqBTkZsQ9OdZZPfx5v/p062hQBlM656P -2jMpJ1xxAgMBAAECggEAS3NBjWgDP4T4EUROaqACWNKeB+nmkdt68T0gGtoNVD+D -EN9UPnpFQPdHFngAgWnzF858UIKzq1Pzdg+HjqRHPK1bS67tvua3xP1GHuR/CGPk -28T1hefqPHRen7GqHDAfdwarYBWCGv4Sjz/yCkcSIrtyfMBb5fAya5GO02pckUSK -19sl7XhkPtHJVirRkjQL29R2TCpkNNpQMjkuYLk7mox+6pNTbxgbk0cnT3eGj1pV -mlPqpwzC5GevRziE/VE/WXFLChY+8KB4fDLRqWnyvabDvQ4coaXgzwbdScJyM5hX -+Dxdfni/P2m7xAZXUyfBsr0VUzqUkJfK3WWvvAGTDQKBgQDNi3RUEjVnU/MN4aDz -iZB2VYGfo/K69xTPNEbLQWs1F4ZMpHVtUVXzTfx/xG9ug989ijEm6ncL9OsnhThn -UldSz2ojSJUxLmhgCHZGYHT72v/9rEqfT9JisWpIj44KXufUHCcl3Cozj1ae3EUp -NVhN1HphB2LsCIJvLYfLIGdBNwKBgQC/PhHQMm/MQe4pOHAbdzDrRZWdG2KSRVxp -9mmJ/aT8LOp7BDjq+Dkct6a56JGqlOTeJirMTTmCKiOiTInuB9S+K7kWJJiYg9g4 -UCiuMU+40Px/1Z4/uxRj3DSdGLXG7S6kPeADx9f9BUNpAytGqOnSnfbDiDVvQVbp -0N0+nIXDlwKBgQC2uZOXrXxGOE4pd/ySpCeF2yvZ1HDTnxWjwlBxHt4Em74rYkR2 -A0mKezjOCL4bHCaYWcKqWuOsAHYQcxEaYQv6NSOg7ESdLSlivgMPO26j+yN5yvGn -wNlCHYBjsyLNu2MSoFh5AsmNfo69uQnOwXqX7h1BJsTdGg+CcJJ4lHzWbwKBgQCD -/CRzGbwKrh3eGPNWIUaDuTxudy3qYTBMeSGReJpa5+zUBa/6imFwLldEyvttTOE/ -Z/v1j/52lPqO0mAHBSSQMsDERXGDIMsi4j+RKLsqhCEfYKCcv1JtMNam7RzXM24T -MBjgwxWPrAg/+03ssDrffuGFRQYLyH5hVCK9SW0P9QKBgQDJ1ZSto+RWxv/uOKNr -7FYeQoKpMb2IvNvnGlnYHC8KS9qRq6wUE+FtuKcdLBQP4M9Cgq71VD/dsawrhEw7 -1rAYk3OqmHxBOU5Dcb152NxYHEf53pfEfWc0x4AEVe+Jzynj2EYixRKNWwODNTEx -LKJOYd0CuWywxg6d9G7A7XbgWQ== +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDPYxemHc21P8y2 +DIDNsuUzVpnc1bj81QIQ5a2aFSVtQFr3Dhpo0XXyu0KHKR64ZelmW33Yoe2J0zwl +R66fg07whBcZspsonXmsUB5uFFF/Dcv9shBNcg67jjynXPDvJK2sC37GCar5ar/v +11v/+Iq6LNerC/OqMJlMYyFDif1BvESbnw+9hsihUT9qK5s6md5krEV1/ro0Noh7 +kBgflylEKjapfCrVMnAMgaMV55jrk5BTpoR6KCDuDmXPk3Ed304Op/krGazfGB2j +1rz3IPfKOOtIgxn8n5PDYmShVUw1R3Rps1LeEyhsH1EGuAvVx1mKhB3QGMqFtIfo +wOpkgZPdAgMBAAECggEAVMurFUB1ZkkmXj9hgPnHLoUX10xJ3ZMIy7jlkS1ZRsD8 +EK0jDj2q0OtRSet9xJ7i3nfFTojzE5obqxCSrWUmp0ATI+4789DjuZluv8quAdm1 +0U73zHq43GZNlY7yco2YN1Lh7H5yepXz0dDILLLGolYIfscdw7YoUCvuI2vt8eyY +FaMkOi8Bs32Abn+53MhaNkEOVemmwP/u2rD81IY7pXQtF+1XfSxYBipHCB7phC1g +dr6ITU9CoF3SXvjYL6uP/7W1Du/CuaDSijVZJkBfQSgbkQbBr6pAovumtpNZgZvO +bixqS4oTZqsd+Rgl3YBOpx+JbuUFL0fG+uQBv1yZbwKBgQD8n7s1w/ouUGwW+qGi +4EFTYVDFD2Rgvg3oFPHt72zreaNXSy8YWrK9Mas/Fzu5knSE3f3O0S1WvqWdt/z6 +uPDLTpg9fIWX6v+hPX09F5ekeLlUDBazBT1PQUD0qd8PiyFNB1F+ffSPTxmKScjc +hTqQCtnun6rlICalWO9VvGt6QwKBgQDSKJkjxIGLtXBaXfMQiF9dQDrMk2it5grl +w0OnpPhYvpdp+Cfi01kMUrnfHwF0v92BqeqoKZo4DwJXkrmwf5kNnftxmRqbk9gE +dJq/E/6SELyT/chtzXfxC/wTmyyxhfZUJvJUxlaZ6KP/86t8A22DJcFO2Z85iUGH +8zy2UJUnXwKBgBvSs9m+FeXX8a+uNvMrY8Z9J1os0c9d30Y6WFLuVb6xjO3mV++E +vb7co5G1S1yq5q5jjLqkiyvMn4z5YKF0kQCzTU0oU8ZhmXn2vb5mxMrWiQLauf1J +jHEYLMFFnE2n8yj6r10RHkhSW+vBKKAxBDwtFceUSkwl+FupqeJ1eBjlAoGBAMou ++LWqdZ89HSwzOobrTCPgiTELmCfFKzLE2q/MTIjEQ9NVRLo57m+mnt+DatkxRR9b +oz/JVm8cMXqi1DZza4HoPWGalDic0bPnooC18bIAnAwcmdjZVcz3ZLpQDX10jfmD +xpu8fNBxOmYhvRcADTmg9wqu3zpxTDRI1F3pxLUtAoGAGfsX4bve5cLm49Oa1p0H +kEErLMuAMIKQNVsbzVELepLYr+uwEXBCXyyoIf79ABDvUHbzxMEwgANuet/4PQzS +yB1qzFk6GDvqZ5dfPUgMUWH9wvD1qEGp6yxkyESGt8CNwnu8GI50NAeSh2/JeUIa +r/u+m2vnJjOXpJdOJ+7f6yM= -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/federation-v0/integration-leaf.pem b/deploy/dockerephemeral/federation-v0/integration-leaf.pem index 635d332de70..abd724df6b1 100644 --- a/deploy/dockerephemeral/federation-v0/integration-leaf.pem +++ b/deploy/dockerephemeral/federation-v0/integration-leaf.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- MIIDQTCCAimgAwIBAgIBADANBgkqhkiG9w0BAQsFADAZMRcwFQYDVQQDDA5jYS5l -eGFtcGxlLmNvbTAeFw0yNDA2MTcxMzE1MzFaFw0yNDA3MTcxMzE1MzFaMAAwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCZjOHeUnlauuxDWgrRnh3hj5Fs -+uh9vyddMX8rSWJIbWFw4QuYzYKY8CQa3MBb6qK1uUwoJ0W1w47IRgA5VLvGxI+T -1wX8E5vljVgfT3CAXHKRB88NrT8A1urQnWpzlq5sNerL6dqgBrjGQBmFF7NxrvjG -gerC2D8+srWfpQ6Jbl9by8c3JDu+T79PM+pW9ycUgdF1AJQBTz9KzNQ7ZTlBQvJG -8WhTMKioJgQsE60oEXD0C8M5yKBBb7DrqkeZInXqCw2y7DZLWzogD+jgoAD5/9sk -3d/gGNqDibzjjwMiJnH/IqBTkZsQ9OdZZPfx5v/p062hQBlM656P2jMpJ1xxAgMB +eGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzhaFw0zNDA5MDExMjAzMzhaMAAwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPYxemHc21P8y2DIDNsuUzVpnc +1bj81QIQ5a2aFSVtQFr3Dhpo0XXyu0KHKR64ZelmW33Yoe2J0zwlR66fg07whBcZ +spsonXmsUB5uFFF/Dcv9shBNcg67jjynXPDvJK2sC37GCar5ar/v11v/+Iq6LNer +C/OqMJlMYyFDif1BvESbnw+9hsihUT9qK5s6md5krEV1/ro0Noh7kBgflylEKjap +fCrVMnAMgaMV55jrk5BTpoR6KCDuDmXPk3Ed304Op/krGazfGB2j1rz3IPfKOOtI +gxn8n5PDYmShVUw1R3Rps1LeEyhsH1EGuAvVx1mKhB3QGMqFtIfowOpkgZPdAgMB AAGjgawwgakwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEgGA1UdEQEB /wQ+MDyCGSouaW50ZWdyYXRpb24uZXhhbXBsZS5jb22CFGhvc3QuZG9ja2VyLmlu -dGVybmFsgglsb2NhbGhvc3QwHQYDVR0OBBYEFPowAfmLPCmdCMdSxQjsR6UQSoyH -MB8GA1UdIwQYMBaAFHNgZ4nZQoNKnb0AnDkefTXxxYDqMA0GCSqGSIb3DQEBCwUA -A4IBAQCMJwbLzUsrkQkgdGKVi/Mb5XAAV0sfkwZch1Fx0vhJI072cZSow5A2ZUHa -LScFNTPmilPKEr6MS4xIKtRQaMHInbfxSsyNViKhpzkSOKoAiJjIJ2xPKFPnbTDI -uV74nxxyf9q/p3SLQfJFk7fxbvNeLqg5bYSrMeklHj4bpMJ9fybS8/mZVc8AkTFK -fsXSu9CW1B3GF+jP3E2GrFF3Zh9MgvWjMlSYg4ljPf5FoMCUq6GmQ17hQeJFvb5h -Jqk6TcgUrp082bcVlPW17XzFwVe3n6uzvWMtwI62EztVUj98+YkBiFL3i4+OQwAU -/noc22fq20OyJtCPJY4FIK7xUcgD +dGVybmFsgglsb2NhbGhvc3QwHQYDVR0OBBYEFBkpAu3ILiU4gtEYffAU7zxGHPC6 +MB8GA1UdIwQYMBaAFFqlhDsVqlH8UUKGOtCDE9xmYL3hMA0GCSqGSIb3DQEBCwUA +A4IBAQBB1VsthdoVT9ExXkfKixotbXm6+eBgYenK1R5Qx/UX3JrlI1nF/8rKMg5e +7QfMCydSJwVEQdvnXD3ddVhUTYRActQvnJwWTyXfeiezrfDCTLu4SNpLOP7ojFlq +9ZX/E9GC0axTIUmEIy8YIC3JJ2PAlvw9qMzrsivyAgbof3NX+9XXKfwZHBwSLsO1 +Gxr9zkL+U/qww7TvyJD1LqBR0UEd9pZriorpVVFAa/JlFQX5ip1Smcd6m97nq20N +qpUIalra+K6qHxjHVwA2UxVgbO9bLFIBmp9pNvSm+5umAKkmqFnHRNAHfCy/IFGl +3fw8u9mXJ8LzUR4tiS0cVb6bwQzd -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/federation-v0/nginz/conf/integration.conf b/deploy/dockerephemeral/federation-v0/nginz/conf/integration.conf index 12c49ccfe88..74dd1a09113 100644 --- a/deploy/dockerephemeral/federation-v0/nginz/conf/integration.conf +++ b/deploy/dockerephemeral/federation-v0/nginz/conf/integration.conf @@ -7,7 +7,7 @@ listen 8081; # port. # This port is only used for trying out nginx http2 forwarding without TLS locally and should not # be ported to any production nginz config. -listen 8090 http2; +listen 8090; ######## TLS/SSL block start ############## # diff --git a/deploy/dockerephemeral/federation-v0/nginz/upstreams b/deploy/dockerephemeral/federation-v0/nginz/upstreams index a3e6afada32..adfdafa6eec 100644 --- a/deploy/dockerephemeral/federation-v0/nginz/upstreams +++ b/deploy/dockerephemeral/federation-v0/nginz/upstreams @@ -1,38 +1,38 @@ upstream cargohold { least_conn; keepalive 32; - server cargohold:8080 max_fails=3 weight=1; + server cargohold-v0:8080 max_fails=3 weight=1; } upstream gundeck { least_conn; keepalive 32; - server gundeck:8080 max_fails=3 weight=1; + server gundeck-v0:8080 max_fails=3 weight=1; } upstream cannon { least_conn; keepalive 32; - server cannon:8080 max_fails=3 weight=1; + server cannon-v0:8080 max_fails=3 weight=1; } upstream galley { least_conn; keepalive 32; - server galley:8080 max_fails=3 weight=1; + server galley-v0:8080 max_fails=3 weight=1; } upstream proxy { least_conn; keepalive 32; - server proxy:8080 max_fails=3 weight=1; + server proxy-v0:8080 max_fails=3 weight=1; } upstream brig { least_conn; keepalive 32; - server brig:8080 max_fails=3 weight=1; + server brig-v0:8080 max_fails=3 weight=1; } upstream spar { least_conn; keepalive 32; - server spar:8080 max_fails=3 weight=1; + server spar-v0:8080 max_fails=3 weight=1; } upstream federator_external { - server federator:8081 max_fails=3 weight=1; + server federator-v0:8081 max_fails=3 weight=1; } diff --git a/deploy/dockerephemeral/federation-v1.yaml b/deploy/dockerephemeral/federation-v1.yaml new file mode 100644 index 00000000000..fc151a37dd0 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1.yaml @@ -0,0 +1,274 @@ +networks: + demo_wire: + external: false + + coredns: + external: false + +services: + brig_schema-v1: + container_name: brig-schema-federation-v1 + image: quay.io/wire/brig-schema:4.42.0-pre.27 + command: --host cassandra --keyspace brig_test_federation_v1 --replication-factor 1 + depends_on: + cassandra: + condition: service_healthy + deploy: + restart_policy: + condition: on-failure + networks: + - demo_wire + + brig-v1: + container_name: brig-federation-v1 + image: quay.io/wire/brig:4.42.0-pre.27 + volumes: + - ./federation-v1:/etc/wire/brig/conf + networks: + - demo_wire + ports: + - '127.0.0.1:22082:8080' + healthcheck: &haskell_health_check + test: "curl --fail localhost:8080/i/status" + depends_on: + brig_schema-v1: + condition: service_completed_successfully + aws_cli: + condition: service_completed_successfully + init_vhosts: + condition: service_completed_successfully + environment: + - AWS_REGION=eu-west-1 + - AWS_ACCESS_KEY_ID=dummykey + - AWS_SECRET_ACCESS_KEY=dummysecret + - RABBITMQ_USERNAME=${RABBITMQ_USERNAME} + - RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD} + + galley_schema-v1: + container_name: galley-schema-federation-v1 + image: quay.io/wire/galley-schema:4.42.0-pre.27 + command: --host cassandra --keyspace galley_test_federation_v1 --replication-factor 1 + depends_on: + cassandra: + condition: service_healthy + deploy: + restart_policy: + condition: on-failure + networks: + - demo_wire + + galley-v1: + container_name: galley-federation-v1 + image: quay.io/wire/galley:4.42.0-pre.27 + volumes: + - ./federation-v1:/etc/wire/galley/conf + networks: + - demo_wire + ports: + - '127.0.0.1:22085:8080' + healthcheck: *haskell_health_check + depends_on: + galley_schema-v1: + condition: service_completed_successfully + aws_cli: + condition: service_completed_successfully + init_vhosts: + condition: service_completed_successfully + environment: + - AWS_REGION=eu-west-1 + - AWS_ACCESS_KEY_ID=dummykey + - AWS_SECRET_ACCESS_KEY=dummysecret + - RABBITMQ_USERNAME=${RABBITMQ_USERNAME} + - RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD} + + cargohold-v1: + container_name: cargohold-federation-v1 + image: quay.io/wire/cargohold:4.42.0-pre.27 + volumes: + - ./federation-v1:/etc/wire/cargohold/conf + networks: + - demo_wire + ports: + - '127.0.0.1:22084:8080' + healthcheck: *haskell_health_check + depends_on: + aws_cli: + condition: service_completed_successfully + environment: + - AWS_REGION=eu-west-1 + - AWS_ACCESS_KEY_ID=dummykey + - AWS_SECRET_ACCESS_KEY=dummysecret + + gundeck_schema-v1: + container_name: gundeck-schema-federation-v1 + image: quay.io/wire/gundeck-schema:4.42.0-pre.27 + command: --host cassandra --keyspace gundeck_test_federation_v1 --replication-factor 1 + depends_on: + cassandra: + condition: service_healthy + deploy: + restart_policy: + condition: on-failure + networks: + - demo_wire + + gundeck-v1: + container_name: gundeck-federation-v1 + image: quay.io/wire/gundeck:4.42.0-pre.27 + volumes: + - ./federation-v1:/etc/wire/gundeck/conf + networks: + - demo_wire + ports: + - '127.0.0.1:22086:8080' + healthcheck: *haskell_health_check + depends_on: + gundeck_schema-v1: + condition: service_completed_successfully + aws_cli: + condition: service_completed_successfully + redis-v1: + condition: service_started + environment: + - AWS_REGION=eu-west-1 + - AWS_ACCESS_KEY_ID=dummykey + - AWS_SECRET_ACCESS_KEY=dummysecret + + spar_schema-v1: + container_name: spar-schema-federation-v1 + image: quay.io/wire/spar-schema:4.42.0-pre.27 + command: --host cassandra --keyspace spar_test_federation_v1 --replication-factor 1 + depends_on: + cassandra: + condition: service_healthy + deploy: + restart_policy: + condition: on-failure + networks: + - demo_wire + + spar-v1: + container_name: spar-federation-v1 + image: quay.io/wire/spar:4.42.0-pre.27 + volumes: + - ./federation-v1:/etc/wire/spar/conf + networks: + - demo_wire + ports: + - '127.0.0.1:22088:8080' + healthcheck: *haskell_health_check + depends_on: + spar_schema-v1: + condition: service_completed_successfully + + cannon-v1: + container_name: cannon-federation-v1 + image: quay.io/wire/cannon:4.42.0-pre.27 + volumes: + - ./federation-v1:/etc/wire/cannon/conf + networks: + - demo_wire + ports: + - '127.0.0.1:22083:8080' + healthcheck: *haskell_health_check + + federator-v1: + container_name: federator-federation-v1 + image: quay.io/wire/federator:4.42.0-pre.27 + volumes: + - ./federation-v1:/etc/wire/federator/conf + networks: + - demo_wire + - coredns + extra_hosts: + - "host.docker.internal.:host-gateway" + ports: + - '127.0.0.1:22097:8080' + - '127.0.0.1:22098:8081' + # healthcheck: *haskell_health_check + healthcheck: + test: "true" + depends_on: + coredns-federation-v1: + condition: service_started + + background_worker-v1: + container_name: background-worker-federation-v1 + image: quay.io/wire/background-worker:4.42.0-pre.27 + volumes: + - ./federation-v1:/etc/wire/background-worker/conf + networks: + - demo_wire + ports: + - '127.0.0.1:22089:8080' + healthcheck: *haskell_health_check + depends_on: + init_vhosts: + condition: service_completed_successfully + environment: + - RABBITMQ_USERNAME=${RABBITMQ_USERNAME} + - RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD} + + proxy-v1: + container_name: proxy-federation-v1 + image: quay.io/wire/proxy:4.42.0-pre.27 + volumes: + - ./federation-v1:/etc/wire/proxy/conf + networks: + - demo_wire + ports: + - '127.0.0.1:22087:8080' + healthcheck: *haskell_health_check + + nginz-v1: + container_name: nginz-federation-v1 + image: quay.io/wire/nginz:4.42.0-pre.27 + volumes: + - ./federation-v1:/etc/wire/ + networks: + - demo_wire + ports: + - '127.0.0.1:22080:8080' + - '127.0.0.1:22443:8443' + depends_on: + brig-v1: + condition: service_healthy + galley-v1: + condition: service_healthy + gundeck-v1: + condition: service_healthy + cargohold-v1: + condition: service_healthy + cannon-v1: + condition: service_healthy + spar-v1: + condition: service_healthy + federator-v1: + condition: service_healthy + proxy-v1: + condition: service_healthy + background_worker-v1: + condition: service_healthy + + # We have to run a separate redis instance for each version of wire-server we + # want. This is because gundeck just assumes the whole redis is for itself + redis-v1: + container_name: redis-federation-v1 + image: redis:6.0-alpine + networks: + - demo_wire + + + # This coredns serves slightly different SRV records, so federator running in + # a docker container can talk to federator running on the host. + coredns-federation-v1: + image: docker.io/coredns/coredns:1.8.4 + volumes: + - ./federation-v1/coredns-config:/coredns-config + entrypoint: + - /coredns + - -conf + - /coredns-config/Corefile + networks: + coredns: + ipv4_address: 172.20.1.4 diff --git a/deploy/dockerephemeral/federation-v1/background-worker.yaml b/deploy/dockerephemeral/federation-v1/background-worker.yaml new file mode 100644 index 00000000000..d70699f7f4e --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/background-worker.yaml @@ -0,0 +1,28 @@ +logLevel: Debug + +backgroundWorker: + host: 0.0.0.0 + port: 8080 + +federatorInternal: + host: federator-federation-v1 + port: 8080 + +galley: + host: galley-federation-v1 + port: 8080 + +brig: + host: brig-federation-v1 + port: 8080 + +rabbitmq: + host: rabbitmq + port: 5672 + vHost: federation-v1 + adminPort: 15672 + +backendNotificationPusher: + pushBackoffMinWait: 1000 + pushBackoffMaxWait: 1000000 + remotesRefreshInterval: 5000000 diff --git a/deploy/dockerephemeral/federation-v1/brig.yaml b/deploy/dockerephemeral/federation-v1/brig.yaml new file mode 100644 index 00000000000..62a6c8c2a8f --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/brig.yaml @@ -0,0 +1,218 @@ +brig: + host: 0.0.0.0 + port: 8080 + +cassandra: + endpoint: + host: demo_wire_cassandra + port: 9042 + keyspace: brig_test_federation_v1 + # filterNodesByDatacentre: datacenter1 + +elasticsearch: + url: http://nginz-federation-v1:9201 + index: directory_test + +rabbitmq: + host: rabbitmq + port: 5672 + vHost: federation-v1 + +cargohold: + host: cargohold-federation-v1 + port: 8080 + +galley: + host: galley-federation-v1 + port: 8080 + +gundeck: + host: gundeck-federation-v1 + port: 8080 + +federatorInternal: + host: federator-federation-v1 + port: 8080 + +multiSFT: false + +# You can set up local SQS/Dynamo running e.g. `../../deploy/dockerephemeral/run.sh` +aws: + userJournalQueue: integration-user-events-federation-v1.fifo + # ^ Comment this out if you don't want to journal user events + prekeyTable: integration-brig-prekeys-federation-v1 + sqsEndpoint: http://fake_sqs:4568 # https://sqs.eu-west-1.amazonaws.com + # dynamoDBEndpoint: http://localhost:4567 # https://dynamodb.eu-west-1.amazonaws.com + +# Uncomment to use the randomPrekey allocation strategy instead of dynamoDB +randomPrekeys: true + +# Uncomment this if you want STOMP. +# +# stomp: +# stompHost: localhost +# stompPort: 61613 +# stompTls: false + +# TODO: possibly move 'userJournalQueue' to the top level as well +internalEvents: + queueType: sqs + queueName: integration-brig-events-internal-federation-v1 + # queueType: stomp + # queueName: /queue/integration-brig-events-internal + +emailSMS: + # You can either use SES directly (in which case, ensure a feedback queue is configured) + # or you can use SMTP directly (blacklisting of email/phone must be otherwise handled by + # the operator). + email: + sesQueue: integration-brig-events-federation-v1 + sesEndpoint: http://ses:4569 # https://email.eu-west-1.amazonaws.com + # If you prefer to use SMTP directly, uncomment the following lines + # and set the correct credentials. + # NOTE: In case a user tries to supply config values for both SES and SMTP, + # SES takes precedence and gets used instead + # smtpEndpoint: + # host: localhost + # port: 2500 + # smtpCredentials: + # username: + # password: test/resources/smtp-secret.txt + # smtpConnType: plain + # ^ NOTE: blacklisting of emails (processing of bounces and complaints) is only done + # automatically IF sesQueue/sesEndpoint are used. If SMTP is used directly, the + # operator must handle these notifications "manually" (there are internal endpoints) + # that may be used for this + + general: + templateDir: /usr/share/wire/templates + emailSender: backend-integration@wire.com + smsSender: "+123456789" # or MG123456789... (twilio alphanumeric sender id) + templateBranding: + brand: Wire + brandUrl: https://wire.com + brandLabelUrl: wire.com # This is the text in the label for the above URL + 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/ + user: + activationUrl: http://127.0.0.1:8080/activate?key=${key}&code=${code} + smsActivationUrl: http://127.0.0.1:8080/v/${code} + passwordResetUrl: http://127.0.0.1:8080/password-reset/${key}?code=${code} + invitationUrl: http://127.0.0.1:8080/register?invitation_code=${code} + deletionUrl: http://127.0.0.1:8080/users/delete?key=${key}&code=${code} + + provider: + homeUrl: https://provider.localhost/ + providerActivationUrl: http://127.0.0.1:8080/provider/activate?key=${key}&code=${code} + approvalUrl: http://127.0.0.1:8080/provider/approve?key=${key}&code=${code} + approvalTo: success@simulator.amazonses.com + providerPwResetUrl: http://127.0.0.1:8080/provider/password-reset?key=${key}&code=${code} + + team: + tInvitationUrl: http://127.0.0.1:8080/register?team=${team}&team_code=${code} + tActivationUrl: http://127.0.0.1:8080/register?team=${team}&team_code=${code} + tCreatorWelcomeUrl: http://127.0.0.1:8080/creator-welcome-website + tMemberWelcomeUrl: http://127.0.0.1:8080/member-welcome-website + +zauth: + privateKeys: /etc/wire/brig/conf/zauth-privkeys.txt + publicKeys: /etc/wire/brig/conf/zauth-pubkeys.txt + authSettings: + keyIndex: 1 + userTokenTimeout: 120 + sessionTokenTimeout: 20 + accessTokenTimeout: 30 + providerTokenTimeout: 60 + legalHoldUserTokenTimeout: 120 + legalHoldAccessTokenTimeout: 30 + +turn: + serversSource: dns # files | dns + baseDomain: example.com + discoveryIntervalSeconds: 100 + + # This should be the same secret as used by the TURN servers + secret: /etc/wire/brig/conf/turn-secret.txt + configTTL: 3600 + tokenTTL: 21600 + +optSettings: + setActivationTimeout: 10 + setVerificationTimeout: 10 + setTeamInvitationTimeout: 10 + setExpiredUserCleanupTimeout: 1 + setTwilio: /etc/wire/brig/conf/twilio-credentials.yaml + setNexmo: /etc/wire/brig/conf/nexmo-credentials.yaml + # setStomp: test/resources/stomp-credentials.yaml + setUserMaxConnections: 16 + setCookieInsecure: true + setUserCookieRenewAge: 2 + setUserCookieLimit: 5 + setUserCookieThrottle: + stdDev: 5 + retryAfter: 3 + setLimitFailedLogins: + timeout: 5 # seconds. if you reach the limit, how long do you have to wait to try again. + retryLimit: 5 # how many times can you have a failed login in that timeframe. + setSuspendInactiveUsers: # if this is omitted: never suspend inactive users. + suspendTimeout: 10 + setRichInfoLimit: 5000 # should be in sync with Spar + setDefaultUserLocale: en + setMaxTeamSize: 32 + setMaxConvSize: 16 + setEmailVisibility: visible_to_self + setPropertyMaxKeyLen: 1024 + setPropertyMaxValueLen: 4096 + setDeleteThrottleMillis: 0 + setSqsThrottleMillis: 1000 + setRestrictUserCreation: false + # setSearchSameTeamOnly: false + # ^ NOTE: this filters out search results for team users, + # i.e., if you are a team user the search endpoints will + # return only users part of the same team. For name search, + # this is slightly more inefficient as it requires 2 extra DB lookups + # setUserMaxPermClients: 7 + # ^ You can limit the max number of permanent clients that a user is allowed + # to register, per account. The default value is '7' if the option is unset. + + # Federation domain is used to qualify local IDs and handles, + # e.g. 0c4d8944-70fa-480e-a8b7-9d929862d18c@wire.com and somehandle@wire.com. + # It should also match the SRV DNS records under which other wire-server installations can find this backend: + # _wire-server-federator._tcp. + # Once set, DO NOT change it: if you do, existing users may have a broken experience and/or stop working + # Remember to keep it the same in Galley. + setFederationDomain: federation-v1.example.com + setFeatureFlags: # see #RefConfigOptions in `/docs/reference` + setFederationDomainConfigsUpdateFreq: 1 + setFederationStrategy: allowAll + setFederationDomainConfigs: + - domain: example.com + search_policy: full_search + set2FACodeGenerationDelaySecs: 5 + setDisabledAPIVersions: [] + setNonceTtlSecs: 5 + setDpopMaxSkewSecs: 1 + setDpopTokenExpirationTimeSecs: 300 # 5 minutes + setPublicKeyBundle: /etc/wire/brig/conf/jwt-ed25519-bundle.pem + setEnableMLS: true + # To only allow specific email address domains to register, uncomment and update the setting below + # setAllowlistEmailDomains: + # - wire.com + # To only allow specific phone number prefixes to register uncomment and update the settings below + # setAllowlistPhonePrefixes: + # - "+1555555" + # needs to be kept in sync with services/nginz/integration-test/resources/oauth/ed25519_public.jwk + setOAuthJwkKeyPair: /etc/wire/brig/conf/oauth-ed25519.jwk + setOAuthAuthCodeExpirationTimeSecs: 3 # 3 secs + setOAuthAccessTokenExpirationTimeSecs: 3 # 3 secs + setOAuthEnabled: true + setOAuthRefreshTokenExpirationTimeSecs: 14515200 # 24 weeks + setOAuthMaxActiveRefreshTokens: 10 + +logLevel: Warn +logNetStrings: false diff --git a/deploy/dockerephemeral/federation-v1/cannon.yaml b/deploy/dockerephemeral/federation-v1/cannon.yaml new file mode 100644 index 00000000000..c091ea67ed2 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/cannon.yaml @@ -0,0 +1,26 @@ +# Example yaml-formatted configuration for cannon used in integration tests + +# cannon can be started with a config file (e.g. ./dist/cannon -c cannon.yaml.example) + +cannon: + host: 0.0.0.0 + port: 8080 + + # Each cannon instance advertises its own location (ip or dns name) to gundeck. + # Either externalHost or externalHostFile must be set (externalHost takes precedence if both are defined) + # externalHostFile expects a file with a single line containing the IP or dns name of this instance of cannon + externalHost: cannon-federation-v1 + #externalHostFile: /etc/wire/cannon/cannon-host.txt + +gundeck: + host: gundeck-federation-v1 + port: 8080 + +drainOpts: + gracePeriodSeconds: 1 + millisecondsBetweenBatches: 500 + minBatchSize: 5 + +disabledAPIVersions: [] +logLevel: Warn +logNetStrings: false diff --git a/deploy/dockerephemeral/federation-v1/cargohold.yaml b/deploy/dockerephemeral/federation-v1/cargohold.yaml new file mode 100644 index 00000000000..4913082900e --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/cargohold.yaml @@ -0,0 +1,33 @@ +brig: + host: brig-federation-v1 + port: 8080 + +cargohold: + host: 0.0.0.0 + port: 8080 + +federator: + host: federator-federation-v1 + port: 8080 + +aws: + s3Bucket: dummy-bucket-federation-v1 # <-- insert-bucket-name-here + s3Endpoint: http://fake_s3:4570 # https://s3-eu-west-1.amazonaws.com:443 + # s3DownloadEndpoint: http://fake-s3:4570 + # ^ When not using a real S3 service, we may need to use a different, + # publicly accessible endpoint for downloading assets. + # + # If you want to use cloudfront for asset downloads + # cloudFront: + # domain: + # keyPairId: + # privateKey: cf-pk.pem + +settings: + maxTotalBytes: 27262976 + downloadLinkTTL: 300 # Seconds + federationDomain: example.com + disabledAPIVersions: [] + +logLevel: Warn +logNetStrings: false diff --git a/deploy/dockerephemeral/federation-v1/coredns-config/Corefile b/deploy/dockerephemeral/federation-v1/coredns-config/Corefile new file mode 100644 index 00000000000..7bf495f2e89 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/coredns-config/Corefile @@ -0,0 +1,4 @@ +example.com { + file /coredns-config/db.example.com + log +} \ No newline at end of file diff --git a/deploy/dockerephemeral/federation-v1/coredns-config/db.example.com b/deploy/dockerephemeral/federation-v1/coredns-config/db.example.com new file mode 100644 index 00000000000..407f4770916 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/coredns-config/db.example.com @@ -0,0 +1,21 @@ +$ORIGIN example.com. +@ 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. ( + 2017042745 ; serial + 7200 ; refresh (2 hours) + 3600 ; retry (1 hour) + 1209600 ; expire (2 weeks) + 3600 ; minimum (1 hour) + ) + + 3600 IN NS a.iana-servers.net. + 3600 IN NS b.iana-servers.net. + +www IN A 127.0.0.1 + IN AAAA ::1 +_wire-server-federator._tcp IN SRV 0 0 8443 host.docker.internal. +_wire-server-federator._tcp.b IN SRV 0 0 9443 host.docker.internal. +_wire-server-federator._tcp.d1 IN SRV 0 0 10443 host.docker.internal. +_wire-server-federator._tcp.d2 IN SRV 0 0 11443 host.docker.internal. +_wire-server-federator._tcp.d3 IN SRV 0 0 12443 host.docker.internal. +_wire-server-federator._tcp.federation-v0 IN SRV 0 0 21443 host.docker.internal. +_wire-server-federator._tcp.federation-v1 IN SRV 0 0 22443 host.docker.internal. diff --git a/deploy/dockerephemeral/federation-v1/federator.yaml b/deploy/dockerephemeral/federation-v1/federator.yaml new file mode 100644 index 00000000000..1973a0540be --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/federator.yaml @@ -0,0 +1,29 @@ +federatorInternal: + host: 0.0.0.0 + port: 8080 +federatorExternal: + host: 0.0.0.0 + port: 8081 +brig: + host: brig-federation-v1 + port: 8080 +cargohold: + host: cargohold-federation-v1 + port: 8080 +galley: + host: galley-federation-v1 + port: 8080 + +logLevel: Warn +logNetStrings: false + +optSettings: + # Filepath to one or more PEM-encoded server certificates to use as a trust + # store when making requests to remote backends + remoteCAStore: "/etc/wire/federator/conf/integration-ca.pem" + useSystemCAStore: false + clientCertificate: "/etc/wire/federator/conf/integration-leaf.pem" + clientPrivateKey: "/etc/wire/federator/conf/integration-leaf-key.pem" + tcpConnectionTimeout: 5000000 + dnsHost: 172.20.1.3 + dnsPort: 53 diff --git a/deploy/dockerephemeral/federation-v1/galley.yaml b/deploy/dockerephemeral/federation-v1/galley.yaml new file mode 100644 index 00000000000..f272536260c --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/galley.yaml @@ -0,0 +1,107 @@ +galley: + host: 0.0.0.0 + port: 8080 + +cassandra: + endpoint: + host: demo_wire_cassandra + port: 9042 + keyspace: galley_test_federation_v1 + # filterNodesByDatacentre: datacenter1 + +brig: + host: brig-federation-v1 + port: 8080 + +gundeck: + host: gundeck-federation-v1 + port: 8080 + +spar: + host: spar-federation-v1 + port: 8080 + +federator: + host: federator-federation-v1 + port: 8080 + +rabbitmq: + host: rabbitmq + port: 5672 + vHost: federation-v1 + +settings: + httpPoolSize: 128 + maxTeamSize: 32 + maxFanoutSize: 18 + exposeInvitationURLsTeamAllowlist: [] + maxConvSize: 16 + intraListing: false + conversationCodeURI: https://account.wire.com/conversation-join/ + concurrentDeletionEvents: 1024 + deleteConvThrottleMillis: 0 + # Federation domain is used to qualify local IDs and handles, + # e.g. 0c4d8944-70fa-480e-a8b7-9d929862d18c@wire.com and somehandle@wire.com. + # It should also match the SRV DNS records under which other wire-server installations can find this backend: + # _wire-server-federator._tcp. + # Once set, DO NOT change it: if you do, existing users may have a broken experience and/or stop working + # Remember to keep it the same in Brig + federationDomain: federation-v1.example.com + mlsPrivateKeyPaths: + removal: + ed25519: /etc/wire/galley/conf/mls-private-key-ed25519.pem + guestLinkTTLSeconds: 604800 + disabledAPIVersions: [] + + featureFlags: # see #RefConfigOptions in `/docs/reference` + sso: disabled-by-default + legalhold: whitelist-teams-and-implicit-consent + teamSearchVisibility: disabled-by-default + appLock: + defaults: + status: enabled + config: + enforceAppLock: false + inactivityTimeoutSecs: 60 + classifiedDomains: + status: enabled + config: + domains: ["example.com"] + fileSharing: + defaults: + status: enabled + lockStatus: unlocked + conferenceCalling: + defaults: + status: enabled + outlookCalIntegration: + defaults: + status: disabled + lockStatus: locked + mlsE2EId: + defaults: + status: disabled + config: + verificationExpiration: 86400 + acmeDiscoveryUrl: null + lockStatus: unlocked + mlsMigration: + defaults: + status: enabled + config: + startTime: "2029-05-16T10:11:12.123Z" + finaliseRegardlessAfter: "2029-10-17T00:00:00.000Z" + usersThreshold: 100 + clientsThreshold: 50 + lockStatus: locked + limitedEventFanout: + defaults: + status: disabled + +logLevel: Warn +logNetStrings: false + +journal: # if set, journals; if not set, disables journaling + queueName: integration-team-events-federation-v1.fifo + endpoint: http://demo_wire_sqs:4568 # https://sqs.eu-west-1.amazonaws.com + region: eu-west-1 diff --git a/deploy/dockerephemeral/federation-v1/gundeck.yaml b/deploy/dockerephemeral/federation-v1/gundeck.yaml new file mode 100644 index 00000000000..6722f2e0b90 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/gundeck.yaml @@ -0,0 +1,46 @@ +gundeck: + host: 0.0.0.0 + port: 8080 + +brig: + host: brig-federation-v1 + port: 8080 + +cassandra: + endpoint: + host: demo_wire_cassandra + port: 9042 + keyspace: gundeck_test_federation_v1 + # filterNodesByDatacentre: datacenter1 + +redis: + host: redis-federation-v1 + port: 6379 + connectionMode: master + +# redisAdditionalWrite: +# host: 127.0.0.1 +# port: 6379 +# connectionMode: master + +aws: + queueName: integration-gundeck-events-federation-v1 + region: eu-west-1 + account: "123456789012" # Default account nr used by localstack + arnEnv: integration + sqsEndpoint: http://demo_wire_sqs:4568 # https://sqs.eu-west-1.amazonaws.com + snsEndpoint: http://demo_wire_sns:4575 # https://sns.eu-west-1.amazonaws.com + +settings: + httpPoolSize: 1024 + notificationTTL: 24192200 + bulkPush: true + perNativePushConcurrency: 32 + sqsThrottleMillis: 1000 + maxConcurrentNativePushes: + hard: 30 # more than this number of threads will not be allowed + soft: 10 # more than this number of threads will be warned about + disabledAPIVersions: [] + +logLevel: Warn +logNetStrings: false diff --git a/deploy/dockerephemeral/federation-v1/integration-ca.pem b/deploy/dockerephemeral/federation-v1/integration-ca.pem new file mode 100644 index 00000000000..6a33fa9e2c3 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/integration-ca.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDEzCCAfugAwIBAgIUEfjIXW9tD1WgwNHJ+kC3r6Cmv5swDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAwwOY2EuZXhhbXBsZS5jb20wHhcNMjQwOTAzMTIwMzM3WhcN +MzQwOTAxMTIwMzM3WjAZMRcwFQYDVQQDDA5jYS5leGFtcGxlLmNvbTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJkd+ihX0Tp6RG4Ue9kQFvTHFy2DL8rC +8u956hy17/9sTyIcMkTOtZMew+ua+ZrNzX1x1WP4BT5RZQA/wy6ioEpP5BVFxe9s +dmVqYawD9C5+XKTekEFF3gI0MIGZ4Vum2hdUOHTwDatAgdiqsBwuCxHM5terItzZ +SsrkYFlAhISmM9CsUFOR+1rqpMNWRQD5zzZ0Sk5HeyM1/9sHvkrq24rqKqkg0/uR +OTHMydrmuSArG5tKqkdb2zoJwOs8somraRzB1JOm5/i/3pCT5iBr5gf8eLGtb2Gu +XgmnTI129R2a+LbifqTC31jIMg/yNfFuU4MdUG9wE512ghhXZI+Hoc8CAwEAAaNT +MFEwHQYDVR0OBBYEFFqlhDsVqlH8UUKGOtCDE9xmYL3hMB8GA1UdIwQYMBaAFFql +hDsVqlH8UUKGOtCDE9xmYL3hMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBADQyh0k3xhcrNOqhvzAK1/A3TY5Hq1FE/a17Yiq8DIiLCJG/nAN60GBx +m7zuL8xoRJ4ylwIswa4z4rHj9p6M6tIbi2tTfJsbyB+FjyFRWoBmTngqNCiw7QUR +/ofSliuEu/YIjphR8LmTBvy4fVccTwXDaBPEGf2iN+DFmryLHxVpsVh3AA0uUSy0 +e2bZJLRwv1z0saC5KGHpWb6RJbAP2nRw5omcorMtP1KW8XyVESiJm7hDZAx6VLgD +k4GcEOUEq9CJs9UVAkIIDS87CfppHZEGPDK3Ufro5AhIwA3hpSJkTgzkf0TcQKIr +4E/zJSeTld4rMC26ghWodIwGRyofTP0= +-----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/federation-v1/integration-leaf-key.pem b/deploy/dockerephemeral/federation-v1/integration-leaf-key.pem new file mode 100644 index 00000000000..e4ee0a09ab1 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/integration-leaf-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDPYxemHc21P8y2 +DIDNsuUzVpnc1bj81QIQ5a2aFSVtQFr3Dhpo0XXyu0KHKR64ZelmW33Yoe2J0zwl +R66fg07whBcZspsonXmsUB5uFFF/Dcv9shBNcg67jjynXPDvJK2sC37GCar5ar/v +11v/+Iq6LNerC/OqMJlMYyFDif1BvESbnw+9hsihUT9qK5s6md5krEV1/ro0Noh7 +kBgflylEKjapfCrVMnAMgaMV55jrk5BTpoR6KCDuDmXPk3Ed304Op/krGazfGB2j +1rz3IPfKOOtIgxn8n5PDYmShVUw1R3Rps1LeEyhsH1EGuAvVx1mKhB3QGMqFtIfo +wOpkgZPdAgMBAAECggEAVMurFUB1ZkkmXj9hgPnHLoUX10xJ3ZMIy7jlkS1ZRsD8 +EK0jDj2q0OtRSet9xJ7i3nfFTojzE5obqxCSrWUmp0ATI+4789DjuZluv8quAdm1 +0U73zHq43GZNlY7yco2YN1Lh7H5yepXz0dDILLLGolYIfscdw7YoUCvuI2vt8eyY +FaMkOi8Bs32Abn+53MhaNkEOVemmwP/u2rD81IY7pXQtF+1XfSxYBipHCB7phC1g +dr6ITU9CoF3SXvjYL6uP/7W1Du/CuaDSijVZJkBfQSgbkQbBr6pAovumtpNZgZvO +bixqS4oTZqsd+Rgl3YBOpx+JbuUFL0fG+uQBv1yZbwKBgQD8n7s1w/ouUGwW+qGi +4EFTYVDFD2Rgvg3oFPHt72zreaNXSy8YWrK9Mas/Fzu5knSE3f3O0S1WvqWdt/z6 +uPDLTpg9fIWX6v+hPX09F5ekeLlUDBazBT1PQUD0qd8PiyFNB1F+ffSPTxmKScjc +hTqQCtnun6rlICalWO9VvGt6QwKBgQDSKJkjxIGLtXBaXfMQiF9dQDrMk2it5grl +w0OnpPhYvpdp+Cfi01kMUrnfHwF0v92BqeqoKZo4DwJXkrmwf5kNnftxmRqbk9gE +dJq/E/6SELyT/chtzXfxC/wTmyyxhfZUJvJUxlaZ6KP/86t8A22DJcFO2Z85iUGH +8zy2UJUnXwKBgBvSs9m+FeXX8a+uNvMrY8Z9J1os0c9d30Y6WFLuVb6xjO3mV++E +vb7co5G1S1yq5q5jjLqkiyvMn4z5YKF0kQCzTU0oU8ZhmXn2vb5mxMrWiQLauf1J +jHEYLMFFnE2n8yj6r10RHkhSW+vBKKAxBDwtFceUSkwl+FupqeJ1eBjlAoGBAMou ++LWqdZ89HSwzOobrTCPgiTELmCfFKzLE2q/MTIjEQ9NVRLo57m+mnt+DatkxRR9b +oz/JVm8cMXqi1DZza4HoPWGalDic0bPnooC18bIAnAwcmdjZVcz3ZLpQDX10jfmD +xpu8fNBxOmYhvRcADTmg9wqu3zpxTDRI1F3pxLUtAoGAGfsX4bve5cLm49Oa1p0H +kEErLMuAMIKQNVsbzVELepLYr+uwEXBCXyyoIf79ABDvUHbzxMEwgANuet/4PQzS +yB1qzFk6GDvqZ5dfPUgMUWH9wvD1qEGp6yxkyESGt8CNwnu8GI50NAeSh2/JeUIa +r/u+m2vnJjOXpJdOJ+7f6yM= +-----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/federation-v1/integration-leaf.pem b/deploy/dockerephemeral/federation-v1/integration-leaf.pem new file mode 100644 index 00000000000..abd724df6b1 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/integration-leaf.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgIBADANBgkqhkiG9w0BAQsFADAZMRcwFQYDVQQDDA5jYS5l +eGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzhaFw0zNDA5MDExMjAzMzhaMAAwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPYxemHc21P8y2DIDNsuUzVpnc +1bj81QIQ5a2aFSVtQFr3Dhpo0XXyu0KHKR64ZelmW33Yoe2J0zwlR66fg07whBcZ +spsonXmsUB5uFFF/Dcv9shBNcg67jjynXPDvJK2sC37GCar5ar/v11v/+Iq6LNer +C/OqMJlMYyFDif1BvESbnw+9hsihUT9qK5s6md5krEV1/ro0Noh7kBgflylEKjap +fCrVMnAMgaMV55jrk5BTpoR6KCDuDmXPk3Ed304Op/krGazfGB2j1rz3IPfKOOtI +gxn8n5PDYmShVUw1R3Rps1LeEyhsH1EGuAvVx1mKhB3QGMqFtIfowOpkgZPdAgMB +AAGjgawwgakwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEgGA1UdEQEB +/wQ+MDyCGSouaW50ZWdyYXRpb24uZXhhbXBsZS5jb22CFGhvc3QuZG9ja2VyLmlu +dGVybmFsgglsb2NhbGhvc3QwHQYDVR0OBBYEFBkpAu3ILiU4gtEYffAU7zxGHPC6 +MB8GA1UdIwQYMBaAFFqlhDsVqlH8UUKGOtCDE9xmYL3hMA0GCSqGSIb3DQEBCwUA +A4IBAQBB1VsthdoVT9ExXkfKixotbXm6+eBgYenK1R5Qx/UX3JrlI1nF/8rKMg5e +7QfMCydSJwVEQdvnXD3ddVhUTYRActQvnJwWTyXfeiezrfDCTLu4SNpLOP7ojFlq +9ZX/E9GC0axTIUmEIy8YIC3JJ2PAlvw9qMzrsivyAgbof3NX+9XXKfwZHBwSLsO1 +Gxr9zkL+U/qww7TvyJD1LqBR0UEd9pZriorpVVFAa/JlFQX5ip1Smcd6m97nq20N +qpUIalra+K6qHxjHVwA2UxVgbO9bLFIBmp9pNvSm+5umAKkmqFnHRNAHfCy/IFGl +3fw8u9mXJ8LzUR4tiS0cVb6bwQzd +-----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/federation-v1/jwt-ed25519-bundle.pem b/deploy/dockerephemeral/federation-v1/jwt-ed25519-bundle.pem new file mode 100644 index 00000000000..afbd4dfb0ec --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/jwt-ed25519-bundle.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIFANnxZLNE4p+GDzWzR3wm/v8x/0bxZYkCyke1aTRucX +-----END PRIVATE KEY----- +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEACPvhIdimF20tOPjbb+fXJrwS2RKDp7686T90AZ0+Th8= +-----END PUBLIC KEY----- diff --git a/deploy/dockerephemeral/federation-v1/mls-private-key-ed25519.pem b/deploy/dockerephemeral/federation-v1/mls-private-key-ed25519.pem new file mode 100644 index 00000000000..182df6f5a7d --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/mls-private-key-ed25519.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIKqoSUVW579Aw8Nz47CRwArSigl/25jg0suQmg6mOwdy +-----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/federation-v1/nexmo-credentials.yaml b/deploy/dockerephemeral/federation-v1/nexmo-credentials.yaml new file mode 100644 index 00000000000..1f83517f2ee --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/nexmo-credentials.yaml @@ -0,0 +1,2 @@ +key: "dummy" +secret: "dummy" diff --git a/deploy/dockerephemeral/federation-v1/nginz/conf/README.md b/deploy/dockerephemeral/federation-v1/nginz/conf/README.md new file mode 100644 index 00000000000..8e614e99d1b --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/nginz/conf/README.md @@ -0,0 +1,7 @@ +# How to regenerate certificates in this directory + +Run from this directory: + +```bash +../../../../../hack/bin/gen-certs.sh +``` diff --git a/deploy/dockerephemeral/federation-v1/nginz/conf/common_response.conf b/deploy/dockerephemeral/federation-v1/nginz/conf/common_response.conf new file mode 100644 index 00000000000..1b8a947f437 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/nginz/conf/common_response.conf @@ -0,0 +1,38 @@ + # remove access_token from logs, see 'Note sanitized_request'. + set $sanitized_request $request; + if ($sanitized_request ~ (.*)access_token=[^&]*(.*)) { + set $sanitized_request $1access_token=****$2; + } + + # Should be overriden when using websockets + proxy_set_header Connection ""; + proxy_set_header Z-Type $zauth_type; + proxy_set_header Z-User $zauth_user; + proxy_set_header Z-Client $zauth_client; + proxy_set_header Z-Connection $zauth_connection; + proxy_set_header Z-Provider $zauth_provider; + proxy_set_header Z-Bot $zauth_bot; + proxy_set_header Z-Conversation $zauth_conversation; + proxy_set_header Request-Id $request_id; + + # NOTE: This should only be used on endpoints where credentials are needed + more_set_headers 'Access-Control-Allow-Credentials: true'; + # NOTE: This allows all origins, you may want to tune this value + more_set_headers 'Access-Control-Allow-Origin: $http_origin'; + more_set_headers 'Access-Control-Expose-Headers: Request-Id, Location'; + more_set_headers 'Request-Id: $request_id'; + more_set_headers 'Strict-Transport-Security: max-age=31536000; preload'; + + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Methods' "GET, POST, PUT, DELETE, OPTIONS"; + add_header 'Access-Control-Allow-Headers' "$http_access_control_request_headers, DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type"; + add_header 'Content-Type' 'text/plain; charset=UTF-8'; + add_header 'Content-Length' 0; + return 204; + } + + + proxy_http_version 1.1; + + # NOTE: You may want to tune this + client_max_body_size 64M; diff --git a/deploy/dockerephemeral/federation-v1/nginz/conf/common_response_no_zauth.conf b/deploy/dockerephemeral/federation-v1/nginz/conf/common_response_no_zauth.conf new file mode 100644 index 00000000000..4277ede8c0f --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/nginz/conf/common_response_no_zauth.conf @@ -0,0 +1,2 @@ + zauth off; + include common_response.conf; diff --git a/deploy/dockerephemeral/federation-v1/nginz/conf/common_response_with_zauth.conf b/deploy/dockerephemeral/federation-v1/nginz/conf/common_response_with_zauth.conf new file mode 100644 index 00000000000..699dd263b31 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/nginz/conf/common_response_with_zauth.conf @@ -0,0 +1,3 @@ + include common_response.conf; + proxy_set_header Authorization ""; + proxy_set_header Z-Host $host; diff --git a/deploy/dockerephemeral/federation-v1/nginz/conf/integration.conf b/deploy/dockerephemeral/federation-v1/nginz/conf/integration.conf new file mode 100644 index 00000000000..74dd1a09113 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/nginz/conf/integration.conf @@ -0,0 +1,19 @@ +# plain TCP/http listening for integration tests only. +listen 8080; +listen 8081; + +# for nginx-without-tls, we need to use a separate port for http2 traffic, +# as nginx cannot handle unencrypted http1 and http2 traffic on the same +# port. +# This port is only used for trying out nginx http2 forwarding without TLS locally and should not +# be ported to any production nginz config. +listen 8090; + +######## TLS/SSL block start ############## +# +# Most integration tests simply use the http ports 8080 and 8081 +# But to also test tls forwarding, this port can be used. +# This applies only locally, as for kubernetes (helm chart) based deployments, +# TLS is terminated at the ingress level, not at nginz level +listen 8443 ssl http2; +listen [::]:8443 ssl http2; diff --git a/deploy/dockerephemeral/federation-v1/nginz/conf/nginx.conf b/deploy/dockerephemeral/federation-v1/nginz/conf/nginx.conf new file mode 100644 index 00000000000..43f8c68b306 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/nginz/conf/nginx.conf @@ -0,0 +1,518 @@ +worker_processes 4; +worker_rlimit_nofile 1024; +include pid.conf; # for easy overriding + +# nb. start up errors (eg. misconfiguration) may still end up in /$(LOG_PATH)/error.log +error_log stderr warn; + +events { + worker_connections 1024; + multi_accept off; +} + +http { + # + # Some temporary paths (by default, will use the `prefix` path given when starting nginx) + # + + client_body_temp_path /tmp; + fastcgi_temp_path /tmp; + proxy_temp_path /tmp; + scgi_temp_path /tmp; + uwsgi_temp_path /tmp; + + # + # Sockets + # + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + + # + # Timeouts + # + + client_body_timeout 60; + client_header_timeout 60; + keepalive_timeout 75; + send_timeout 60; + + ignore_invalid_headers off; + + types_hash_max_size 2048; + + server_names_hash_bucket_size 64; + server_name_in_redirect off; + + large_client_header_buffers 4 8k; + + # + # Security + # + + server_tokens off; + + # + # Logging + # + # Note sanitized_request: + # We allow passing access_token as query parameter for e.g. websockets + # 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'; + access_log /dev/stdout custom_zeta; + + # + # Monitoring + # + vhost_traffic_status_zone; + + # + # Gzip + # + + gzip on; + gzip_disable msie6; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_min_length 1024; + gzip_types 'text/plain text/css application/json text/xml'; + + # + # Proxied Upstream Services + # + + include ../upstreams; + + # + # Mapping for websocket connections + # + + map $http_upgrade $connection_upgrade { + websocket upgrade; + default ''; + } + + # + # Locations + # + + server { + # elastic search does not support running http and https listeners + # at the same time. so our instance only runs https, but + # federation-v1 only supports http. this proxy rule helps with + # that. + # + # see also: git grep -Hn 'elasticsearch:' ../../brig.yaml + listen 9201; + + zauth_keystore /etc/wire/zauth-pubkeys.txt; + zauth_acl /etc/wire/nginz/conf/zauth_acl.txt; + + location "" { + zauth off; + + proxy_pass https://demo_wire_elasticsearch:9200; + proxy_set_header Authorization "Basic ZWxhc3RpYzpjaGFuZ2VtZQ=="; + } + } + + server { + include integration.conf; + + # self-signed certificates generated using wire-server/hack/bin/gen-certs.sh + ssl_certificate /etc/wire/integration-leaf.pem; + ssl_certificate_key /etc/wire/integration-leaf-key.pem; + + ssl_verify_client on; + ssl_client_certificate /etc/wire/integration-ca.pem; + ######## TLS/SSL block end ############## + + zauth_keystore /etc/wire/zauth-pubkeys.txt; + zauth_acl /etc/wire/nginz/conf/zauth_acl.txt; + # needs to be kept in sync with services/brig/test/resources/oauth/ed25519.jwk + oauth_pub_key /etc/wire/oauth-ed25519_public.jwk; + + location /status { + set $sanitized_request $request; + zauth off; + return 200; + } + + location /i/status { + set $sanitized_request $request; + zauth off; + return 200; + } + + location /vts { + set $sanitized_request $request; + zauth off; + vhost_traffic_status_display; + vhost_traffic_status_display_format html; + } + + # + # Service Routing + # + + # Federator endpoints: expose the federatorExternal port (Inward service) + location /federation { + set $sanitized_request $request; + zauth off; + + proxy_set_header "X-SSL-Certificate" $ssl_client_escaped_cert; + proxy_pass http://federator_external; + + # FUTUREWORK(federation): are any other settings + # (e.g. timeouts, body size, buffers, headers,...) + # useful/recommended/important-for-security?) + } + + # Brig Endpoints + # + ## brig unauthenticated endpoints + + location ~* ^(/v[0-9]+)?/api/swagger-ui { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^(/v[0-9]+)?/api/swagger.json { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^(/v[0-9]+)?/api-internal/swagger-ui { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^(/v[0-9]+)?/api-internal/swagger.json { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location /register { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location /access { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location /activate { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location /login { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^(/v[0-9]+)?/teams/invitations/([^/]*)$ { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location /verification-code/send { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + ## brig authenticated endpoints + + location ~* ^(/v[0-9]+)?/self$ { + include common_response_with_zauth.conf; + oauth_scope self; + proxy_pass http://brig; + } + + location /users { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location /list-users { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location /search { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location /list-connections { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^/teams/([^/]+)/search$ { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location /connections { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^(/v[0-9]+)?/clients { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^(/v[0-9]+)?/mls/key-packages { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location /properties { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location /calls/config { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^/teams/([^/]*)/size$ { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^(/v[0-9]+)?/system/settings/unauthorized$ { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^(/v[0-9]+)?/system/settings$ { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^/oauth/clients/([^/]*)$ { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^/oauth/authorization/codes$ { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location /oauth/token { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location /oauth/revoke { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location /oauth/applications { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + # Cargohold Endpoints + + location ~* ^(/v[0-9]+)?/assets { + include common_response_with_zauth.conf; + proxy_pass http://cargohold; + } + + location /assets { + include common_response_with_zauth.conf; + proxy_pass http://cargohold; + } + + location /bot/assets { + include common_response_with_zauth.conf; + proxy_pass http://cargohold; + } + + location /provider/assets { + include common_response_with_zauth.conf; + proxy_pass http://cargohold; + } + + # Galley Endpoints + + location ~* ^(/v[0-9]+)?/legalhold/conversations/(.*)$ { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^(/v[0-9]+)?/conversations$ { + include common_response_with_zauth.conf; + oauth_scope conversations; + proxy_pass http://galley; + } + + location ~* ^(/v[0-9]+)?/conversations/([^/]*)/code { + include common_response_with_zauth.conf; + oauth_scope conversations_code; + proxy_pass http://galley; + } + + location ~* ^(/v[0-9]+)?/conversations.* { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^/conversations/([^/]*)/otr/messages { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^/conversations/([^/]*)/([^/]*)/proteus/messages { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location /broadcast { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location /bot/conversation { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location /bot/messages { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^/teams$ { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^/teams/([^/]*)$ { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^/teams/([^/]*)/members(.*) { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^/teams/([^/]*)/conversations(.*) { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^/teams/([^/]*)/features { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^/teams/([^/]*)/features/([^/]*) { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^(/v[0-9]+)?/feature-configs$ { + include common_response_with_zauth.conf; + oauth_scope feature_configs; + proxy_pass http://galley; + } + + location ~* ^(/v[0-9]+)?/feature-configs(.*) { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^/teams/([^/]*)/legalhold(.*) { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^/teams/([^/]*)/members/csv$ { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location /mls/welcome { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location /mls/messages { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^(/v[0-9]+)?/mls/commit-bundles { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^(/v[0-9]+)?/mls/public-keys { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + # Gundeck Endpoints + + location /push { + include common_response_with_zauth.conf; + proxy_pass http://gundeck; + } + + location /presences { + include common_response_with_zauth.conf; + proxy_pass http://gundeck; + } + + location ~* ^(/v[0-9]+)?/notifications$ { + include common_response_with_zauth.conf; + proxy_pass http://gundeck; + } + + # Proxy Endpoints + + location /proxy { + include common_response_with_zauth.conf; + proxy_pass http://proxy; + } + + # Cannon Endpoints + + location /await { + include common_response_with_zauth.conf; + proxy_pass http://cannon; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 1h; + } + + # Spar Endpoints + + location /sso { + include common_response_no_zauth.conf; + proxy_pass http://spar; + } + + location /identity-providers { + include common_response_with_zauth.conf; + proxy_pass http://spar; + } + } +} diff --git a/deploy/dockerephemeral/federation-v1/nginz/conf/pid.conf b/deploy/dockerephemeral/federation-v1/nginz/conf/pid.conf new file mode 100644 index 00000000000..e722aa5ae23 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/nginz/conf/pid.conf @@ -0,0 +1 @@ +pid /tmp/nginz.pid; diff --git a/deploy/dockerephemeral/federation-v1/nginz/conf/zauth_acl.txt b/deploy/dockerephemeral/federation-v1/nginz/conf/zauth_acl.txt new file mode 100644 index 00000000000..3b644bf3d98 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/nginz/conf/zauth_acl.txt @@ -0,0 +1,15 @@ +a (blacklist (regex "(/v[0-9]+)?/provider(/.*)?") + (regex "(/v[0-9]+)?/bot(/.*)?") + (regex "(/v[0-9]+)?/i/.*")) + +b (whitelist (regex "(/v[0-9]+)?/bot(/.*)?")) + +p (whitelist (regex "(/v[0-9]+)?/provider(/.*)?")) + +# LegalHold Access Tokens +# FUTUREWORK: remove /legalhold/conversations/ when support for v1 dropped +la (whitelist (regex "(/v[0-9]+)?/notifications") + (regex "(/v[0-9]+)?/assets/v3/.*") + (regex "(/v[0-9]+)?/users(/.*)?") + (regex "(/v[0-9]+)?/legalhold/conversations/[^/]+") + (regex "(/v[0-9]+)?/conversations/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")) diff --git a/deploy/dockerephemeral/federation-v1/nginz/upstreams b/deploy/dockerephemeral/federation-v1/nginz/upstreams new file mode 100644 index 00000000000..cbf7b353fe1 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/nginz/upstreams @@ -0,0 +1,38 @@ +upstream cargohold { + least_conn; + keepalive 32; + server cargohold-v1:8080 max_fails=3 weight=1; +} +upstream gundeck { + least_conn; + keepalive 32; + server gundeck-v1:8080 max_fails=3 weight=1; +} +upstream cannon { + least_conn; + keepalive 32; + server cannon-v1:8080 max_fails=3 weight=1; +} +upstream galley { + least_conn; + keepalive 32; + server galley-v1:8080 max_fails=3 weight=1; +} +upstream proxy { + least_conn; + keepalive 32; + server proxy-v1:8080 max_fails=3 weight=1; +} +upstream brig { + least_conn; + keepalive 32; + server brig-v1:8080 max_fails=3 weight=1; +} +upstream spar { + least_conn; + keepalive 32; + server spar-v1:8080 max_fails=3 weight=1; +} +upstream federator_external { + server federator-v1:8081 max_fails=3 weight=1; +} diff --git a/deploy/dockerephemeral/federation-v1/oauth-ed25519.jwk b/deploy/dockerephemeral/federation-v1/oauth-ed25519.jwk new file mode 100644 index 00000000000..c00a8270aa4 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/oauth-ed25519.jwk @@ -0,0 +1 @@ +{"kty":"OKP","crv":"Ed25519","x":"mhP-NgFw3ifIXGZqJVB0kemt9L3BtD5P8q4Gah4Iklc","d":"R8-pV2-sPN7dykV8HFJ73S64F3kMHTNnJiSN8UdWk_o"} diff --git a/deploy/dockerephemeral/federation-v1/proxy.config b/deploy/dockerephemeral/federation-v1/proxy.config new file mode 100644 index 00000000000..d2225ca26c9 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/proxy.config @@ -0,0 +1,8 @@ +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/deploy/dockerephemeral/federation-v1/proxy.yaml b/deploy/dockerephemeral/federation-v1/proxy.yaml new file mode 100644 index 00000000000..43a117e6a20 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/proxy.yaml @@ -0,0 +1,20 @@ +# Example yaml-formatted configuration for proxy +# proxy can be started with a config file (e.g. ./dist/proxy -c proxy.yaml.example) + +host: 0.0.0.0 +port: 8080 + +# number of connections for the http pool +httpPoolSize: 1000 + +# maximum number of incoming connections +maxConns: 5000 + +# File containing upstream secrets. +secretsConfig: /etc/wire/proxy/conf/proxy.config + +disabledAPIVersions: [] + +# Logging settings +logLevel: Info +logNetStrings: false diff --git a/deploy/dockerephemeral/federation-v1/spar.yaml b/deploy/dockerephemeral/federation-v1/spar.yaml new file mode 100644 index 00000000000..e111292bc03 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/spar.yaml @@ -0,0 +1,44 @@ +saml: + version: SAML2.0 + logLevel: Warn + + spHost: 0.0.0.0 + spPort: 8080 + # TODO: change these + spAppUri: http://localhost:8080/ + spSsoUri: http://localhost:8080/sso + + contacts: + - type: ContactBilling + company: evil corp. + givenName: Dr. + surname: Girlfriend + email: email:president@evil.corp + +brig: + host: brig-federation-v1 + port: 8080 + +galley: + host: galley-federation-v1 + port: 8080 + +cassandra: + endpoint: + host: demo_wire_cassandra + port: 9042 + keyspace: spar_test_federation_v1 + filterNodesByDatacentre: datacenter1 + +# Wire/AWS specific, optional +# discoUrl: "https://" + +disabledAPIVersions: [] + +maxttlAuthreq: 5 # seconds. don't set this too large, it is also the run time of one TTL test. +maxttlAuthresp: 7200 # seconds. do not set this to 1h or less, as that is what the mock idp wants. + +maxScimTokens: 2 # Token limit {#RefScimToken} +richInfoLimit: 5000 # should be in sync with Brig + +logNetStrings: False # log using netstrings encoding (see http://cr.yp.to/proto/netstrings.txt) diff --git a/deploy/dockerephemeral/federation-v1/turn-secret.txt b/deploy/dockerephemeral/federation-v1/turn-secret.txt new file mode 100644 index 00000000000..5e558cab2cc --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/turn-secret.txt @@ -0,0 +1 @@ +xMtZyTpu=Leb?YKCoq#BXQR:gG^UrE83dNWzFJ2VcD \ No newline at end of file diff --git a/deploy/dockerephemeral/federation-v1/twilio-credentials.yaml b/deploy/dockerephemeral/federation-v1/twilio-credentials.yaml new file mode 100644 index 00000000000..d64e0ec4f23 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/twilio-credentials.yaml @@ -0,0 +1,2 @@ +sid: "dummy" +token: "dummy" diff --git a/deploy/dockerephemeral/federation-v1/zauth-privkeys.txt b/deploy/dockerephemeral/federation-v1/zauth-privkeys.txt new file mode 100644 index 00000000000..7b6d17ed984 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/zauth-privkeys.txt @@ -0,0 +1,4 @@ +qjIAZtKrpXInwyqgM7JCZ3QeK9B4JGBYAv1_63YjTtgDylLfTTpdwvDYSy32is13biThD03QZAUOhBO042Odrw== +dNLsH_oIA6hJCyw-AwokLz3AukHNghlP3H-pW5Ao1Wy06OI2MGgBwRnvjgfI2l1mgCLPJQflUR-7DsYO0p6zoQ== +drShe2GnggBy-VAW1gdE6myf4UAFcN1ZdixCO8NRuYLv_TO-xNQzRj-8RfemJ4R6Oz-R5KTfP6Oj_Tj0qezDTw== +tZWlAKOCe5-vlQl0TbECvxeIptEBGRrnGSiej-olAFe-46gXpFkWTas2Ci84VUWyhWzRJj4rtBmyJkAm-TMvwQ== diff --git a/deploy/dockerephemeral/federation-v1/zauth-pubkeys.txt b/deploy/dockerephemeral/federation-v1/zauth-pubkeys.txt new file mode 100644 index 00000000000..661fcfc71ba --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/zauth-pubkeys.txt @@ -0,0 +1,4 @@ +A8pS3006XcLw2Est9orNd24k4Q9N0GQFDoQTtONjna8= +tOjiNjBoAcEZ744HyNpdZoAizyUH5VEfuw7GDtKes6E= +7_0zvsTUM0Y_vEX3pieEejs_keSk3z-jo_049Knsw08= +vuOoF6RZFk2rNgovOFVFsoVs0SY-K7QZsiZAJvkzL8E= diff --git a/deploy/dockerephemeral/init_vhosts.sh b/deploy/dockerephemeral/init_vhosts.sh index 688d635e0a5..93746557fa8 100755 --- a/deploy/dockerephemeral/init_vhosts.sh +++ b/deploy/dockerephemeral/init_vhosts.sh @@ -16,5 +16,6 @@ create_vhost d1.example.com create_vhost d2.example.com create_vhost d3.example.com create_vhost federation-v0 +create_vhost federation-v1 echo 'RabbitMQ resources created successfully!' diff --git a/deploy/dockerephemeral/rabbitmq-config/certificates/ca-key.pem b/deploy/dockerephemeral/rabbitmq-config/certificates/ca-key.pem index 406f6d9ed97..68009b9f18d 100644 --- a/deploy/dockerephemeral/rabbitmq-config/certificates/ca-key.pem +++ b/deploy/dockerephemeral/rabbitmq-config/certificates/ca-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC/vE2Cea18UZ1J -J0a3IkIoXl2JPSJp7y/bPXsN6sk44F5Dv9mt5hxVERyCQSMiuM6dXfzkRcMAZ7dx -5nQ7GpSEJksqe4h+WFHWDQjaoxrOYVg9UAa6q0rq5h+uHZEpBWwJWNlwRgzyf5zf -IZnjttVD2mu4Gp2xRqtNkEbAOgMJp7ijb76foKsGLFrxJNA3khNjsnDlwRuoffVS -LafF0CA7cW2FYxjwKM/IymCaRVUS18IftCtm3KCl5ou+1aD0/rMsLMKEY1HYCyGo -ZSOnvd5xhRPj6upk3MpWUUyULSkpkQtVPy+RZKUNXb3CGVNJz3UgvMwNXKpW9FdG -Suze9HxdAgMBAAECggEAEU8SKZA10tOaAQue/P4GaOyJQdAXYObV3tNAXkjux3Ks -hS3hnIBPLc1wpxWdnWR/n9c8nZg/+rO3l3xiy8nM1IKR0JD8Xnjh/RKKKmqvtdKL -NmXDZcCm775nPRRa5rrK6QEbXWEFiYgZr6Rckcu57vkzNkM42dMeYyR+Lpujazs6 -Um3Z7rPXevX/gVr9XHjxJ5bX9WYB7sJfZTHLqkO7VGwrXf7HGrtT1ES+iXqjGLpH -5Sg55V5XJfxsqhq+TQgEnorzp8+LEXms2HYTP3G47wP51IWbHa54BUBwkwhiNYV7 -os71j5mrZbUnJ/2KvQPMjiF7uHKlKYjxXiAoj9wRZQKBgQD4e4RuFVaLtF1+khNI -uEgmY4AfakeCB9D2Do1/fhLDTT6EdAxFeSx62VyY3wTG5Pi8DyrFIUNbIYbO8vRx -u8XpzCPxn9TnPnLZ9BRf1+GrCuyQWaFZOnnfAovk3KK4D3vWD9Yn38aTYpTd+3Hg -AEIzd7Bd4dozKtKW7+wI9uOm0wKBgQDFiUih6D0TYrS4T+cM5KhI+ErqTTiFpZ/L -BvA2hyRZTbP+erII9A+IqRNlwidGc1UF4xGu9Ei5QBVfFFbch6C1IRwIoog0hqsH -7s47VIcDuoASq52DHoUABbw9SrfsLjAZz5bLNPmvrEorwIImHNwDG/yOgpT8z7PV -z4/MhoWyDwKBgB+8FrPAgechx/cMTO4yqvRMLObWOf+/Y86pGSU5Qsgyq1NbRt3w -ld+ytwLHKOMGB0ZtYXb/wox3AbKYkOOdqa8sZULMuPI3pY90fs2m0ql3obLl35d3 -wmza9GbsTtPXFmfGagF5sPDN3FllbavAHLRaCupSl/2E8JRaW/jhHz4FAoGAfL4H -Ggd4mkdY7JO4ytGS3BG/7Vo6eVtwH1wQUb7h22tQYUHGMBU/wgNTdo03FCw84uzT -+/HUAvhPBq3ndHhJqlhwRZut+82XL/lETv9AC8C4pBGv9F9PigYVK3eF0iYQxhvr -lAOuMZvRcvOsvLi4z1XbFXus7kGTxU+/9V52C00CgYBY5SgRETt5kgbH/rm36SsE -4x58yK8uYF8MgtBCLxn7E0vnZ2cAMmmDC9wWCHtuq2QhqL/pB+fPI8ri4XNPMXJC -faAxJ0VNmz8fYTzliAWy3Sqp/kgeXdrX9KJkN24LP345LocDBcaML+thDFevmXBW -mahBgoa1ZWxnLJe5XweVkg== +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCT9qwdV6saUQBO +oUmJGG33f6XXzq1W0sLGamI/Ouoo67WRYdPgFrY2FJIixWBolyDWXZ7Oki1T/Gnz +wlysp42f9s8yKbr1+dc4R7a7pcQXCsjfOYHJJtNyhi8+RjRhTZfAnmCX7kViUgPg +DsZEdvApAQQ+XkdHEXPI9BPW7g7WN+sJoKhLZA7wHOD75oACDy9iqL5pjKIRstoR +yWSyggPkBs3i1KkMfDExd0bGo1fLbUYPsH1EDOsHTCW7m/d8JoC1X53GkHTvRP4x +Fa9pFNFnidZJsbONHdW3dNlN//KJhsmskRl/aqn2hd4PqA8wKIhey2w7gu1TVRHP +Pzz9bem9AgMBAAECggEAB53Ld0kzfAvOXb3MqLtysapG8ahoYM9BinAgdYvFUOzt +MDoea2sP3xHJAfQyBcA9xvsTuSNqJOgZ1hvbpGGNlz3fpX/jaVT+gJ2kaN1cDimW +dHMj3KRcfwksNmLrwFR7qsUcSMXRmYGKRIbOKukKbLnqK4Gz3pMl6CK0QLyJU/qB +0v5rTFOX8XH3Krd59naXxWFvZ/5xdaQs4rnRyxS+N5xLEJ5McGyVwto5NHV9Jujs +CHK1Av5RDqY+lOh251ZZE5068+qdXFKuqDnJge/Uii82imNWH9NZMBDJgPr4SsNE +tWt8aU7d3Qrde1FQfxOCwgdgyQvCweTH90z7anrKWwKBgQDOZMn8Oo9OsJFvzhdz +iGrTeqpN9tZq8LrQ8Ob2hYY0+q+RaywbVIRt9A5LfWSSpOAQdGHnnBmm7TS8UiPl +OadgW7+ME0cp9sPLImpAxXnDoct5Edb8/qBN76eTO6jhz5aBYqfubb6SHHCOs5UF +uA7XpvvHD5Nlrzn19S4sSY5lSwKBgQC3hrtBWhtXj6YbfYQFsB+nwYEXqtcw8Azm +8xJtMl5mH+bWGr8zeUjVE+bfFK4CjLnVnwuK8OOq7fDrlQYbAGtJ8DJozkvjzxsY +ztDwnTSl4kubqhJMYWDh8GcM8ovI/y36wd8DKzl+MMPiCFmf66xtevp/KKGShNaO +i/JG9rZwFwKBgQClGodh2E6Peju9nrWv3C7oobXezFjWD2DCiBOanVGwy+DqiTst +WbzeYF+XD+YGURJU12UCbCMxH4wSIftJAYfdU0e1fC5vaVFTDGLHEbHIR4OHhDfh +Bqeh8NaytwTwLqmNMyh3WR8brthzr0DE5GorJQ9APDuDGltZYBrhnq3kZQKBgG2u +YLTy5ApVeGFPhxpbIuAAHmWFnWvK2vsfY/DMvGvuPufQPlrF7kghx8Wkt0Yg0mMf +1ScpRfb+kxBIFMkIXBZpLcdDG0m/maMe3vIeEbvd3W/fmWX6gIsnQH8VaYMrNlB5 +kw1yxL5s6HRqpx2THI6lg5WBM+a76vpwGtBcW7XrAoGACT/DkL63HNnLs01D4aru +YSmRI3eyCBBWd9qOciCgkGMXZJd+kfGCkFX/Z98PzsPpeVkv/kmsPUMLtObT80eq +HTCAGoHeswPzei5/PRvJ3XkKD0x4/YxHjJD6uKjvJGBfBo23zygZStsIO5cB9CFc +evdAe37T8iiUL3PWTaT50s0= -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/rabbitmq-config/certificates/ca.pem b/deploy/dockerephemeral/rabbitmq-config/certificates/ca.pem index cb18742fab2..2aa8d89e4ac 100644 --- a/deploy/dockerephemeral/rabbitmq-config/certificates/ca.pem +++ b/deploy/dockerephemeral/rabbitmq-config/certificates/ca.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- -MIIDJTCCAg2gAwIBAgIUaJxRWt/eEYHgz+Rs5QNWVHMfk5swDQYJKoZIhvcNAQEL -BQAwIjEgMB4GA1UEAwwXcmFiYml0bXEuY2EuZXhhbXBsZS5jb20wHhcNMjQwNjE3 -MTQwMjE0WhcNMzQwNjE1MTQwMjE0WjAiMSAwHgYDVQQDDBdyYWJiaXRtcS5jYS5l -eGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL+8TYJ5 -rXxRnUknRrciQiheXYk9ImnvL9s9ew3qyTjgXkO/2a3mHFURHIJBIyK4zp1d/ORF -wwBnt3HmdDsalIQmSyp7iH5YUdYNCNqjGs5hWD1QBrqrSurmH64dkSkFbAlY2XBG -DPJ/nN8hmeO21UPaa7ganbFGq02QRsA6AwmnuKNvvp+gqwYsWvEk0DeSE2OycOXB -G6h99VItp8XQIDtxbYVjGPAoz8jKYJpFVRLXwh+0K2bcoKXmi77VoPT+sywswoRj -UdgLIahlI6e93nGFE+Pq6mTcylZRTJQtKSmRC1U/L5FkpQ1dvcIZU0nPdSC8zA1c -qlb0V0ZK7N70fF0CAwEAAaNTMFEwHQYDVR0OBBYEFN8gWZGKR0/K/e+qyGcN+8Ae -IokuMB8GA1UdIwQYMBaAFN8gWZGKR0/K/e+qyGcN+8AeIokuMA8GA1UdEwEB/wQF -MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAKTpmSYDx+Fabe/idnMlC9+5KaQmD/dp -x1BW8HZT+ZK+NuadPUVyUx1xHOw+wh1u5G8docGkrCsA/hvgyIRSyycJRCaySt1y -zjml3s3T4wRktgx6Z5X3kfw612/tZ5NE4QyQuN9A7DC9Fh4Z520fMDel15D+t70z -nNjZdp5gxpJPUJCebJ7+OhSUhtgr6g4hXwNqDR7DLwXyhp90UFdjfx4kBYFE8Vnk -nA9ZwC7GhUioMV/yXOuekyiJBv9LtaSuc/Y29EbLufLAwZJD1lA7WN254nNmZgAE -hAhTqL6dgvIIhuKHQ6f4vqAWi4FsrRy6cvh7S80+ldcchMBDcIgh1BA= +MIIDJTCCAg2gAwIBAgIUBbMHNT+GZgCVyopxX3sciD+E5uowDQYJKoZIhvcNAQEL +BQAwIjEgMB4GA1UEAwwXcmFiYml0bXEuY2EuZXhhbXBsZS5jb20wHhcNMjQwOTAz +MTIwMzQwWhcNMzQwOTAxMTIwMzQwWjAiMSAwHgYDVQQDDBdyYWJiaXRtcS5jYS5l +eGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJP2rB1X +qxpRAE6hSYkYbfd/pdfOrVbSwsZqYj866ijrtZFh0+AWtjYUkiLFYGiXINZdns6S +LVP8afPCXKynjZ/2zzIpuvX51zhHtrulxBcKyN85gckm03KGLz5GNGFNl8CeYJfu +RWJSA+AOxkR28CkBBD5eR0cRc8j0E9buDtY36wmgqEtkDvAc4PvmgAIPL2KovmmM +ohGy2hHJZLKCA+QGzeLUqQx8MTF3RsajV8ttRg+wfUQM6wdMJbub93wmgLVfncaQ +dO9E/jEVr2kU0WeJ1kmxs40d1bd02U3/8omGyayRGX9qqfaF3g+oDzAoiF7LbDuC +7VNVEc8/PP1t6b0CAwEAAaNTMFEwHQYDVR0OBBYEFOv/4GK9l7p7p9nk2hf/59sD +PhEVMB8GA1UdIwQYMBaAFOv/4GK9l7p7p9nk2hf/59sDPhEVMA8GA1UdEwEB/wQF +MAMBAf8wDQYJKoZIhvcNAQELBQADggEBABt+JodEGOjnFA+VCnRWOGl1q4wlcEbl ++5mEuVwwWGzbispmJxIdf+FlOotonvhksGQUDZ3gr7FvLcsGy6OnOK2YBSLOcnRP +amKPaiQwB38VcxQEUOL+1ZqLsLTseGJUCkGk+OmfjInqCURS5jRUbVtYZiqkzD40 +7Rz5iyrXwv1vbuXpW2s/kUgD6dLrRwt1ydaxCbA3C92farZJFvpUwTyhAXUkKyPZ +Hgu5E/nppujH2h6nOJfHGcyaVHai7pDManjO1icWmfx+t2s94rdAEevvBu0k/qL4 +tXWWSh81MtGjLjQ88ozbmr7/LSo3KaAB7M/AnZdL3JjtmFy9eFhqQaY= -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/rabbitmq-config/certificates/cert.pem b/deploy/dockerephemeral/rabbitmq-config/certificates/cert.pem index 6d5744d1f7d..f055a8ab9b4 100644 --- a/deploy/dockerephemeral/rabbitmq-config/certificates/cert.pem +++ b/deploy/dockerephemeral/rabbitmq-config/certificates/cert.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- MIIDPTCCAiWgAwIBAgIBADANBgkqhkiG9w0BAQsFADAiMSAwHgYDVQQDDBdyYWJi -aXRtcS5jYS5leGFtcGxlLmNvbTAeFw0yNDA2MTcxNDAyMTRaFw0yNDA3MTcxNDAy -MTRaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBAJZ3b8mfnf8XuUJmFQ8xN9V8N1PiMe5X+WMqOKduZXqPeW9rECmC -B3opcDVMQ3iyRtc+fXYSJiCllMeCCwzIWQw+k1PcFZ6zXWsvtEFQRCN91vcShZm0 -v8YlNcYl3wxsnIcZ5/IAZTiyX2U/hTBkgOszJcfe8cBOZsI9QzRuLRzE3kkpA+U7 -/3ekPsIxk/g0NtbRA4BgSrcKl3iAI4CMJTJlsezQbF6LZqW7yIOyvaQzT0kyJ564 -0X7YCT5QozL09ZdbQY5b6pphNNfXqY1KEP/aje+UrzQm2R3e9BUGMM4o14pQOU7Q -cxWRjPSPL3nDKUxI3kI9etrluFLH9lQ1uT8CAwEAAaOBizCBiDAdBgNVHSUEFjAU +aXRtcS5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzNDBaFw0zNDA5MDExMjAz +NDBaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL/WoO1EcKQXxxhoQ0l3STTha0N+8d7OVxJbx+rO0+eBtxa9IGnd +0X9yCm5o1dsrDRfVP/NsWAvJ3SieiDuK/bdv9bNjFVV7M+ZvxH73kARrts2Lbdhz +Ip8uv0ReZUnx33JbiT3pIhO0oOPaAtc6nRhcLMT4QX+wBJd4yRgNT5W6r4DruwmM +ss2nqpdv0cPoWGrjv3IcuGgdliRkGVeIqeTT/g4JqQOLN7ocjFquRkZCdYIfXAOl +5gI/qj/Dc27M/Oql497dmO83nC7+naNOjPIVF+CklAHfyf0W8IEqtD8qQ7rnaMEi +opYfsY7Nf45FD9TSoKBYCN3bMLBiojMd8UkCAwEAAaOBizCBiDAdBgNVHSUEFjAU BggrBgEFBQcDAQYIKwYBBQUHAwIwJwYDVR0RAQH/BB0wG4IJbG9jYWxob3N0gghy -YWJiaXRtcYcEfwAAATAdBgNVHQ4EFgQUf53Mqv9QZmcO5uwUUNZcMQA05cAwHwYD -VR0jBBgwFoAU3yBZkYpHT8r976rIZw37wB4iiS4wDQYJKoZIhvcNAQELBQADggEB -ABXBCl+jy+EeDPLwFlHX/DTJrce3VQMAG+x5WxbuKr68zS8uwJFfqmb4dK01RiSe -QAaISp/vr4KRbbNc5f/TA5dOhc2qXf8dZ0rILWE0u1I+1y9DFuNnymIywbodo6ho -ln7bj2wNl1vZ1A6Tm9fH6MJhavCCM18AHZuz+ml9b8SSVnL3XfPUWuZjYnElSXWj -qTJUF+o/1QC3E+ILj5iiwaAgp8kJJezr5m90RC/DTchYS/CRtz79jYMY8IMdOpN6 -JC92KzpO0jKZ4qWkDi4ZgszPTNcUdnjUc4botJrfZhioA26skUiuacyqfpvnspno -y5DFD+Od2XpBCCwgeYk6IPM= +YWJiaXRtcYcEfwAAATAdBgNVHQ4EFgQUo/Gh8fNHfKZlVVuhKfA2Dvn+u2swHwYD +VR0jBBgwFoAU6//gYr2Xunun2eTaF//n2wM+ERUwDQYJKoZIhvcNAQELBQADggEB +ABtd35pW14Rrxa4SZSo06/CkGqpMnq7+NE2+84OfZyQxqKz8pFpIT6ny3YFfA/IB +S8nquyCdSsdzLbLN15R+iUdoYaKWKUCsntdVYRu68qOzJX/dSf8v6cmTLaYL38TW +g4gkkuXDi2SEId2R7+UtnlRnVqbufqUPxaUxpGnUEpsH9+zLMF5RRmH/l1A2AeqU +eZ7S5TgA9G/WYgpLLzgVSGU2/U6N4wCiIO2mXGnajF7lITLb9Kh7ad8LIR5xhT5S +Yo66IXFR8aWGejqSABLQGrKyiL/puyoGL+qrkNprTNPvF6vtAZQyFTvxo9zE8j0s +cKULlVSVz2IHxFJQAAYz8B0= -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/rabbitmq-config/certificates/key.pem b/deploy/dockerephemeral/rabbitmq-config/certificates/key.pem index 6471c8d1781..d216da8dc97 100644 --- a/deploy/dockerephemeral/rabbitmq-config/certificates/key.pem +++ b/deploy/dockerephemeral/rabbitmq-config/certificates/key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWd2/Jn53/F7lC -ZhUPMTfVfDdT4jHuV/ljKjinbmV6j3lvaxApggd6KXA1TEN4skbXPn12EiYgpZTH -ggsMyFkMPpNT3BWes11rL7RBUEQjfdb3EoWZtL/GJTXGJd8MbJyHGefyAGU4sl9l -P4UwZIDrMyXH3vHATmbCPUM0bi0cxN5JKQPlO/93pD7CMZP4NDbW0QOAYEq3Cpd4 -gCOAjCUyZbHs0Gxei2alu8iDsr2kM09JMieeuNF+2Ak+UKMy9PWXW0GOW+qaYTTX -16mNShD/2o3vlK80Jtkd3vQVBjDOKNeKUDlO0HMVkYz0jy95wylMSN5CPXra5bhS -x/ZUNbk/AgMBAAECggEAFSsQawktrSmlQpYh+FUwSbSEBCUaaTGvQCg8eDGrzSZK -K0agq3ZDnwgdZSIpi91o4fdEp0u+WXFyEO9WpqG5BWP4Th/0WrNZPS8k6Ntl+qhF -idTtPsaTBElP22SQkKrnCoq2evFbTDKsAQ6CqmA5Ut2LPyc6U5e0FTeRMNsfNaC1 -e+60J5yjxYWfZQdU5F+uiycWWiqabOafJfbN0gdLeuIICG+Z8AuWoUjLg2v55itw -X9T3AWZ2+/kdUY8j5FXFoK2MfuzW7Ys+Y1JeLMHrquy2hicSMbJE7vnxNsv1VMPc -IZzlgS+N/Lqre0S0NQAKqTGxe4PcUw+Mp5ZqXHtBwQKBgQDEViEeOAAtfvpK4pFv -drXmv2KacieEtUeEVfgbzMY4tL2q7RfFGxC4iiLklvwhQSGyfRamtut+t+eR4eFx -XKHaZxobwwfW5sMi6Ye/iyuL3YXvtWiaOz6XNImFTeWUPLnrX5qtMuVbx4UGiKa7 -kjg/214A8Zf/qoVJxzAJwp1E6QKBgQDEMOM+dnUlUc8FrllXmlsGYMxwWdQ+vvvw -BdKrm6Q61z3+C5189VwQQ1+ruIcmfVqCm1BKa0J76evgdqHo/pgiAaGEhItVt8cN -3IVnpQu9Fhphgd/iFYxyTOCW2d1Nze30H1oqwpgmZsw2vE/6WrU8e1j279+SUevS -2+rx7i1T5wKBgE6rhFGrdsbEHl5rMoNLOc/f2A6ytwsB6EoqeGQLRVHreiRHJEMi -eSy4jQqzRQu+IVZ3sN/UY8A+yFc3/zGBQIlWzqtZFocRqBcRJAeoKCa++K/4LJXA -L3A+6Ou1LsybGJQrlrrXrfd8ltzrXIPELy3HJH+UTqdvGEFbwu/mP0YhAoGBAINX -Pyp33yDmzbM97y3Idhuk/fhRCtgev0cGfuzHu4BwzF2gpQQctk9k601osYHA9bDu -DShk+hM+nNyeTvJOTsalVN4EZcsyxx2ufdjPEza471xLt/gA+Q8kDE6w94i4zg5a -VuC9eWJr+1bBZsFxrFcbNInMOF4aXcfB1l20V8ANAoGAXZcAv5zU5Cj4ktoe0uqi -7p9zR8mgW2oXU0orgdQ3Ce2Z2qy4yFU5AfHPmn1RuRFsQCxX8RpUqLDHOvpn6gyt -/u9GBqlCqYG4KAbGKGVjodEIXilbIVNEbCIi4kGcRO038fzZJawwhrXg3FuMd6EV -G92A1vtGnTZYkatPK4LRnBk= +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/1qDtRHCkF8cY +aENJd0k04WtDfvHezlcSW8fqztPngbcWvSBp3dF/cgpuaNXbKw0X1T/zbFgLyd0o +nog7iv23b/WzYxVVezPmb8R+95AEa7bNi23YcyKfLr9EXmVJ8d9yW4k96SITtKDj +2gLXOp0YXCzE+EF/sASXeMkYDU+Vuq+A67sJjLLNp6qXb9HD6Fhq479yHLhoHZYk +ZBlXiKnk0/4OCakDize6HIxarkZGQnWCH1wDpeYCP6o/w3NuzPzqpePe3ZjvN5wu +/p2jTozyFRfgpJQB38n9FvCBKrQ/KkO652jBIqKWH7GOzX+ORQ/U0qCgWAjd2zCw +YqIzHfFJAgMBAAECggEAAN392KEwjALe0+07faHIAEdgQIV23x5g0ccurj4H/tD8 +DBG6ttD4hp0ySZhgDuwR6ymFbRuMpIsOx/3ZKtgHm0qG6H7zPncfjZrG1eUPoNLI +FnxIXnDKQ6YxBwKNUA5pFMHViS4sV+o9P2Ivvnfr4Zbf1wVblW/ytqjVhIS8AKIS +fbUS4csP1HN+v/mbFDrz/WMBxAzmrg/3Q4UQOIl9Wqv+Dn+FRQkx0XTKR8dB9XRY +XBVU4fVXFKK5+pm4ChAkQcNjVBJUsmFlWcrF6jP5GhBhL2/0jR4tcyqScsd6nyrb +WIUKlVxHdLU5UUC3iJ92Vw6TCn1UUCWXXTEKGv7PQQKBgQDfyI/j7R+wMSeA6Wdh +KIgzIlNCq9Xa4mvRQGxwLhQgqbTGuIpHmYlohJlPTtLGnrhuKlulKwU9DnK/3hjC +KWpOoZB3ozpDVVip52iQeWD3qcA9gi4C1I34d2EqBB0Sn0jNQaMsey5OktlSmWjA +jPIuE5/Sar7DMfpL8j/QwpRrQQKBgQDbdL79E8tvA3yJ7paSg6Wlqp+kvJn36rz+ +mrohJ58JiHGql7kccuOiALY6mrWBhGR9jXgeA809BgnHGCNJZgQnceXcBgy+3VdU +BWJDwNu86avpOWSDJcOMiZYO7TPD5imsB/GMqt/eKxY14WSZp4mfYd8/SEimgl+l +9HobrrcsCQKBgF/zWt7bmS6upMV9Tjo2as+h9BkuHG/RjXEXMmeXGkI3AbADCCdT +CbuqvyFmJrHK9EEoIEtdes2HGGR0EiGOKGq2k374mc6tFWskMY1gvdbzDd5RpvDH +umfCqAKf6OdHLKv1bMVxu5UtGcl0xZlp1Z00BN7vrgN7tlpB8GLOS10BAoGBAI0t ++OQ0xM4BLzBJBiLxgDIopxVCo8ajA7zDa3SC2cQ3O/CkNNkBbEG/NXxUJOpScpd2 +2Exu460bYlTryV+hupBprJc0aSSsnk6WPBYcTwCkTwz1+ByKwdd6d8fYf1HKkwpx +/coh720syNgWzTIwXs/jFczPQrWj78aClb+TUZGJAoGBAJavH8rWYzo9ivw/2WT8 +3dG4PuB4iGOYLgkQ6q8616XmRThf9CXmz5yfNxLcgLUmIeypeyzMEgJkBcdFDQy1 +WKRFa+FXqKf23nedrW3ojYwM9C14iYVgVjLd53xB++72EHs/s22Rd1h7ZQX+cWrV +Sc0kUf+KOfAdqYnCJYBKea0b -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/run.sh b/deploy/dockerephemeral/run.sh index 8d9a98cc8be..bbb6f012079 100755 --- a/deploy/dockerephemeral/run.sh +++ b/deploy/dockerephemeral/run.sh @@ -1,17 +1,37 @@ #!/usr/bin/env bash -set -xe +# To start the federation v0, v1 backends, set ENABLE_FEDERATION_V0=1, ENABLE_FEDERATION_V1=1 +# in the env where this script is run + +set -e # run.sh should work no matter what is the current directory -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DOCKER_FILE="$SCRIPT_DIR/docker-compose.yaml" +FED_VERSIONS=(0 1) + +opts=("--file" "$DOCKER_FILE") +for v in "${FED_VERSIONS[@]}"; do + var="ENABLE_FEDERATION_V$v" + if [[ "${!var}" == 1 ]]; then + opts+=("--file" "$SCRIPT_DIR/federation-v$v.yaml") + fi +done + +dc() { + docker-compose "${opts[@]}" "$@" +} -cleanup () { - docker-compose --file "$DOCKER_FILE" --file "$SCRIPT_DIR/federation-v0.yaml" down +cleanup() { + dc down } -docker-compose --file "$DOCKER_FILE" --file "$SCRIPT_DIR/federation-v0.yaml" up -d -trap cleanup EXIT -echo "All Services started successfully, press Ctrl+C to stop them" -# Wait for something to kill this -while true; do sleep 100000000; done +if [ -z "$1" ]; then + dc up -d + trap cleanup EXIT + echo "All Services started successfully, press Ctrl+C to stop them" + # Wait for something to kill this + sleep infinity +else + dc "$@" +fi diff --git a/docs/src/developer/developer/FedCalls.png b/docs/src/developer/developer/FedCalls.png deleted file mode 100644 index 50070640b77..00000000000 Binary files a/docs/src/developer/developer/FedCalls.png and /dev/null differ diff --git a/docs/src/developer/developer/building.md b/docs/src/developer/developer/building.md index 2096fe9d93f..223026e8ca8 100644 --- a/docs/src/developer/developer/building.md +++ b/docs/src/developer/developer/building.md @@ -218,3 +218,26 @@ After all containers are up you can use these Makefile targets to run the tests ``` `TASTY_NUM_THREADS` can also be set to other values, it defaults to number of cores available. + +## How to run the webapp locally against locally running backend + +1. Clone the webapp from: https://github.com/wireapp/wire-webapp +2. Install these depedencies needed for the webapp: + 1. nodejs + 2. yarn + 3. mkcert +3. Copy `.env.localhost` to `.env` and uncomment the local section +4. Run the webapp using: + ```bash + yarn + yarn start + ``` +4. From wire-server repo start the dependencies using: + ```bash + ./deploy/dockerephemeral/run.sh + ``` +5. From wire-server repo start the backend using: + ```bash + make crm + ``` +6. Go to http://localhost:8081 in the browser. diff --git a/docs/src/developer/developer/federation-api-conventions.md b/docs/src/developer/developer/federation-api-conventions.md index f27e148ff73..612a4eb67ed 100644 --- a/docs/src/developer/developer/federation-api-conventions.md +++ b/docs/src/developer/developer/federation-api-conventions.md @@ -31,7 +31,3 @@ this request has authority on, like a conversation got created, or a message is sent, then use the second format like `on-conversation-created` or `on-message-sent` - - A call graph of the API endpoints the can call to federation members is included below. - - ![Federation call graph](FedCalls.png) \ No newline at end of file diff --git a/docs/src/developer/developer/open-telemetry.md b/docs/src/developer/developer/open-telemetry.md new file mode 100644 index 00000000000..62381a3818d --- /dev/null +++ b/docs/src/developer/developer/open-telemetry.md @@ -0,0 +1,47 @@ +# OpenTelemetry Instrumentation + +## Current Status + +The following components have been instrumented: +- brig +- galley +- gundeck +- cannon + +## Known Issues and future work + +- Proper HTTP/2 instrumentation is missing for federator & co - this is related to http/2 outobj in the http2 libraray throwing away all structured information +- Some parts of the service, such as background jobs, may need additional instrumentation. It's currently unclear if these are appearing in the tracing data. +- we need to ingest the data into grafana tempo + + +## Setup instructions for local use + +To view the tracing data: + +1. Start Jaeger using Docker: + ```bash + docker run --rm --name jaeger \ + -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ + -p 6831:6831/udp \ + -p 6832:6832/udp \ + -p 5778:5778 \ + -p 16686:16686 \ + -p 4317:4317 \ + -p 4318:4318 \ + -p 14250:14250 \ + -p 14268:14268 \ + -p 14269:14269 \ + -p 9411:9411 \ + jaegertracing/all-in-one:latest + ``` + +2. Start your services or run integration tests. +3. Open the Jaeger UI at [http://localhost:16686/](http://localhost:16686/) + +## Relevant Resources + +We're using the `hs-opentelemetry-*` family of haskell packages available [here](https://github.com/iand675/hs-opentelemetry). + +- [hs-opentelemetry-instrumentation-wai](https://hackage.haskell.org/package/hs-opentelemetry-instrumentation-wai-0.1.0.0/docs/src/OpenTelemetry.Instrumentation.Wai.html#local-6989586621679045744) +- [hs-opentelemetry-sdk](https://hackage.haskell.org/package/hs-opentelemetry-sdk-0.0.3.6) diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 1c90bdfcc57..74565d0dd21 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -307,6 +307,19 @@ mls: This default configuration can be overriden on a per-team basis through the [feature config API](../developer/features.md) +This flag also supports setting an `initialConfig` value, which is applied when a team is created: + +```yaml +# galley.yaml +mls: + initialConfig: + protocolToggleUsers: [] + defaultProtocol: mls + supportedProtocols: [proteus, mls] # must contain defaultProtocol + allowedCipherSuites: [1] + defaultCipherSuite: 1 +``` + ### MLS End-to-End Identity The MLS end-to-end identity team feature adds an extra level of security and practicability. If turned on, automatic device authentication ensures that team members know they are communicating with people using authenticated devices. Team members get a certificate on all their devices. @@ -332,6 +345,34 @@ mlsE2EId: lockStatus: unlocked ``` +#### Key for DPoP access token signing + +The key for signing DPoP access tokens has to be configured at path `brig.secrets.dpopSigKeyBundle` e.g. as follows: + +```yaml +brig: + secrets: + dpopSigKeyBundle: | + -----BEGIN PRIVATE KEY----- + MIGHAgEAMBMGByqGSM49.... + -----END PRIVATE KEY----- +``` + +The corresponding public key has to be known by the ACME server. + +The key must be an ECDSA P-256 key and can be created with the following `openssl` command: + +```shell +openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:P-256 --out private.pem +``` + +To get the public key run: + +```shell +openssl ec -in private.pem -pubout --out public.pem +``` + + ### Federation Domain Regardless of whether a backend wants to enable federation or not, the operator @@ -423,6 +464,18 @@ federator: clientPrivateKey: client-key.pem ``` +### Federation protocols + +A backend can restrict creation of new federated conversations according to the protocol used (Proteus or MLS). This is controlled by the `federationProtocols` setting. For example: + +```yaml +galley: + settings: + federationProtocols: ["mls"] +``` + +will prevent the creation of a Proteus conversation containing federated users, and will prevent federated users from joining a Proteus conversation on this backend. However, existing Proteus conversations with federated users will continue to function, and users of this backend will be allowed to join new and existing Proteus conversations on federated backends. + ### Outlook calendar integration This feature setting only applies to the Outlook Calendar extension for Wire. As it is an external service, it should only be configured through this feature flag and otherwise ignored by the backend. @@ -666,6 +719,47 @@ optSettings: setOAuthMaxActiveRefreshTokens: 10 ``` +#### Password hashing options + +Since release 5.6.0, wire-server can hash passwords with +[argon2id](https://datatracker.ietf.org/doc/html/rfc9106) to be stored at rest. +If you do not do anything, the deployment will still use scrypt. + +The password hashing options are set for brig and galley: + +```yaml +brig: + optSettings: + setPasswordHashingOptions: + algorithm: # argon2id or scrypt + # These options only apply to argon2id + iterations: ... + memory: ... # memory needed in KiB + parallelism: ... +galley: + settings: + passwordHashingOptions: + algorithm: # argon2id or scrypt + # These options only apply to argon2id + iterations: ... + memory: ... # memory needed in KiB + parallelism: ... +``` + +**Performance implications:** argon2id typically takes longer and uses more +memory than scrypt. So when migrating to it brig and galley pods must be +allocated more resouces according to the chosen paramters. + +When configured to use argon2id, the DB will be migrated slowly over time as the +users enter their passwords (either to login or to do other operations which +require explicit password entry). This migration is **NOT** done in reverse, +i.e., if a deployment started with argon2id as the algorithm then chose to move +to scrypt, the passwords already stored will not get rehashed automatically, +however the users will still be able to use them to login. + +**NOTE** It is highly recommended to move to argon2id as it will be made the + only available choice for the `algorithm` config option in future. + #### Disabling API versions It is possible to disable one ore more API versions. When an API version is disabled it won't be advertised on the `GET /api-version` endpoint, neither in the `supported`, nor in the `development` section. Requests made to any endpoint of a disabled API version will result in the same error response as a request made to an API version that does not exist. diff --git a/docs/src/understand/api-client-perspective/swagger.md b/docs/src/understand/api-client-perspective/swagger.md index b466fc7f8b8..a188dd166dc 100644 --- a/docs/src/understand/api-client-perspective/swagger.md +++ b/docs/src/understand/api-client-perspective/swagger.md @@ -1,37 +1,27 @@ (swagger-api-docs)= -# Swagger API documentation +# Swagger / OpenAPI documentation -Our staging system provides [Swagger / -OpenAPI](https://swagger.io/resources/open-api/) documentation of our HTTP REST -API. +Our staging system provides [OpenAPI +3.0](https://swagger.io/resources/open-api/) documentation of our HTTP +REST API under the following URL: -The swagger docs are correct by construction (compiled from the server -code), and they are complete up to bots/services and event notification -payloads (as of 2023-01-16). +[https://staging-nginz-https.zinfra.io/api/swagger-ui](https://staging-nginz-https.zinfra.io/api/swagger-ui) -There are several ways to interpret this kind of documentation: +There are several ways to interpret this documentation: - Read it as a reference - Generate client code from it - Interactively explore the API by making requests -## Swagger docs (Swagger 2.0) +To find the source code of end-points mentioned in the API, a *route +internal ID* (field `operationId` in openapi) is provided for every +end-point. See {ref}`named-and-internal-route-ids` for details and +usage. -The [Swagger / OpenAPI 2.0](https://swagger.io/specification/v2/) -documentation for endpoints depends on the API version. For a list of -all swagger docs for all supported API versions, [visit -https://staging-nginz-https.zinfra.io/api/swagger-ui](https://staging-nginz-https.zinfra.io/api/swagger-ui). +If you find anything you don't like or understand, please let us know! -To learn which versions are supported, look at -`https:///api-version`. ([See -also.](../../developer/developer/api-versioning.md)) - -If you want to get the raw json for the swagger (ie., for compiling it -into client code in typescript, kotlin, swift, ...), replace -`swagger-ui` with `swagger.json` in the above URL pattern. - -#### Example: doing it manually +## Example To get the versions a backend (`staging-nginz-https.zinfra.io` in this case) supports, execute: @@ -43,55 +33,3 @@ curl https:///api-version The URL to open in your browser for the development version `4` is `https:///v4/api/swagger-ui/`. - -### On-prem and test instances, versioning - -The above is valid for the official wire.com staging environment and -includes both all released API versions and the current development -version, which changes continuously until released. - -If you talk to any other backend, the development version may differ. -Try to ask the backend you're talking if it exposes its docs itself: - -``` -curl https://nginz-https..example.com//api/swagger-ui/ -curl https://nginz-https..example.com//api/swagger.json -``` - -### Internal endpoints - -Swagger docs for internal endpoints are served per service. I.e. there's one for -`brig`, one for `cannon`, etc.. This is because Swagger doesn't play well with -multiple actions having the same combination of HTTP method and URL path. - -Internal APIs are not under version control. - -- Unversioned: - - [`brig` - **internal** (private) - endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig) - - [`cannon` - **internal** (private) - endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/cannon) - - [`cargohold` - **internal** (private) - endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/cargohold) - - [`galley` - **internal** (private) - endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/galley) - - [`spar` - **internal** (private) - endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/spar) - -The URL pattern is similar to that of public endpoints for latest version: -`https:///api-internal/swagger-ui/`. - -If you want to get the raw json of the swagger: -`https:///api-internal/swagger-ui/-swagger.json`. - -### Federation API - -- Unversioned - - [`brig` - Federation API](https://staging-nginz-https.zinfra.io/api-federation/swagger-ui/brig) - - [`galley` - Federation API](https://staging-nginz-https.zinfra.io/api-federation/swagger-ui/galley) - - [`cargohold` - Federation API](https://staging-nginz-https.zinfra.io/api-federation/swagger-ui/cargohold) - -### Finding the source code for an end-point - -A *route internal ID* is provided for every end-point. See -{ref}`named-and-internal-route-ids` for details and usage. diff --git a/docs/src/understand/configure-federation.md b/docs/src/understand/configure-federation.md index 455aafd6437..d97e3182bbc 100644 --- a/docs/src/understand/configure-federation.md +++ b/docs/src/understand/configure-federation.md @@ -371,7 +371,7 @@ certificate. Read {ref}`choose-backend-domain` again, then set the backend domain three times to the same value in the subcharts cargohold, galley and brig. You also need to set `enableFederation` to -`true`. +`true` in background-worker in addition to those charts. ``` yaml # override values for wire-server @@ -393,6 +393,10 @@ cargohold: enableFederation: true settings: federationDomain: example.com # your chosen "backend domain" + +background-worker: + config: + enableFederation: true ``` (configure-federation-strategy-in-brig)= diff --git a/docs/src/understand/federation/fedcalls.md b/docs/src/understand/federation/fedcalls.md deleted file mode 100644 index 80fdb3c03e3..00000000000 --- a/docs/src/understand/federation/fedcalls.md +++ /dev/null @@ -1,18 +0,0 @@ -# Federated API calls by client API end-point (generated) - -**Updated manually using using [the fedcalls tool](https://github.com/wireapp/wire-server/blob/8760b4978ccb039b229d458b7a08136a05e12ff9/tools/fedcalls/README.md); last change: 2023-01-16.** - -This is most likely only interesting for backend developers. - -This graph and csv file describe which public (client) API end-points trigger calls to which end-points at backends federating with the one that is called. The data is correct by construction (see [the fedcalls tool](https://github.com/wireapp/wire-server/blob/8760b4978ccb039b229d458b7a08136a05e12ff9/tools/fedcalls/README.md) for more details). - -The target can only be understood in the context of the [backend code base](https://github.com/wireapp/wire-server/). It is described by component (sub-directory in `/services`) and end-point name (use grep to find it). - -links: - -- [dot](img/wire-fedcalls.dot) -- [png](img/wire-fedcalls.png) -- [csv](img/wire-fedcalls.csv) - -```{image} img/wire-fedcalls.png -``` diff --git a/docs/src/understand/federation/img/wire-fedcalls.csv b/docs/src/understand/federation/img/wire-fedcalls.csv deleted file mode 100644 index bfc571a6d85..00000000000 --- a/docs/src/understand/federation/img/wire-fedcalls.csv +++ /dev/null @@ -1,122 +0,0 @@ -source method,source path,target component,target name -get,/users/{uid_domain}/{uid},brig,get-users-by-ids -post,/list-users,brig,get-users-by-ids -put,/self,brig,on-user-deleted-connections -delete,/self,brig,on-user-deleted-connections -delete,/self/phone,brig,on-user-deleted-connections -delete,/self/email,brig,on-user-deleted-connections -put,/self/locale,brig,on-user-deleted-connections -put,/self/handle,brig,on-user-deleted-connections -post,/register,brig,on-user-deleted-connections -post,/delete,brig,on-user-deleted-connections -get,/activate,brig,on-user-deleted-connections -post,/activate,brig,on-user-deleted-connections -get,/users/{uid_domain}/{uid}/clients,brig,get-user-clients -get,/users/{uid_domain}/{uid}/clients/{client},brig,get-user-clients -post,/users/list-clients,brig,get-user-clients -get,/users/{uid_domain}/{uid}/prekeys/{client},brig,claim-prekey -get,/users/{uid_domain}/{uid}/prekeys,brig,claim-prekey-bundle -post,/users/list-prekeys,brig,claim-multi-prekey-bundle -post,/clients,brig,on-user-deleted-connections -put,/connections/{uid_domain}/{uid},brig,send-connection-action -post,/connections/{uid_domain}/{uid},brig,send-connection-action -get,/search/contacts,brig,get-users-by-ids -get,/search/contacts,brig,search-users -post,/mls/key-packages/claim/{user_domain}/{user},brig,claim-key-packages -post,/access,brig,on-user-deleted-connections -post,/login,brig,on-user-deleted-connections -get,/assets/{key_domain}/{key},cargohold,get-asset -get,/assets/{key_domain}/{key},cargohold,stream-asset -put,/conversations/{cnv},galley,on-conversation-updated -put,/conversations/{cnv},galley,on-mls-message-sent -put,/conversations/{cnv},galley,on-new-remote-conversation -get,/conversations/{cnv_domain}/{cnv},galley,get-conversations -get,/conversations/{cnv_domain}/{cnv}/groupinfo,galley,query-group-info -post,/conversations/list,galley,get-conversations -post,/conversations/join,galley,on-conversation-updated -post,/conversations/join,galley,on-new-remote-conversation -post,/conversations,galley,on-conversation-created -post,/conversations/one2one,galley,on-conversation-created -post,/conversations/{cnv_domain}/{cnv}/members,galley,on-conversation-updated -post,/conversations/{cnv_domain}/{cnv}/members,galley,on-mls-message-sent -post,/conversations/{cnv_domain}/{cnv}/members,galley,on-new-remote-conversation -post,/conversations/{cnv}/join,galley,on-conversation-updated -post,/conversations/{cnv}/join,galley,on-new-remote-conversation -post,/conversations/{cnv_domain}/{cnv}/typing,galley,on-typing-indicator-updated -put,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,on-conversation-updated -put,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,on-mls-message-sent -put,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,on-new-remote-conversation -delete,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,leave-conversation -delete,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,on-conversation-updated -delete,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,on-mls-message-sent -delete,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,on-new-remote-conversation -put,/conversations/{cnv}/members/{usr},galley,on-conversation-updated -put,/conversations/{cnv}/members/{usr},galley,on-mls-message-sent -put,/conversations/{cnv}/members/{usr},galley,on-new-remote-conversation -put,/conversations/{cnv}/name,galley,on-conversation-updated -put,/conversations/{cnv}/name,galley,on-mls-message-sent -put,/conversations/{cnv}/name,galley,on-new-remote-conversation -put,/conversations/{cnv_domain}/{cnv}/name,galley,on-conversation-updated -put,/conversations/{cnv_domain}/{cnv}/name,galley,on-mls-message-sent -put,/conversations/{cnv_domain}/{cnv}/name,galley,on-new-remote-conversation -put,/conversations/{cnv}/message-timer,galley,on-conversation-updated -put,/conversations/{cnv}/message-timer,galley,on-mls-message-sent -put,/conversations/{cnv}/message-timer,galley,on-new-remote-conversation -put,/conversations/{cnv_domain}/{cnv}/message-timer,galley,on-conversation-updated -put,/conversations/{cnv_domain}/{cnv}/message-timer,galley,on-mls-message-sent -put,/conversations/{cnv_domain}/{cnv}/message-timer,galley,on-new-remote-conversation -put,/conversations/{cnv}/receipt-mode,galley,on-conversation-updated -put,/conversations/{cnv}/receipt-mode,galley,on-mls-message-sent -put,/conversations/{cnv}/receipt-mode,galley,on-new-remote-conversation -put,/conversations/{cnv}/receipt-mode,galley,update-conversation -put,/conversations/{cnv_domain}/{cnv}/receipt-mode,galley,on-conversation-updated -put,/conversations/{cnv_domain}/{cnv}/receipt-mode,galley,on-mls-message-sent -put,/conversations/{cnv_domain}/{cnv}/receipt-mode,galley,on-new-remote-conversation -put,/conversations/{cnv_domain}/{cnv}/receipt-mode,galley,update-conversation -put,/conversations/{cnv_domain}/{cnv}/access,galley,on-conversation-updated -put,/conversations/{cnv_domain}/{cnv}/access,galley,on-mls-message-sent -put,/conversations/{cnv_domain}/{cnv}/access,galley,on-new-remote-conversation -delete,/teams/{tid}/conversations/{cid},galley,on-conversation-updated -delete,/teams/{tid}/conversations/{cid},galley,on-mls-message-sent -delete,/teams/{tid}/conversations/{cid},galley,on-new-remote-conversation -post,/conversations/{cnv}/otr/messages,galley,on-message-sent -post,/conversations/{cnv}/otr/messages,brig,get-user-clients -post,/conversations/{cnv_domain}/{cnv}/proteus/messages,brig,get-user-clients -post,/conversations/{cnv_domain}/{cnv}/proteus/messages,galley,on-message-sent -post,/conversations/{cnv_domain}/{cnv}/proteus/messages,galley,send-message -post,/bot/messages,galley,on-message-sent -post,/bot/messages,brig,get-user-clients -put,/teams/{tid}/features/legalhold,galley,on-conversation-updated -put,/teams/{tid}/features/legalhold,galley,on-mls-message-sent -put,/teams/{tid}/features/legalhold,galley,on-new-remote-conversation -post,/mls/welcome,galley,mls-welcome -post,/mls/messages,galley,on-mls-message-sent -post,/mls/messages,galley,send-mls-message -post,/mls/messages,galley,on-conversation-updated -post,/mls/messages,galley,on-new-remote-conversation -post,/mls/messages,brig,get-mls-clients -post,/mls/commit-bundles,galley,on-mls-message-sent -post,/mls/commit-bundles,galley,mls-welcome -post,/mls/commit-bundles,galley,send-mls-commit-bundle -post,/mls/commit-bundles,galley,on-conversation-updated -post,/mls/commit-bundles,galley,on-new-remote-conversation -post,/mls/commit-bundles,brig,get-mls-clients -delete,/teams/{tid}/legalhold/settings,galley,on-conversation-updated -delete,/teams/{tid}/legalhold/settings,galley,on-mls-message-sent -delete,/teams/{tid}/legalhold/settings,galley,on-new-remote-conversation -post,/teams/{tid}/legalhold/{uid},galley,on-conversation-updated -post,/teams/{tid}/legalhold/{uid},galley,on-mls-message-sent -post,/teams/{tid}/legalhold/{uid},galley,on-new-remote-conversation -delete,/teams/{tid}/legalhold/{uid},galley,on-conversation-updated -delete,/teams/{tid}/legalhold/{uid},galley,on-mls-message-sent -delete,/teams/{tid}/legalhold/{uid},galley,on-new-remote-conversation -post,/teams/{tid}/legalhold/consent,galley,on-conversation-updated -post,/teams/{tid}/legalhold/consent,galley,on-mls-message-sent -post,/teams/{tid}/legalhold/consent,galley,on-new-remote-conversation -put,/teams/{tid}/legalhold/{uid}/approve,galley,on-conversation-updated -put,/teams/{tid}/legalhold/{uid}/approve,galley,on-mls-message-sent -put,/teams/{tid}/legalhold/{uid}/approve,galley,on-new-remote-conversation -post,/i/users,brig,on-user-deleted-connections -post,/i/users/spar,brig,on-user-deleted-connections -post,/i/legalhold-login,brig,on-user-deleted-connections -post,/i/sso-login,brig,on-user-deleted-connections \ No newline at end of file diff --git a/docs/src/understand/federation/img/wire-fedcalls.dot b/docs/src/understand/federation/img/wire-fedcalls.dot deleted file mode 100644 index 77648a9d950..00000000000 --- a/docs/src/understand/federation/img/wire-fedcalls.dot +++ /dev/null @@ -1,219 +0,0 @@ -strict digraph { - graph [rankdir=LR] - node [shape=rectangle] - edge [style=dashed] - subgraph { - "37: delete /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w - "3: delete /self":w - "5: delete /self/email":w - "4: delete /self/phone":w - "46: delete /teams/{tid}/conversations/{cid}":w - "54: delete /teams/{tid}/legalhold/settings":w - "56: delete /teams/{tid}/legalhold/{uid}":w - "10: get /activate":w - "25: get /assets/{key_domain}/{key}":w - "27: get /conversations/{cnv_domain}/{cnv}":w - "28: get /conversations/{cnv_domain}/{cnv}/groupinfo":w - "21: get /search/contacts":w - "0: get /users/{uid_domain}/{uid}":w - "12: get /users/{uid_domain}/{uid}/clients":w - "13: get /users/{uid_domain}/{uid}/clients/{client}":w - "16: get /users/{uid_domain}/{uid}/prekeys":w - "15: get /users/{uid_domain}/{uid}/prekeys/{client}":w - "23: post /access":w - "11: post /activate":w - "49: post /bot/messages":w - "18: post /clients":w - "20: post /connections/{uid_domain}/{uid}":w - "31: post /conversations":w - "30: post /conversations/join":w - "29: post /conversations/list":w - "32: post /conversations/one2one":w - "33: post /conversations/{cnv_domain}/{cnv}/members":w - "48: post /conversations/{cnv_domain}/{cnv}/proteus/messages":w - "35: post /conversations/{cnv_domain}/{cnv}/typing":w - "34: post /conversations/{cnv}/join":w - "47: post /conversations/{cnv}/otr/messages":w - "9: post /delete":w - "61: post /i/legalhold-login":w - "62: post /i/sso-login":w - "59: post /i/users":w - "60: post /i/users/spar":w - "1: post /list-users":w - "24: post /login":w - "53: post /mls/commit-bundles":w - "22: post /mls/key-packages/claim/{user_domain}/{user}":w - "52: post /mls/messages":w - "51: post /mls/welcome":w - "8: post /register":w - "57: post /teams/{tid}/legalhold/consent":w - "55: post /teams/{tid}/legalhold/{uid}":w - "14: post /users/list-clients":w - "17: post /users/list-prekeys":w - "19: put /connections/{uid_domain}/{uid}":w - "45: put /conversations/{cnv_domain}/{cnv}/access":w - "36: put /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w - "42: put /conversations/{cnv_domain}/{cnv}/message-timer":w - "40: put /conversations/{cnv_domain}/{cnv}/name":w - "44: put /conversations/{cnv_domain}/{cnv}/receipt-mode":w - "26: put /conversations/{cnv}":w - "38: put /conversations/{cnv}/members/{usr}":w - "41: put /conversations/{cnv}/message-timer":w - "39: put /conversations/{cnv}/name":w - "43: put /conversations/{cnv}/receipt-mode":w - "2: put /self":w - "7: put /self/handle":w - "6: put /self/locale":w - "50: put /teams/{tid}/features/legalhold":w - "58: put /teams/{tid}/legalhold/{uid}/approve":w - } - subgraph { - "71: [brig]:claim-key-packages":e - "68: [brig]:claim-multi-prekey-bundle":e - "66: [brig]:claim-prekey":e - "67: [brig]:claim-prekey-bundle":e - "87: [brig]:get-mls-clients":e - "65: [brig]:get-user-clients":e - "63: [brig]:get-users-by-ids":e - "64: [brig]:on-user-deleted-connections":e - "70: [brig]:search-users":e - "69: [brig]:send-connection-action":e - "72: [cargohold]:get-asset":e - "73: [cargohold]:stream-asset":e - "77: [galley]:get-conversations":e - "81: [galley]:leave-conversation":e - "85: [galley]:mls-welcome":e - "79: [galley]:on-conversation-created":e - "74: [galley]:on-conversation-updated":e - "83: [galley]:on-message-sent":e - "75: [galley]:on-mls-message-sent":e - "76: [galley]:on-new-remote-conversation":e - "80: [galley]:on-typing-indicator-updated":e - "78: [galley]:query-group-info":e - "84: [galley]:send-message":e - "88: [galley]:send-mls-commit-bundle":e - "86: [galley]:send-mls-message":e - "82: [galley]:update-conversation":e - } - "0: get /users/{uid_domain}/{uid}":w -> "63: [brig]:get-users-by-ids":e - "1: post /list-users":w -> "63: [brig]:get-users-by-ids":e - "2: put /self":w -> "64: [brig]:on-user-deleted-connections":e - "3: delete /self":w -> "64: [brig]:on-user-deleted-connections":e - "4: delete /self/phone":w -> "64: [brig]:on-user-deleted-connections":e - "5: delete /self/email":w -> "64: [brig]:on-user-deleted-connections":e - "6: put /self/locale":w -> "64: [brig]:on-user-deleted-connections":e - "7: put /self/handle":w -> "64: [brig]:on-user-deleted-connections":e - "8: post /register":w -> "64: [brig]:on-user-deleted-connections":e - "9: post /delete":w -> "64: [brig]:on-user-deleted-connections":e - "10: get /activate":w -> "64: [brig]:on-user-deleted-connections":e - "11: post /activate":w -> "64: [brig]:on-user-deleted-connections":e - "12: get /users/{uid_domain}/{uid}/clients":w -> "65: [brig]:get-user-clients":e - "13: get /users/{uid_domain}/{uid}/clients/{client}":w -> "65: [brig]:get-user-clients":e - "14: post /users/list-clients":w -> "65: [brig]:get-user-clients":e - "15: get /users/{uid_domain}/{uid}/prekeys/{client}":w -> "66: [brig]:claim-prekey":e - "16: get /users/{uid_domain}/{uid}/prekeys":w -> "67: [brig]:claim-prekey-bundle":e - "17: post /users/list-prekeys":w -> "68: [brig]:claim-multi-prekey-bundle":e - "18: post /clients":w -> "64: [brig]:on-user-deleted-connections":e - "19: put /connections/{uid_domain}/{uid}":w -> "69: [brig]:send-connection-action":e - "20: post /connections/{uid_domain}/{uid}":w -> "69: [brig]:send-connection-action":e - "21: get /search/contacts":w -> "63: [brig]:get-users-by-ids":e - "21: get /search/contacts":w -> "70: [brig]:search-users":e - "22: post /mls/key-packages/claim/{user_domain}/{user}":w -> "71: [brig]:claim-key-packages":e - "23: post /access":w -> "64: [brig]:on-user-deleted-connections":e - "24: post /login":w -> "64: [brig]:on-user-deleted-connections":e - "25: get /assets/{key_domain}/{key}":w -> "72: [cargohold]:get-asset":e - "25: get /assets/{key_domain}/{key}":w -> "73: [cargohold]:stream-asset":e - "26: put /conversations/{cnv}":w -> "74: [galley]:on-conversation-updated":e - "26: put /conversations/{cnv}":w -> "75: [galley]:on-mls-message-sent":e - "26: put /conversations/{cnv}":w -> "76: [galley]:on-new-remote-conversation":e - "27: get /conversations/{cnv_domain}/{cnv}":w -> "77: [galley]:get-conversations":e - "28: get /conversations/{cnv_domain}/{cnv}/groupinfo":w -> "78: [galley]:query-group-info":e - "29: post /conversations/list":w -> "77: [galley]:get-conversations":e - "30: post /conversations/join":w -> "74: [galley]:on-conversation-updated":e - "30: post /conversations/join":w -> "76: [galley]:on-new-remote-conversation":e - "31: post /conversations":w -> "79: [galley]:on-conversation-created":e - "32: post /conversations/one2one":w -> "79: [galley]:on-conversation-created":e - "33: post /conversations/{cnv_domain}/{cnv}/members":w -> "74: [galley]:on-conversation-updated":e - "33: post /conversations/{cnv_domain}/{cnv}/members":w -> "75: [galley]:on-mls-message-sent":e - "33: post /conversations/{cnv_domain}/{cnv}/members":w -> "76: [galley]:on-new-remote-conversation":e - "34: post /conversations/{cnv}/join":w -> "74: [galley]:on-conversation-updated":e - "34: post /conversations/{cnv}/join":w -> "76: [galley]:on-new-remote-conversation":e - "35: post /conversations/{cnv_domain}/{cnv}/typing":w -> "80: [galley]:on-typing-indicator-updated":e - "36: put /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "74: [galley]:on-conversation-updated":e - "36: put /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "75: [galley]:on-mls-message-sent":e - "36: put /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "76: [galley]:on-new-remote-conversation":e - "37: delete /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "81: [galley]:leave-conversation":e - "37: delete /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "74: [galley]:on-conversation-updated":e - "37: delete /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "75: [galley]:on-mls-message-sent":e - "37: delete /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "76: [galley]:on-new-remote-conversation":e - "38: put /conversations/{cnv}/members/{usr}":w -> "74: [galley]:on-conversation-updated":e - "38: put /conversations/{cnv}/members/{usr}":w -> "75: [galley]:on-mls-message-sent":e - "38: put /conversations/{cnv}/members/{usr}":w -> "76: [galley]:on-new-remote-conversation":e - "39: put /conversations/{cnv}/name":w -> "74: [galley]:on-conversation-updated":e - "39: put /conversations/{cnv}/name":w -> "75: [galley]:on-mls-message-sent":e - "39: put /conversations/{cnv}/name":w -> "76: [galley]:on-new-remote-conversation":e - "40: put /conversations/{cnv_domain}/{cnv}/name":w -> "74: [galley]:on-conversation-updated":e - "40: put /conversations/{cnv_domain}/{cnv}/name":w -> "75: [galley]:on-mls-message-sent":e - "40: put /conversations/{cnv_domain}/{cnv}/name":w -> "76: [galley]:on-new-remote-conversation":e - "41: put /conversations/{cnv}/message-timer":w -> "74: [galley]:on-conversation-updated":e - "41: put /conversations/{cnv}/message-timer":w -> "75: [galley]:on-mls-message-sent":e - "41: put /conversations/{cnv}/message-timer":w -> "76: [galley]:on-new-remote-conversation":e - "42: put /conversations/{cnv_domain}/{cnv}/message-timer":w -> "74: [galley]:on-conversation-updated":e - "42: put /conversations/{cnv_domain}/{cnv}/message-timer":w -> "75: [galley]:on-mls-message-sent":e - "42: put /conversations/{cnv_domain}/{cnv}/message-timer":w -> "76: [galley]:on-new-remote-conversation":e - "43: put /conversations/{cnv}/receipt-mode":w -> "74: [galley]:on-conversation-updated":e - "43: put /conversations/{cnv}/receipt-mode":w -> "75: [galley]:on-mls-message-sent":e - "43: put /conversations/{cnv}/receipt-mode":w -> "76: [galley]:on-new-remote-conversation":e - "43: put /conversations/{cnv}/receipt-mode":w -> "82: [galley]:update-conversation":e - "44: put /conversations/{cnv_domain}/{cnv}/receipt-mode":w -> "74: [galley]:on-conversation-updated":e - "44: put /conversations/{cnv_domain}/{cnv}/receipt-mode":w -> "75: [galley]:on-mls-message-sent":e - "44: put /conversations/{cnv_domain}/{cnv}/receipt-mode":w -> "76: [galley]:on-new-remote-conversation":e - "44: put /conversations/{cnv_domain}/{cnv}/receipt-mode":w -> "82: [galley]:update-conversation":e - "45: put /conversations/{cnv_domain}/{cnv}/access":w -> "74: [galley]:on-conversation-updated":e - "45: put /conversations/{cnv_domain}/{cnv}/access":w -> "75: [galley]:on-mls-message-sent":e - "45: put /conversations/{cnv_domain}/{cnv}/access":w -> "76: [galley]:on-new-remote-conversation":e - "46: delete /teams/{tid}/conversations/{cid}":w -> "74: [galley]:on-conversation-updated":e - "46: delete /teams/{tid}/conversations/{cid}":w -> "75: [galley]:on-mls-message-sent":e - "46: delete /teams/{tid}/conversations/{cid}":w -> "76: [galley]:on-new-remote-conversation":e - "47: post /conversations/{cnv}/otr/messages":w -> "83: [galley]:on-message-sent":e - "47: post /conversations/{cnv}/otr/messages":w -> "65: [brig]:get-user-clients":e - "48: post /conversations/{cnv_domain}/{cnv}/proteus/messages":w -> "65: [brig]:get-user-clients":e - "48: post /conversations/{cnv_domain}/{cnv}/proteus/messages":w -> "83: [galley]:on-message-sent":e - "48: post /conversations/{cnv_domain}/{cnv}/proteus/messages":w -> "84: [galley]:send-message":e - "49: post /bot/messages":w -> "83: [galley]:on-message-sent":e - "49: post /bot/messages":w -> "65: [brig]:get-user-clients":e - "50: put /teams/{tid}/features/legalhold":w -> "74: [galley]:on-conversation-updated":e - "50: put /teams/{tid}/features/legalhold":w -> "75: [galley]:on-mls-message-sent":e - "50: put /teams/{tid}/features/legalhold":w -> "76: [galley]:on-new-remote-conversation":e - "51: post /mls/welcome":w -> "85: [galley]:mls-welcome":e - "52: post /mls/messages":w -> "75: [galley]:on-mls-message-sent":e - "52: post /mls/messages":w -> "86: [galley]:send-mls-message":e - "52: post /mls/messages":w -> "74: [galley]:on-conversation-updated":e - "52: post /mls/messages":w -> "76: [galley]:on-new-remote-conversation":e - "52: post /mls/messages":w -> "87: [brig]:get-mls-clients":e - "53: post /mls/commit-bundles":w -> "75: [galley]:on-mls-message-sent":e - "53: post /mls/commit-bundles":w -> "85: [galley]:mls-welcome":e - "53: post /mls/commit-bundles":w -> "88: [galley]:send-mls-commit-bundle":e - "53: post /mls/commit-bundles":w -> "74: [galley]:on-conversation-updated":e - "53: post /mls/commit-bundles":w -> "76: [galley]:on-new-remote-conversation":e - "53: post /mls/commit-bundles":w -> "87: [brig]:get-mls-clients":e - "54: delete /teams/{tid}/legalhold/settings":w -> "74: [galley]:on-conversation-updated":e - "54: delete /teams/{tid}/legalhold/settings":w -> "75: [galley]:on-mls-message-sent":e - "54: delete /teams/{tid}/legalhold/settings":w -> "76: [galley]:on-new-remote-conversation":e - "55: post /teams/{tid}/legalhold/{uid}":w -> "74: [galley]:on-conversation-updated":e - "55: post /teams/{tid}/legalhold/{uid}":w -> "75: [galley]:on-mls-message-sent":e - "55: post /teams/{tid}/legalhold/{uid}":w -> "76: [galley]:on-new-remote-conversation":e - "56: delete /teams/{tid}/legalhold/{uid}":w -> "74: [galley]:on-conversation-updated":e - "56: delete /teams/{tid}/legalhold/{uid}":w -> "75: [galley]:on-mls-message-sent":e - "56: delete /teams/{tid}/legalhold/{uid}":w -> "76: [galley]:on-new-remote-conversation":e - "57: post /teams/{tid}/legalhold/consent":w -> "74: [galley]:on-conversation-updated":e - "57: post /teams/{tid}/legalhold/consent":w -> "75: [galley]:on-mls-message-sent":e - "57: post /teams/{tid}/legalhold/consent":w -> "76: [galley]:on-new-remote-conversation":e - "58: put /teams/{tid}/legalhold/{uid}/approve":w -> "74: [galley]:on-conversation-updated":e - "58: put /teams/{tid}/legalhold/{uid}/approve":w -> "75: [galley]:on-mls-message-sent":e - "58: put /teams/{tid}/legalhold/{uid}/approve":w -> "76: [galley]:on-new-remote-conversation":e - "59: post /i/users":w -> "64: [brig]:on-user-deleted-connections":e - "60: post /i/users/spar":w -> "64: [brig]:on-user-deleted-connections":e - "61: post /i/legalhold-login":w -> "64: [brig]:on-user-deleted-connections":e - "62: post /i/sso-login":w -> "64: [brig]:on-user-deleted-connections":e -} \ No newline at end of file diff --git a/docs/src/understand/federation/img/wire-fedcalls.png b/docs/src/understand/federation/img/wire-fedcalls.png deleted file mode 100644 index 35fb0ed9d2a..00000000000 Binary files a/docs/src/understand/federation/img/wire-fedcalls.png and /dev/null differ diff --git a/docs/src/understand/single-sign-on/understand/main.md b/docs/src/understand/single-sign-on/understand/main.md index f413538c138..adabf76b74a 100644 --- a/docs/src/understand/single-sign-on/understand/main.md +++ b/docs/src/understand/single-sign-on/understand/main.md @@ -269,7 +269,7 @@ You need to configure your SCIM client to use the following mandatory SCIM attri 3. The `externalId` attribute: - 1. If you are using Wire's SAML SSO feature then set `externalId` attribute to the same identifier used for `NameID` in your SAML configuration. + 1. If you are using Wire's SAML SSO feature then set `externalId` attribute to the same identifier used for `NameID` in your SAML configuration (suppored `NameID` types are `email` and `unspecified`). 2. If you are using email/password authentication then set the `externalId` attribute to the user's email address. The user will receive an invitation email during provisioning. Also note that the account will be set to `"active": false` until the user has accepted the invitation and activated the account. diff --git a/hack/bin/cabal-run-integration.sh b/hack/bin/cabal-run-integration.sh index 66daccfb538..582b2de874e 100755 --- a/hack/bin/cabal-run-integration.sh +++ b/hack/bin/cabal-run-integration.sh @@ -49,13 +49,15 @@ run_integration_tests() { then cd "$TOP_LEVEL" "$TOP_LEVEL/dist/run-services" \ - "$TOP_LEVEL/dist/integration" \ - "${@:2}" + -- \ + "$TOP_LEVEL/dist/integration" \ + "${@:2}" else service_dir="$TOP_LEVEL/services/$package" cd "$service_dir" "$TOP_LEVEL/dist/run-services" \ + -- \ "$TOP_LEVEL/dist/$package-integration" \ -s "$service_dir/$package.integration.yaml" \ -i "$TOP_LEVEL/services/integration.yaml" \ diff --git a/hack/bin/check-weed.sh b/hack/bin/check-weed.sh new file mode 100755 index 00000000000..a55e4c26ca7 --- /dev/null +++ b/hack/bin/check-weed.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Define ANSI color code for red +RED='\033[0;31m' +NC='\033[0m' # No Color (reset) + +echo "Checking for weed…" +echo "Make sure you have compiled everything with the correct settings." + +output=$(weeder -N) + +# Check if the output is empty +if [[ -z "$output" ]]; then + echo "No weed found! 🚫🪴" +else + echo "We found some weed!" + echo -e "${RED}$output${NC}" +fi diff --git a/hack/bin/gen-certs.sh b/hack/bin/gen-certs.sh index a2a33a26253..462f321e1ba 100755 --- a/hack/bin/gen-certs.sh +++ b/hack/bin/gen-certs.sh @@ -34,7 +34,7 @@ gen_cert() { if [ -n "$3" ]; then subj=(-subj "/CN=$3") 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 -extfile <( echo "extendedKeyUsage = serverAuth, clientAuth"; echo "subjectAltName = critical, $2" ) 2>/dev/null + 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 } # usage: install_certs source_dir target_dir ca ca-key cert key @@ -56,6 +56,8 @@ install_certs "$TEMP/federation" "$ROOT_DIR/services/nginz/integration-test/conf integration-ca integration-ca-key integration-leaf integration-leaf-key install_certs "$TEMP/federation" "$ROOT_DIR/deploy/dockerephemeral/federation-v0" \ integration-ca "" integration-leaf integration-leaf-key +install_certs "$TEMP/federation" "$ROOT_DIR/deploy/dockerephemeral/federation-v1" \ + integration-ca "" integration-leaf integration-leaf-key # elasticsearch mkdir -p "$TEMP/es" diff --git a/hack/bin/integration-teardown-federation.sh b/hack/bin/integration-teardown-federation.sh index 01791d223cd..198cfa850d6 100755 --- a/hack/bin/integration-teardown-federation.sh +++ b/hack/bin/integration-teardown-federation.sh @@ -12,6 +12,8 @@ export NAMESPACE_2="$NAMESPACE-fed2" # these don't matter for destruction but have to be set. export FEDERATION_DOMAIN_1="." export FEDERATION_DOMAIN_2="." +export FEDERATION_DOMAIN_BASE_1="." +export FEDERATION_DOMAIN_BASE_2="." KUBERNETES_VERSION_MAJOR="$(kubectl version -o json | jq -r .serverVersion.major)" KUBERNETES_VERSION_MINOR="$(kubectl version -o json | jq -r .serverVersion.minor)" diff --git a/hack/helm_vars/certs/elasticsearch-ca-key.pem b/hack/helm_vars/certs/elasticsearch-ca-key.pem index 53785fe3292..f59d94e52b6 100644 --- a/hack/helm_vars/certs/elasticsearch-ca-key.pem +++ b/hack/helm_vars/certs/elasticsearch-ca-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/oFJpJMdyG9FT -pNw4K9f1pdkNikwbQsx4dokiQBMTu89IMTnNfsHz2IDrxCKTCKC3oPupniaEPNpr -YpV6RMz1UPvUYu/IpvOXGeIGlVd9ixcoYN6763R2nZhMlFS8Tma9mV+e/B0jr9Db -V1pUWIPufuPrYXcOotxDO/W5I+GpKVTz/ZGD//O5odX1mJzkwqjeqGa1WNdg+/AL -iDtVZ/YAKGdfjx81uqc16fYuYRDw3BYImBp5MyNu/jxdgNxFB1edcVowvcKXVs5p -Slay2ad0eQSa0Ux8n3RjfisjTLAHks/4dkPa3hQyBYzmxwBhMcMDc06yxiCkXsVn -lXRn9nf/AgMBAAECggEABQZr3GzMSImPaRvqPnrZdFkMb30QVw94YMxS9xf3dOc4 -hB8hi4PNPqf1yx9e/Lx9yNleE1BqmCf0XltWdvKPVJUlrw5TiJwZyGOZ+F9tAB81 -CA6j29YZcFoPoJDfOMghjGVIpNjdqfSC8jP0BXQ3LK22xZLOIw8eqypLKYPvkTA3 -OfuJ/1doiHl+geZkXaKcLSpCCddLKCaWSbLyqYMJxbQ5SSZ9bPUeQ7aQppb5M/wO -1B4+oMmRLcmG81QnL0kU9JiAtYaGsrP22qGuEGVjEZE8RXJz3iQ1KvSlj0xerqi7 -/LY0HLixkx4n3Qtpm9FFaT3rzeDlJIE54qmI73sdYQKBgQDw6KJJIxmQScLZb4ml -yjd+pBvPuUe9cM9KMRNk2C7Z1QMxORXsIbgCPqpkJ96XUquta2ii7rxt/sXAkrh1 -c8IYU3Qp03+585J+6lZF6yaH9TrwYfDCRqKoSgAEwhJlvtoWHSMI6YguWsaczdgH -czd+0OzJl1w4vQqQBuXFwz4eEQKBgQDLoVzk+z/1//CJUfAe/Z6WYFHmTh+M9RGP -vC7GCQVCjIFUNsXqrWLl6DL5UeipYVhqu5eB7vOo/gNnb6J1vMOO0j9e1cY1Q2lG -BdSIHUD7P1Lly1/K+pn2QqIIHp+72H5qsX+8R5Tkln00jwQ0t5DrMVgJvWBW7/GC -lach4BZlDwKBgQDjfdraE8ItJepRJ+mk3GtBNLlqk/0x4FhvKB63SQoc+/Dyx4Rz -Ing/7ms6/wdMgG3L6rS5v5XCjSayrhpwFyr/i7cTVDy2HVOGc8Waau9Mzf+lRedz -nf41ywNvetCisfIBlewim1zU4TXSlvNcPan3IFWqHDui/Kj/zvOlp7R98QKBgQCn -fdi89/TKUXT2XpFVzGLvadazyrqk5MdHJRCMD8tly9BtBoiQ2YEpfm6/KKJpAAsL -77VVSMjezeDa6bYFhfiMt18skEXydbpXwF/qfbV/c7yqCziF6s9NAc3pQ9c7WX3S -IKHiqjZMN4RRAPoCqqLm8bVqfXyKxd4u/Q12Da4d/QKBgByVkmAoFVF1iYkb5b/0 -cPRXMnn5Xw5C6CRWpEwl3dSlt/uVACcFyxKsUP7QDTbqN7DSl/RX6DxkDIR6GBEz -vt0yXsdFx8y9Lzw1TNj2zhPVrtyslX/GFmi0R7/oyTEuVOMNy1rl/wftRqRX90md -JLSFJ4QslRlPwYOWGPDjJZVe +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDhfp2hokSJ88qb +6Gl8BBwg4jMbkt4l2ynOa/lO6DZFRheFlWZBaUZAcj1ojBgbd3EoOXygZReL9TGB +Rrc5iACzytlOJkwMgNUUIq1KsPwWII41VJw3h3NO07tMTsf0kFvH1pEllJqorhQ1 +eZnU1SISyQcjk0oRQEyWd6arF7TLHna69OHF2ybYYXMDMFWNr+O6t8RUfYs4kb7z +1Nx3OnUKrUhIyaYeoyvBBOdXA5/G5GenDu/G4iVozsuLgofYWu+77EnpProF0KRK ++XlKakvF7bD26Qm1ol5qsXbdXOXKYq/c4KDdC938HNd/FNPz7EsnW0brBXqtz8Te +QmxHix7DAgMBAAECggEAOEemxiG+44OCbRk7wqUv9BEg2l/0rBQgQhH23nfcm7ub +wU6BgA/rZchdhUt59NkB2B1I+qtgjiD7Yx2oO2azbixRwkySrIg3JlhlUgAMWuVz +OOJOPxnCcMkttSTwiRzCm4T1IyEM3M7d4l7gQxuS7odYDcwEL3wR4XgplAhNqmgP +AxBayjX/POTsIoH2xPhjYsILPnRjDihQdxIWJoqZHUfIvM925tFi7WtzaFvyX8VU +s8t2zcByiqi1q9MLDv+uwhweP7tYGUj1RoQYoMuFjVRrm09nCRmAix2Evok1zXqV +6jPqgDe4+p03+816n8EWKgCTcAiZvFb7YhYkAO1gWQKBgQD7IKkUzvK6qKrN6Fg4 +bX4U6tAvKVGRhiNEfPRWv44OatusI1K14ndtLbZesQ+R2WgjwO1P2bhs6BDeV/oo +Y5T2ETBRcAV154NJxv5ktGw5IaI0IAlYBy/daaTY9wZBZ8+Qy5+FHra0ClHOMzKl +qQ3UeSMNZ+E5mH9ofMfhRFs8lwKBgQDl3qMqaBEDg/2O58IqwjWG9QGEGfoyqEi4 +C1DwQhvPAFa8PJ/mqwFZtYd1WpLV+wMm6WZWhVTpjQIYo7jYX54Vz2/y94AqtRC6 +sqlJgMCDSGjVeVgzIw2L1yaxdl+WzhbtGQUPmKxh9BM1WmJysst2/f7rk9MjyTFg +auhtAkH4tQKBgQCqA92UsdrRFjm093Uqlq5CWQqiszV+8TJVPsdpJ3x0NFIOg0eO +zgiOiOEr0HG7C1YexpGjesIKMT6iWSuKRojl4pM0v0NjJF7VBvzZjvCp6SRYZ8wL +pan5G3m4Td0VUMPMwp530GhfEZF6qVzDnOU5EN3zSH3JsX2obrofv1iJdwKBgDnO +iFfkvcqVidFDRRf9qPpcaNowsjPFECyAZAVXiqi+3BEQaeHXRUqrFPqVIXIAYuWJ +Mnw1oYnuNQW/Pn/jY9z2Qp/mT+vthtx8i4f5gfBB6GMu1dheS0zMeWWNcDJ7d1Z+ +wUAP0+H6QE5dgX54qiQtccsKbMGGGg22NOcc9zw1AoGAGiacZEXrdHsg1piMlHjw +LE96b7mynZfOO4LPm/0Xl5FJ8mElNnZGWlpMFVq+Hi2WnHQ6MRRIZmOS3YTdS4FB +cBvwiGGn18QEgFGOI7JonzPDp1LZWnxy5nmZhmTuI4GiXyYAqGw6QLGiqqQo4p08 +J7OLSRyQ7aiG97iUS1QgJFU= -----END PRIVATE KEY----- diff --git a/hack/helm_vars/certs/elasticsearch-ca.pem b/hack/helm_vars/certs/elasticsearch-ca.pem index f17e9cb41ac..6511f688a73 100644 --- a/hack/helm_vars/certs/elasticsearch-ca.pem +++ b/hack/helm_vars/certs/elasticsearch-ca.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDLzCCAhegAwIBAgIUMGKU64YSPkGrWyHiXiLsuoKC/9owDQYJKoZIhvcNAQEL +MIIDLzCCAhegAwIBAgIUUOLn63PL3FEyGdhOK1ocDAn8dC8wDQYJKoZIhvcNAQEL BQAwJzElMCMGA1UEAwwcZWxhc3RpY3NlYXJjaC5jYS5leGFtcGxlLmNvbTAeFw0y -NDA2MTcxMzE1MzFaFw0zNDA2MTUxMzE1MzFaMCcxJTAjBgNVBAMMHGVsYXN0aWNz +NDA5MDMxMjAzMzhaFw0zNDA5MDExMjAzMzhaMCcxJTAjBgNVBAMMHGVsYXN0aWNz ZWFyY2guY2EuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK -AoIBAQC/oFJpJMdyG9FTpNw4K9f1pdkNikwbQsx4dokiQBMTu89IMTnNfsHz2IDr -xCKTCKC3oPupniaEPNprYpV6RMz1UPvUYu/IpvOXGeIGlVd9ixcoYN6763R2nZhM -lFS8Tma9mV+e/B0jr9DbV1pUWIPufuPrYXcOotxDO/W5I+GpKVTz/ZGD//O5odX1 -mJzkwqjeqGa1WNdg+/ALiDtVZ/YAKGdfjx81uqc16fYuYRDw3BYImBp5MyNu/jxd -gNxFB1edcVowvcKXVs5pSlay2ad0eQSa0Ux8n3RjfisjTLAHks/4dkPa3hQyBYzm -xwBhMcMDc06yxiCkXsVnlXRn9nf/AgMBAAGjUzBRMB0GA1UdDgQWBBSGMhy1Uvrs -lmdHKAGQ9avMSWhz2jAfBgNVHSMEGDAWgBSGMhy1UvrslmdHKAGQ9avMSWhz2jAP -BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA4vndI6NRcMgzba1y3 -lUPxy40bs/jQajR3A5fmCCX4c0ZeRc4YqE9cdYgeGffCZvPogyYjWDlavOma2uAQ -+3lZ35k0wG9GsU2g3fDIXpUuoSUjfYRLBQ3oqD7VRKYs1rDD87c+91DrsfIVZKF1 -W1RzOOvcW9QX2RHghFS4IluX6LEboo48cKtycA/nfmYDT/L9I4oYjaxc9l+HMUSH -gkQUU1xZnQ9GCqdhL3+2dmn0jvdgJLiFuefMGkE0oP/kFD/bhuOmDhpIDb10Cuck -Nw/nOSbBLINx2qDOa1f3Kox/PesQO4tp0dMp6XqZCOPTQ95vHsIOxuX1d+pxhX2V -ToWP +AoIBAQDhfp2hokSJ88qb6Gl8BBwg4jMbkt4l2ynOa/lO6DZFRheFlWZBaUZAcj1o +jBgbd3EoOXygZReL9TGBRrc5iACzytlOJkwMgNUUIq1KsPwWII41VJw3h3NO07tM +Tsf0kFvH1pEllJqorhQ1eZnU1SISyQcjk0oRQEyWd6arF7TLHna69OHF2ybYYXMD +MFWNr+O6t8RUfYs4kb7z1Nx3OnUKrUhIyaYeoyvBBOdXA5/G5GenDu/G4iVozsuL +gofYWu+77EnpProF0KRK+XlKakvF7bD26Qm1ol5qsXbdXOXKYq/c4KDdC938HNd/ +FNPz7EsnW0brBXqtz8TeQmxHix7DAgMBAAGjUzBRMB0GA1UdDgQWBBTWzp/VRTGq +s/IwlrROQGDwavTpyzAfBgNVHSMEGDAWgBTWzp/VRTGqs/IwlrROQGDwavTpyzAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBFue/SGpAVZ0TOVp8l +bdaaY/e9wUBSdCH5b39Nzd3rKmH3lIHqcafLlFCx+scKUIlFbohJr0aTK339wFfl +L0LzBJVUT9JzeDNPhk8pl/jBJk+eGP3fiykFMCgxxGvHtccHu8E/y8U0SeEtKqDn +Xy0ZbC3M54UedhDpHMovfHEsfN24Ev0DK13sBR2T8fmXCyCrfq887cCqJyP2ODgb +xAY/R4F8Ueadn0ywHYSY3MqmDsvDul0QlaOu2J5A0+k5oy4hAfFB8PzPYZrmPkeU +N5oxudTTihIZ+0JiL2JmWGBzMGzgtmD1rHC6lugUlWq+BoPu2+/+hn8RcVHBCFDk +WMSU -----END CERTIFICATE----- diff --git a/hack/helm_vars/wire-server/values-domain1.yaml.gotmpl b/hack/helm_vars/wire-server/values-domain1.yaml.gotmpl new file mode 100644 index 00000000000..65bc78ca64a --- /dev/null +++ b/hack/helm_vars/wire-server/values-domain1.yaml.gotmpl @@ -0,0 +1,30 @@ +galley: + secrets: + mlsPrivateKeys: + removal: + ed25519: | + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIAocCDXsKIAjb65gOUn5vEF0RIKnVJkKR4ebQzuZ709c + -----END PRIVATE KEY----- + ecdsa_secp256r1_sha256: | + -----BEGIN PRIVATE KEY----- + MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg3qjgQ9U+/rTBObn9 + tXSVi2UtHksRDXmQ1VOszFZfjryhRANCAATNkLmZZLyORf5D3PUOxt+rkJTE5vuD + aCqZ7sE5NSN8InRRwuQ1kv0oblDVeQA89ZlHqyxx75JPK+/air7Z1n5I + -----END PRIVATE KEY----- + ecdsa_secp384r1_sha384: | + -----BEGIN PRIVATE KEY----- + MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBLwv3i5LDz9b++O0iw + QAit/Uq7L5PWPgKN99wCm8xkZnuyqWujXW4wvlVUVlZWgh2hZANiAAT0+RXKE31c + VxdYazaVopY50/nV9c18uRdqoENBvtxuD6oDtJtU6oCS/Htkd8JEArTQ9ZHqq144 + yRjuc3d2CqvJmEA/lzIBk9wnz+lghFhvB4TkSHvvLyEBc9DZvhb4EEQ= + -----END PRIVATE KEY----- + ecdsa_secp521r1_sha512: | + -----BEGIN PRIVATE KEY----- + MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBiaEARm5BMaRct1xj + MlemUHijWGAoHtNMhSttSr4jo0WxMwfMnvnDQJSlO2Zs4Tzum2j5eO34EHu6MUrv + qquZYwyhgYkDgYYABAHuvCV/+gJitvAbDwgrBHZJ41oy8Lc+wPIM7Yp6s/vTzTsG + Klo7aMdkx6DUjv/56tVD9bZNulFAjwS8xoIyWg8NSAE1ofo8CBvN1XGZOWuMYjEh + zLrZADduEnOvayw5sEvm135WC0vWjPJaYwKZPdDIXUz9ILJPgNe3gEUvHsDEXvdX + lw== + -----END PRIVATE KEY----- diff --git a/hack/helm_vars/wire-server/values-domain2.yaml.gotmpl b/hack/helm_vars/wire-server/values-domain2.yaml.gotmpl new file mode 100644 index 00000000000..0c317f59726 --- /dev/null +++ b/hack/helm_vars/wire-server/values-domain2.yaml.gotmpl @@ -0,0 +1,30 @@ +galley: + secrets: + mlsPrivateKeys: + removal: + ed25519: | + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEINsoqzdpVTv5+odHwwGO1I+Kp1+T24p7URvq50n79iCJ + -----END PRIVATE KEY----- + ecdsa_secp256r1_sha256: | + -----BEGIN PRIVATE KEY----- + MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgl2RK2WydNOp7ViAB + 5mhF4N0HdmJQ89f4YtxiCE252LehRANCAASJgocuA+eIaebS+M6t6ouQT3LzObg3 + XkWNvPZWE/4wsm6FAZ7ulLKU02AumSUx4u71d/1x9epAHJyc+RdACUQt + -----END PRIVATE KEY----- + ecdsa_secp384r1_sha384: | + -----BEGIN PRIVATE KEY----- + MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDD/lnMI4VPb7VchrKHj + hiE6ntS71rVDXps8UdnHenKQhBccRlqznBV6vP5QbcKVQcqhZANiAAR3yJC8eV/G + tC6ZDk5uMNaiqlmwVzH0mRNiJPGShVHfL3rBFq99sf3nOTs4v79PXajGwFJXppJC + /TQc7PMy2IbTor5tdWjcNBgSZiPn734IACuLpExqvsPuD6MlV2aHmXQ= + -----END PRIVATE KEY----- + ecdsa_secp521r1_sha512: | + -----BEGIN PRIVATE KEY----- + MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAcHs/PncuxyMlECqE + DeUHeqbd3aktlnenf8q0vemi4DSebEEp3ONCG7ZfFQFZyp5aZQWgZaOj1pwGDzth + FmmOFwShgYkDgYYABAF5QeG/mdn2MNuaHzDc+/6UbAfsb+ddighFuxqobl4731w2 + 2myXfvFGcseoKjymDe8kuv1a4eDmLzLkrUGNZhWIfQD7CP0j0JmBNzVnYAKlRcOd + SU5XbMx1q7oyaiQ51B47IObxT8sVKZzbnE3qZa060cAglu4G0OS3OlJzVOinBAfA + 5w== + -----END PRIVATE KEY----- diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 452b3864685..e7f72583f39 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -6,7 +6,7 @@ tags: cannon: true cargohold: true spar: true - federation: true # also see galley.config.enableFederation and brig.config.enableFederation + federation: true # also see {background-worker,brig,cargohold,galley}.config.enableFederation backoffice: true proxy: false legalhold: false @@ -115,6 +115,12 @@ brig: setMaxConvSize: 16 # See helmfile for the real value setFederationDomain: integration.example.com + setFeatureFlags: + conferenceCalling: + defaultForNew: + status: disabled + defaultForNull: + status: disabled setFederationStrategy: allowAll setFederationDomainConfigsUpdateFreq: 10 setDisabledAPIVersions: [] @@ -128,6 +134,13 @@ brig: setOAuthEnabled: true setOAuthRefreshTokenExpirationTimeSecs: 14515200 # 24 weeks setOAuthMaxActiveRefreshTokens: 10 + # These values are insecure, against anyone getting hold of the hash, + # but its not a concern for the integration tests. + setPasswordHashingOptions: + algorithm: argon2id + iterations: 1 + parallelism: 4 + memory: 32 # This needs to be at least 8 * parallelism. aws: sesEndpoint: http://fake-aws-ses:4569 sqsEndpoint: http://fake-aws-sqs:4568 @@ -252,14 +265,31 @@ galley: federationDomain: integration.example.com disabledAPIVersions: [] + # These values are insecure, against anyone getting hold of the hash, + # but its not a concern for the integration tests. + passwordHashingOptions: + algorithm: argon2id + iterations: 1 + parallelism: 4 + memory: 32 # This needs to be at least 8 * parallelism. + featureFlags: sso: disabled-by-default # this needs to be the default; tests can enable it when needed. legalhold: whitelist-teams-and-implicit-consent teamSearchVisibility: disabled-by-default + conferenceCalling: + defaults: + status: disabled classifiedDomains: status: enabled config: domains: ["example.com"] + enforceFileDownloadLocation: + defaults: + status: disabled + lockStatus: unlocked + config: + enforcedDownloadLocation: "downloads" mlsMigration: defaults: status: enabled @@ -269,6 +299,14 @@ galley: usersThreshold: 100 clientsThreshold: 50 lockStatus: locked + mlsE2EId: + defaults: + status: disabled + config: + verificationExpiration: 86400 + acmeDiscoveryUrl: null + crlProxy: https://crlproxy.example.com + lockStatus: unlocked limitedEventFanout: defaults: status: disabled @@ -350,7 +388,6 @@ gundeck: sqsEndpoint: http://fake-aws-sqs:4568 snsEndpoint: http://fake-aws-sns:4575 disabledAPIVersions: [] - bulkPush: true setMaxConcurrentNativePushes: hard: 30 soft: 10 @@ -459,6 +496,7 @@ federator: config: optSettings: useSystemCAStore: false + logLevel: Debug tests: {{- if .Values.uploadXml }} config: @@ -475,6 +513,7 @@ background-worker: requests: {} imagePullPolicy: {{ .Values.imagePullPolicy }} config: + enableFederation: true backendNotificationPusher: pushBackoffMinWait: 1000 # 1ms pushBackoffMaxWait: 500000 # 0.5s diff --git a/hack/helmfile.yaml b/hack/helmfile.yaml index a7ed6861883..3581c373a78 100644 --- a/hack/helmfile.yaml +++ b/hack/helmfile.yaml @@ -221,6 +221,7 @@ releases: chart: '../.local/charts/wire-server' values: - './helm_vars/wire-server/values.yaml.gotmpl' + - './helm_vars/wire-server/values-domain1.yaml.gotmpl' set: - name: brig.config.optSettings.setFederationDomain value: {{ .Values.federationDomain1 }} @@ -236,6 +237,7 @@ releases: chart: '../.local/charts/wire-server' values: - './helm_vars/wire-server/values.yaml.gotmpl' + - './helm_vars/wire-server/values-domain2.yaml.gotmpl' set: - name: brig.config.optSettings.setFederationDomain value: {{ .Values.federationDomain2 }} diff --git a/integration/Setup.hs b/integration/Setup.hs index a9c6110e00b..71b172a7e29 100644 --- a/integration/Setup.hs +++ b/integration/Setup.hs @@ -191,10 +191,11 @@ testHooks hooks = [ "module RunAllTests where", "import Testlib.PTest", "import Prelude", + "import Control.Monad.Trans.Writer", unlines (map ("import qualified " <>) modules), - "allTests :: [Test]", - "allTests =", - " " <> intercalate " <>\n " (map (\(m, n, s, f) -> "mkTests " <> unwords [show m, show n, show s, show f, m <> "." <> n]) tests) + "mkAllTests :: IO [Test]", + "mkAllTests = execWriterT $ do", + unlines (map (\(m, n, s, f) -> " yieldTests " <> unwords [show m, show n, show s, show f, m <> "." <> n]) tests) ] ) pure () diff --git a/integration/default.nix b/integration/default.nix index 040ab8db6f5..37d66c8daf5 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -19,6 +19,7 @@ , Cabal , case-insensitive , containers +, cookie , cql , cql-io , crypton @@ -59,6 +60,7 @@ , regex-base , regex-tdfa , retry +, saml2-web-sso , scientific , split , stm @@ -114,6 +116,7 @@ mkDerivation { bytestring-conversion case-insensitive containers + cookie cql cql-io crypton @@ -151,6 +154,7 @@ mkDerivation { regex-base regex-tdfa retry + saml2-web-sso scientific split stm diff --git a/integration/integration.cabal b/integration/integration.cabal index 0c4c2f93b6c..a3989f28e76 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -13,7 +13,7 @@ build-type: Custom custom-setup setup-depends: , base - , Cabal + , Cabal >=3.12 , containers , directory , filepath @@ -123,7 +123,27 @@ library Test.Errors Test.ExternalPartner Test.FeatureFlags + Test.FeatureFlags.AppLock + Test.FeatureFlags.ClassifiedDomains + Test.FeatureFlags.ConferenceCalling + Test.FeatureFlags.DigitalSignatures + Test.FeatureFlags.EnforceFileDownloadLocation + Test.FeatureFlags.FileSharing + Test.FeatureFlags.GuestLinks + Test.FeatureFlags.Initialisation + Test.FeatureFlags.LegalHold + Test.FeatureFlags.Mls + Test.FeatureFlags.MlsE2EId + Test.FeatureFlags.MlsMigration + Test.FeatureFlags.OutlookCalIntegration + Test.FeatureFlags.SearchVisibilityAvailable + Test.FeatureFlags.SearchVisibilityInbound + Test.FeatureFlags.SelfDeletingMessages + Test.FeatureFlags.SndFactorPasswordChallenge + Test.FeatureFlags.SSO + Test.FeatureFlags.User Test.FeatureFlags.Util + Test.FeatureFlags.ValidateSAMLEmails Test.Federation Test.Federator Test.LegalHold @@ -135,10 +155,14 @@ library Test.MLS.Message Test.MLS.Notifications Test.MLS.One2One + Test.MLS.Services Test.MLS.SubConversation Test.MLS.Unreachable Test.Notifications + Test.OAuth + Test.PasswordReset Test.Presence + Test.Property Test.Provider Test.PushToken Test.Roles @@ -146,6 +170,7 @@ library Test.Services Test.Spar Test.Swagger + Test.Teams Test.TeamSettings Test.User Test.Version @@ -170,6 +195,7 @@ library Testlib.Run Testlib.RunServices Testlib.Types + Testlib.VersionedFed Testlib.XML build-depends: @@ -188,6 +214,7 @@ library , bytestring-conversion , case-insensitive , containers + , cookie , cql , cql-io , crypton @@ -225,6 +252,7 @@ library , regex-base , regex-tdfa , retry + , saml2-web-sso , scientific , split , stm diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index c41865273e9..d084bdf542d 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -123,14 +123,6 @@ getUser user target = do joinHttpPath ["users", domain, uid] submit "GET" req -getUserByHandle :: (HasCallStack, MakesValue user, MakesValue domain) => user -> domain -> String -> App Response -getUserByHandle user domain handle = do - domainStr <- asString domain - req <- - baseRequest user Brig Versioned $ - joinHttpPath ["users", "by-handle", domainStr, handle] - submit "GET" req - -- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/get_clients__client_ getClient :: (HasCallStack, MakesValue user, MakesValue client) => @@ -144,6 +136,15 @@ getClient u cli = do joinHttpPath ["clients", c] submit "GET" req +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/get_clients +getSelfClients :: + (HasCallStack, MakesValue user) => + user -> + App Response +getSelfClients u = + baseRequest u Brig Versioned (joinHttpPath ["clients"]) + >>= submit "GET" + -- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/delete_self deleteUser :: (HasCallStack, MakesValue user) => user -> App Response deleteUser user = do @@ -257,6 +258,13 @@ searchContacts user searchTerm domain = do d <- objDomain domain submit "GET" (req & addQueryParams [("q", q), ("domain", d)]) +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/get_teams__tid__search +searchTeam :: (HasCallStack, MakesValue user) => user -> String -> App Response +searchTeam user q = do + tid <- user %. "team" & asString + req <- baseRequest user Brig Versioned $ joinHttpPath ["teams", tid, "search"] + submit "GET" (req & addQueryParams [("q", q)]) + getAPIVersion :: (HasCallStack, MakesValue domain) => domain -> App Response getAPIVersion domain = do req <- baseRequest domain Brig Unversioned $ "/api-version" @@ -349,14 +357,14 @@ deleteKeyPackages cid kps = do req <- baseRequest cid Brig Versioned ("/mls/key-packages/self/" <> cid.client) submit "DELETE" $ req & addJSONObject ["key_packages" .= kps] -replaceKeyPackages :: ClientIdentity -> [Ciphersuite] -> [ByteString] -> App Response -replaceKeyPackages cid suites kps = do +replaceKeyPackages :: ClientIdentity -> Maybe [Ciphersuite] -> [ByteString] -> App Response +replaceKeyPackages cid mSuites kps = do req <- baseRequest cid Brig Versioned $ "/mls/key-packages/self/" <> cid.client submit "PUT" $ req - & addQueryParams [("ciphersuites", intercalate "," (map (.code) suites))] + & maybe id (\suites -> addQueryParams [("ciphersuites", intercalate "," (map (.code) suites))]) mSuites & addJSONObject ["key_packages" .= map (T.decodeUtf8 . Base64.encode) kps] -- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/get_self @@ -410,6 +418,12 @@ putSelfEmail caller emailAddress = do req <- baseRequest caller Brig Versioned $ joinHttpPath ["users", callerid, "email"] submit "PUT" $ req & addJSONObject ["email" .= emailAddress] +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/delete_self_email +deleteSelfEmail :: (HasCallStack, MakesValue caller) => caller -> App Response +deleteSelfEmail caller = do + req <- baseRequest caller Brig Versioned $ joinHttpPath ["self", "email"] + submit "DELETE" req + -- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/put_self_handle -- FUTUREWORK: rename to putSelfHandle for consistency putHandle :: (HasCallStack, MakesValue user) => user -> String -> App Response @@ -442,11 +456,12 @@ putUserSupportedProtocols user ps = do submit "PUT" (req & addJSONObject ["supported_protocols" .= ps]) data PostInvitation = PostInvitation - { email :: Maybe String + { email :: Maybe String, + role :: Maybe String } instance Default PostInvitation where - def = PostInvitation Nothing + def = PostInvitation Nothing Nothing postInvitation :: (HasCallStack, MakesValue user) => @@ -460,7 +475,7 @@ postInvitation user inv = do joinHttpPath ["teams", tid, "invitations"] email <- maybe randomEmail pure inv.email submit "POST" $ - req & addJSONObject ["email" .= email] + req & addJSONObject (["email" .= email] <> ["role" .= r | r <- toList inv.role]) getApiVersions :: (HasCallStack) => App Response getApiVersions = do @@ -476,9 +491,6 @@ getSwaggerPublicTOC = do joinHttpPath ["api", "swagger-ui"] submit "GET" req -getSwaggerInternalTOC :: (HasCallStack) => App Response -getSwaggerInternalTOC = error "FUTUREWORK: this API end-point does not exist." - getSwaggerPublicAllUI :: (HasCallStack) => Int -> App Response getSwaggerPublicAllUI version = do req <- @@ -668,10 +680,188 @@ getCallsConfigV2 user = do req <- baseRequest user Brig Versioned $ joinHttpPath ["calls", "config", "v2"] submit "GET" req -addBot :: (MakesValue user) => user -> String -> String -> String -> App Response +addBot :: (HasCallStack, MakesValue user) => user -> String -> String -> String -> App Response addBot user providerId serviceId convId = do - req <- baseRequest user Brig Versioned $ joinHttpPath ["conversations", convId, "bots"] + req <- baseRequest user Brig Versioned $ joinHttpPath ["bot", "conversations", convId] submit "POST" $ req & zType "access" & addJSONObject ["provider" .= providerId, "service" .= serviceId] + +setProperty :: (MakesValue user, ToJSON val) => user -> String -> val -> App Response +setProperty user propName val = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["properties", propName] + submit "PUT" $ req & addJSON val + +getProperty :: (MakesValue user) => user -> String -> App Response +getProperty user propName = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["properties", propName] + submit "GET" req + +deleteProperty :: (MakesValue user) => user -> String -> App Response +deleteProperty user propName = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["properties", propName] + submit "DELETE" req + +getAllPropertyNames :: (MakesValue user) => user -> App Response +getAllPropertyNames user = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["properties"] + submit "GET" req + +getAllPropertyValues :: (MakesValue user) => user -> App Response +getAllPropertyValues user = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["properties-values"] + submit "GET" req + +clearProperties :: (MakesValue user) => user -> App Response +clearProperties user = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["properties"] + submit "DELETE" req + +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/post_oauth_authorization_codes +generateOAuthAuthorizationCode :: (HasCallStack, MakesValue user, MakesValue cid) => user -> cid -> [String] -> String -> App Response +generateOAuthAuthorizationCode user cid scopes redirectUrl = do + cidStr <- asString cid + req <- baseRequest user Brig Versioned "/oauth/authorization/codes" + submit "POST" $ + req + & addJSONObject + [ "client_id" .= cidStr, + "scope" .= unwords scopes, + "redirect_uri" .= redirectUrl, + "code_challenge" .= "G7CWLBqYDT8doT_oEIN3un_QwZWYKHmOqG91nwNzITc", + "code_challenge_method" .= "S256", + "response_type" .= "code", + "state" .= "abc" + ] + +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/post_oauth_token +createOAuthAccessToken :: (HasCallStack, MakesValue user, MakesValue cid) => user -> cid -> String -> String -> App Response +createOAuthAccessToken user cid code redirectUrl = do + cidStr <- asString cid + req <- baseRequest user Brig Versioned "/oauth/token" + submit "POST" $ + req + & addUrlEncodedForm + [ ("grant_type", "authorization_code"), + ("client_id", cidStr), + ("code_verifier", "nE3k3zykOmYki~kriKzAmeFiGT7cWugcuToFwo1YPgrZ1cFvaQqLa.dXY9MnDj3umAmG-8lSNIYIl31Cs_.fV5r2psa4WWZcB.Nlc3A-t3p67NDZaOJjIiH~8PvUH_hR"), + ("code", code), + ("redirect_uri", redirectUrl) + ] + +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/post_oauth_token +createOAuthAccessTokenWithRefreshToken :: (HasCallStack, MakesValue user, MakesValue cid) => user -> cid -> String -> App Response +createOAuthAccessTokenWithRefreshToken user cid token = do + cidStr <- asString cid + req <- baseRequest user Brig Versioned "/oauth/token" + submit "POST" $ + req + & addUrlEncodedForm + [ ("grant_type", "refresh_token"), + ("client_id", cidStr), + ("refresh_token", token) + ] + +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/get_oauth_applications +getOAuthApplications :: (HasCallStack, MakesValue user) => user -> App Response +getOAuthApplications user = do + req <- baseRequest user Brig Versioned "/oauth/applications" + submit "GET" req + +deleteOAuthSession :: (HasCallStack, MakesValue user, MakesValue cid) => user -> cid -> String -> String -> App Response +deleteOAuthSession user cid password tokenId = do + cidStr <- asString cid + req <- baseRequest user Brig Versioned $ joinHttpPath ["oauth", "applications", cidStr, "sessions", tokenId] + submit "DELETE" $ req & addJSONObject ["password" .= password] + +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/delete_oauth_applications__OAuthClientId_ +revokeApplicationAccessV6 :: (HasCallStack, MakesValue user, MakesValue cid) => user -> cid -> App Response +revokeApplicationAccessV6 user cid = do + cidStr <- asString cid + req <- baseRequest user Brig (ExplicitVersion 6) $ joinHttpPath ["oauth", "applications", cidStr] + submit "DELETE" req + +revokeApplicationAccess :: (HasCallStack, MakesValue user, MakesValue cid) => user -> cid -> String -> App Response +revokeApplicationAccess user cid password = do + cidStr <- asString cid + req <- baseRequest user Brig Versioned $ joinHttpPath ["oauth", "applications", cidStr, "sessions"] + submit "DELETE" $ req & addJSONObject ["password" .= password] + +registerUser :: (HasCallStack, MakesValue domain) => domain -> String -> String -> App Response +registerUser domain email inviteeCode = do + req <- baseRequest domain Brig Versioned "register" + submit "POST" $ + req + & addJSONObject + [ "name" .= "Alice", + "email" .= email, + "password" .= defPassword, + "team_code" .= inviteeCode + ] + +activate :: (HasCallStack, MakesValue domain) => domain -> String -> String -> App Response +activate domain key code = do + req <- rawBaseRequest domain Brig Versioned $ joinHttpPath ["activate"] + submit "GET" $ + req + & addQueryParams [("key", key), ("code", code)] + +acceptTeamInvitation :: (HasCallStack, MakesValue user) => user -> String -> Maybe String -> App Response +acceptTeamInvitation user code mPw = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["teams", "invitations", "accept"] + submit "POST" $ req & addJSONObject (["code" .= code] <> maybeToList (((.=) "password") <$> mPw)) + +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/get_teams__tid__invitations +listInvitations :: (HasCallStack, MakesValue user) => user -> String -> App Response +listInvitations user tid = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["teams", tid, "invitations"] + submit "GET" req + +passwordReset :: (HasCallStack, MakesValue domain) => domain -> String -> App Response +passwordReset domain email = do + req <- baseRequest domain Brig Versioned "password-reset" + submit "POST" $ req & addJSONObject ["email" .= email] + +completePasswordReset :: (HasCallStack, MakesValue domain) => domain -> String -> String -> String -> App Response +completePasswordReset domain key code pw = do + req <- baseRequest domain Brig Versioned $ joinHttpPath ["password-reset", "complete"] + submit "POST" $ req & addJSONObject ["key" .= key, "code" .= code, "password" .= pw] + +login :: (HasCallStack, MakesValue domain) => domain -> String -> String -> App Response +login domain email password = do + req <- baseRequest domain Brig Versioned "login" + submit "POST" $ req & addJSONObject ["email" .= email, "password" .= password] & addQueryParams [("persist", "true")] + +updateEmail :: (HasCallStack, MakesValue user) => user -> String -> String -> String -> App Response +updateEmail user email cookie token = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["access", "self", "email"] + submit "PUT" $ req & addJSONObject ["email" .= email] & setCookie cookie & addHeader "Authorization" ("Bearer " <> token) + +upgradePersonalToTeam :: (HasCallStack, MakesValue user) => user -> String -> App Response +upgradePersonalToTeam user name = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["upgrade-personal-to-team"] + submit "POST" $ req & addJSONObject ["name" .= name, "icon" .= "default"] + +postServiceWhitelist :: + ( HasCallStack, + MakesValue user, + MakesValue tid, + MakesValue update + ) => + user -> + tid -> + update -> + App Response +postServiceWhitelist user tid update = do + tidStr <- asString tid + updateJson <- make update + req <- + baseRequest user Brig Versioned $ + joinHttpPath + [ "teams", + tidStr, + "services", + "whitelist" + ] + submit "POST" (addJSON updateJson req) diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index 5fbfd5cf2e5..ff8b6f40f61 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -148,9 +148,13 @@ deleteOAuthClient user cid = do getInvitationCode :: (HasCallStack, MakesValue user, MakesValue inv) => user -> inv -> App Response getInvitationCode user inv = do tid <- user %. "team" & asString + getInvitationCodeForTeam user tid inv + +getInvitationCodeForTeam :: (HasCallStack, MakesValue domain, MakesValue inv) => domain -> String -> inv -> App Response +getInvitationCodeForTeam domain tid inv = do invId <- inv %. "id" & asString req <- - baseRequest user Brig Unversioned $ + baseRequest domain Brig Unversioned $ "i/teams/invitation-code?team=" <> tid <> "&invitation_id=" <> invId submit "GET" req @@ -160,17 +164,6 @@ refreshIndex domain = do res <- submit "POST" req res.status `shouldMatchInt` 200 -connectWithRemoteUser :: (MakesValue userFrom, MakesValue userTo) => userFrom -> userTo -> App () -connectWithRemoteUser userFrom userTo = do - userFromId <- objId userFrom - qUserTo <- make userTo - let body = ["tag" .= "CreateConnectionForTest", "user" .= userFromId, "other" .= qUserTo] - req <- - baseRequest userFrom Brig Unversioned $ - joinHttpPath ["i", "connections", "connection-update"] - res <- submit "PUT" (req & addJSONObject body) - res.status `shouldMatchInt` 200 - addFederationRemoteTeam :: (HasCallStack, MakesValue domain, MakesValue remoteDomain, MakesValue team) => domain -> remoteDomain -> team -> App () addFederationRemoteTeam domain remoteDomain team = do void $ addFederationRemoteTeam' domain remoteDomain team >>= getBody 200 @@ -255,10 +248,84 @@ getEJPDInfo dom handles mode = do bad -> error $ show bad submit "POST" $ req & addJSONObject ["EJPDRequest" .= handles] & addQueryParams query --- https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/get_i_users__uid__verification_code__action_ +-- | https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/get_i_users__uid__verification_code__action_ getVerificationCode :: (HasCallStack, MakesValue user) => user -> String -> App Response getVerificationCode user action = do uid <- objId user domain <- objDomain user req <- baseRequest domain Brig Unversioned $ joinHttpPath ["i", "users", uid, "verification-code", action] submit "GET" req + +-- | http://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/get_i_users__uid__features_conferenceCalling +getFeatureForUser :: (HasCallStack, MakesValue user) => user -> String -> App Response +getFeatureForUser user featureName = do + uid <- objId user + req <- baseRequest user Brig Unversioned $ joinHttpPath ["i", "users", uid, "features", featureName] + submit "GET" req + +-- | http://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/put_i_users__uid__features_conferenceCalling +putFeatureForUser :: + (HasCallStack, MakesValue user, MakesValue config) => + user -> + String -> + config -> + App Response +putFeatureForUser user featureName config = do + uid <- objId user + req <- baseRequest user Brig Unversioned $ joinHttpPath ["i", "users", uid, "features", featureName] + configValue <- make config + submit "PUT" $ req & addJSON configValue + +-- | http://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/delete_i_users__uid__features_conferenceCalling +deleteFeatureForUser :: (HasCallStack, MakesValue user) => user -> String -> App Response +deleteFeatureForUser user featureName = do + uid <- objId user + req <- baseRequest user Brig Unversioned $ joinHttpPath ["i", "users", uid, "features", featureName] + submit "DELETE" req + +-- | https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/post_i_oauth_clients +createOAuthClient :: (HasCallStack, MakesValue user) => user -> String -> String -> App Response +createOAuthClient user name url = do + req <- baseRequest user Brig Unversioned "i/oauth/clients" + submit "POST" $ req & addJSONObject ["application_name" .= name, "redirect_url" .= url] + +getInvitationByEmail :: (HasCallStack, MakesValue domain) => domain -> String -> App Response +getInvitationByEmail domain email = do + req <- baseRequest domain Brig Unversioned "i/teams/invitations/by-email" + submit "GET" $ req & addQueryParams [("email", email)] + +getActivationCode :: (HasCallStack, MakesValue domain) => domain -> String -> App Response +getActivationCode domain email = do + req <- baseRequest domain Brig Unversioned "i/users/activation-code" + submit "GET" $ req & addQueryParams [("email", email)] + +getPasswordResetCode :: (HasCallStack, MakesValue domain) => domain -> String -> App Response +getPasswordResetCode domain email = do + req <- baseRequest domain Brig Unversioned "i/users/password-reset-code" + submit "GET" $ req & addQueryParams [("email", email)] + +data PutSSOId = PutSSOId + { scimExternalId :: Maybe String, + subject :: Maybe String, + tenant :: Maybe String + } + +instance Default PutSSOId where + def = + PutSSOId + { scimExternalId = Nothing, + subject = Nothing, + tenant = Nothing + } + +putSSOId :: (HasCallStack, MakesValue user) => user -> PutSSOId -> App Response +putSSOId user args = do + uid <- objId user + req <- baseRequest user Brig Unversioned (joinHttpPath ["i", "users", uid, "sso-id"]) + submit "PUT" $ + req + & addJSONObject + [ "scim_external_id" .= args.scimExternalId, + "subject" .= args.subject, + "tenant" .= args.tenant + ] diff --git a/integration/test/API/Cargohold.hs b/integration/test/API/Cargohold.hs index e21e26fed81..df8af34d71c 100644 --- a/integration/test/API/Cargohold.hs +++ b/integration/test/API/Cargohold.hs @@ -72,9 +72,6 @@ textPlainMime = MIME.Text $ T.pack "plain" multipartMixedMime :: String multipartMixedMime = "multipart/mixed; boundary=" <> multipartBoundary -mimeTypeToString :: MIME.MIMEType -> String -mimeTypeToString = T.unpack . MIME.showMIMEType - buildUploadAssetRequestBody :: (HasCallStack, MakesValue assetRetention) => Bool -> diff --git a/integration/test/API/Common.hs b/integration/test/API/Common.hs index c07816cc5b4..e1c91d05b7c 100644 --- a/integration/test/API/Common.hs +++ b/integration/test/API/Common.hs @@ -5,16 +5,11 @@ import Control.Monad.IO.Class import Data.Array ((!)) import qualified Data.Array as Array import qualified Data.ByteString as BS +import Data.Scientific (scientific) +import qualified Data.Vector as Vector import System.Random (randomIO, randomRIO) import Testlib.Prelude -teamRole :: String -> Int -teamRole "partner" = 1025 -teamRole "member" = 1587 -teamRole "admin" = 5951 -teamRole "owner" = 8191 -teamRole bad = error $ "unknown team role: " <> bad - -- | please don't use special shell characters like '!' here. it makes writing shell lines -- that use test data a lot less straight-forward. defPassword :: String @@ -25,6 +20,15 @@ randomEmail = do u <- randomName pure $ u <> "@example.com" +randomExternalId :: App String +randomExternalId = liftIO $ do + -- external ID has no constraints, but we only generate human-readable samples + n <- randomRIO (8, 15) + replicateM n pick + where + chars = mkArray $ ['A' .. 'Z'] <> ['a' .. 'z'] <> ['0' .. '9'] + pick = (chars !) <$> randomRIO (Array.bounds chars) + randomName :: App String randomName = liftIO $ do n <- randomRIO (8, 15) @@ -47,6 +51,29 @@ randomHandleWithRange min' max' = liftIO $ do randomBytes :: Int -> App ByteString randomBytes n = liftIO $ BS.pack <$> replicateM n randomIO +randomString :: Int -> App String +randomString n = liftIO $ replicateM n randomIO + +randomJSON :: App Value +randomJSON = do + let maxThings = 5 + liftIO (randomRIO (0 :: Int, 5)) >>= \case + 0 -> String . fromString <$> (randomString =<< randomRIO (0, maxThings)) + 1 -> Number <$> liftIO (scientific <$> randomIO <*> randomIO) + 2 -> Bool <$> liftIO randomIO + 3 -> pure Null + 4 -> do + n <- liftIO $ randomRIO (0, maxThings) + Array . Vector.fromList <$> replicateM n randomJSON + 5 -> do + n <- liftIO $ randomRIO (0, maxThings) + keys <- do + keyLength <- randomRIO (0, maxThings) + replicateM n (randomString keyLength) + vals <- replicateM n randomJSON + pure . object $ zipWith (.=) keys vals + _ -> error $ "impopssible: randomJSON" + randomHex :: Int -> App String randomHex n = liftIO $ replicateM n pick where diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index d4c4b6e366e..d1c4066ae70 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -228,6 +228,11 @@ getMLSPublicKeys user = do req <- baseRequest user Galley Versioned "/mls/public-keys" submit "GET" req +getMLSPublicKeysJWK :: (HasCallStack, MakesValue user) => user -> App Response +getMLSPublicKeysJWK user = do + req <- baseRequest user Galley Versioned "/mls/public-keys" + submit "GET" $ addQueryParams [("format", "jwk")] req + postMLSMessage :: (HasCallStack) => ClientIdentity -> ByteString -> App Response postMLSMessage cid msg = do req <- baseRequest cid Galley Versioned "/mls/messages" @@ -322,6 +327,18 @@ deleteTeamConv team conv user = do req <- baseRequest user Galley Versioned (joinHttpPath ["teams", teamId, "conversations", convId]) submit "DELETE" req +getMLSOne2OneConversationLegacy :: + (HasCallStack, MakesValue self, MakesValue other) => + self -> + other -> + App Response +getMLSOne2OneConversationLegacy self other = do + (domain, uid) <- objQid other + req <- + baseRequest self Galley Versioned + $ joinHttpPath ["conversations", "one2one", domain, uid] + submit "GET" req + getMLSOne2OneConversation :: (HasCallStack, MakesValue self, MakesValue other) => self -> @@ -331,7 +348,7 @@ getMLSOne2OneConversation self other = do (domain, uid) <- objQid other req <- baseRequest self Galley Versioned - $ joinHttpPath ["conversations", "one2one", domain, uid] + $ joinHttpPath ["one2one-conversations", domain, uid] submit "GET" req getGroupClients :: @@ -514,6 +531,12 @@ updateMessageTimer user qcnv update = do req <- baseRequest user Galley Versioned path submit "PUT" (addJSONObject ["message_timer" .= updateReq] req) +getTeam :: (HasCallStack, MakesValue user, MakesValue tid) => user -> tid -> App Response +getTeam user tid = do + tidStr <- asString tid + req <- baseRequest user Galley Versioned (joinHttpPath ["teams", tidStr]) + submit "GET" req + getTeamMembers :: (HasCallStack, MakesValue user, MakesValue tid) => user -> tid -> App Response getTeamMembers user tid = do tidStr <- asString tid @@ -592,13 +615,6 @@ legalholdUserStatus tid ownerid user = do req <- baseRequest ownerid Galley Versioned (joinHttpPath ["teams", tidS, "legalhold", uid]) submit "GET" req --- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_teams__tid__legalhold_settings -enableLegalHold :: (HasCallStack, MakesValue tid, MakesValue ownerid) => tid -> ownerid -> App Response -enableLegalHold tid ownerid = do - tidStr <- asString tid - req <- baseRequest ownerid Galley Versioned (joinHttpPath ["teams", tidStr, "features", "legalhold"]) - submit "PUT" (addJSONObject ["status" .= "enabled", "ttl" .= "unlimited"] req) - -- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/delete_teams__tid__legalhold__uid_ disableLegalHold :: (HasCallStack, MakesValue tid, MakesValue ownerid, MakesValue uid) => @@ -614,6 +630,21 @@ disableLegalHold tid ownerid uid pw = do req <- baseRequest ownerid Galley Versioned (joinHttpPath ["teams", tidStr, "legalhold", uidStr]) submit "DELETE" (addJSONObject ["password" .= pw] req) +-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_teams__tid__legalhold_consent +consentToLegalHold :: (HasCallStack, MakesValue tid, MakesValue zusr) => tid -> zusr -> String -> App Response +consentToLegalHold tid zusr pwd = do + tidStr <- asString tid + req <- baseRequest zusr Galley Versioned (joinHttpPath ["teams", tidStr, "legalhold", "consent"]) + submit "POST" (addJSONObject ["password" .= pwd] req) + +-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/get_teams__tid__legalhold__uid_ +getLegalHoldStatus :: (HasCallStack, MakesValue tid, MakesValue zusr) => tid -> zusr -> App Response +getLegalHoldStatus tid zusr = do + tidStr <- asString tid + uidStr <- asString $ zusr %. "id" + req <- baseRequest zusr Galley Versioned (joinHttpPath ["teams", tidStr, "legalhold", uidStr]) + submit "GET" req + -- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_teams__tid__legalhold_settings postLegalHoldSettings :: (HasCallStack, MakesValue ownerid, MakesValue tid, MakesValue newService) => tid -> ownerid -> newService -> App Response postLegalHoldSettings tid owner newSettings = @@ -653,21 +684,6 @@ approveLegalHoldDevice' tid uid forUid pwd = do req <- baseRequest uid Galley Versioned (joinHttpPath ["teams", tidStr, "legalhold", uidStr, "approve"]) submit "PUT" (addJSONObject ["password" .= pwd] req) --- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_teams__tid__legalhold_consent -consentToLegalHold :: (HasCallStack, MakesValue tid, MakesValue zusr) => tid -> zusr -> String -> App Response -consentToLegalHold tid zusr pwd = do - tidStr <- asString tid - req <- baseRequest zusr Galley Versioned (joinHttpPath ["teams", tidStr, "legalhold", "consent"]) - submit "POST" (addJSONObject ["password" .= pwd] req) - --- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/get_teams__tid__legalhold__uid_ -getLegalHoldStatus :: (HasCallStack, MakesValue tid, MakesValue zusr) => tid -> zusr -> App Response -getLegalHoldStatus tid zusr = do - tidStr <- asString tid - uidStr <- asString $ zusr %. "id" - req <- baseRequest zusr Galley Versioned (joinHttpPath ["teams", tidStr, "legalhold", uidStr]) - submit "GET" req - -- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/put_teams__tid__features_legalhold putLegalholdStatus :: (HasCallStack, MakesValue tid, MakesValue usr) => @@ -712,3 +728,20 @@ setTeamFeatureConfigVersioned versioned user team featureName payload = do p <- make payload req <- baseRequest user Galley versioned $ joinHttpPath ["teams", tid, "features", fn] submit "PUT" $ req & addJSON p + +-- | http://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/get_feature_configs +getFeaturesForUser :: (HasCallStack, MakesValue user) => user -> App Response +getFeaturesForUser user = baseRequest user Galley Versioned "feature-configs" >>= submit "GET" + +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/get_teams_notifications +getTeamNotifications :: (HasCallStack, MakesValue user) => user -> Maybe String -> App Response +getTeamNotifications user mSince = + baseRequest user Galley Versioned "teams/notifications" >>= \req -> + submit "GET" + $ addQueryParams [("since", since) | since <- maybeToList mSince] req + +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/get_teams__tid__members_csv +getTeamMembersCsv :: (HasCallStack, MakesValue user) => user -> String -> App Response +getTeamMembersCsv user tid = do + req <- baseRequest user Galley Versioned (joinHttpPath ["teams", tid, "members", "csv"]) + submit "GET" req diff --git a/integration/test/API/GalleyInternal.hs b/integration/test/API/GalleyInternal.hs index ef0f773d426..de6f5c21c47 100644 --- a/integration/test/API/GalleyInternal.hs +++ b/integration/test/API/GalleyInternal.hs @@ -106,6 +106,14 @@ setTeamFeatureConfig domain team featureName payload = do req <- baseRequest domain Galley Unversioned $ joinHttpPath ["i", "teams", tid, "features", fn] submit "PUT" $ req & addJSON p +patchTeamFeatureConfig :: (HasCallStack, MakesValue domain, MakesValue team, MakesValue featureName, MakesValue payload) => domain -> team -> featureName -> payload -> App Response +patchTeamFeatureConfig domain team featureName payload = do + tid <- asString team + fn <- asString featureName + p <- make payload + req <- baseRequest domain Galley Unversioned $ joinHttpPath ["i", "teams", tid, "features", fn] + submit "PATCH" $ req & addJSON p + -- https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/galley/#/galley/post_i_features_multi_teams_searchVisibilityInbound getFeatureStatusMulti :: (HasCallStack, MakesValue domain, MakesValue featureName) => domain -> featureName -> [String] -> App Response getFeatureStatusMulti domain featureName tids = do diff --git a/integration/test/API/Spar.hs b/integration/test/API/Spar.hs index e8d1e7cc2f3..c925c7cc5d7 100644 --- a/integration/test/API/Spar.hs +++ b/integration/test/API/Spar.hs @@ -1,7 +1,9 @@ module API.Spar where import API.Common (defPassword) +import Data.String.Conversions.Monomorphic (fromLT) import GHC.Stack +import qualified SAML2.WebSSO as SAML import Testlib.Prelude -- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/get_scim_auth_tokens @@ -10,14 +12,56 @@ getScimTokens caller = do req <- baseRequest caller Spar Versioned "/scim/auth-tokens" submit "GET" req --- https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_scim_auth_tokens +-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_scim_auth_tokens createScimToken :: (HasCallStack, MakesValue caller) => caller -> App Response createScimToken caller = do req <- baseRequest caller Spar Versioned "/scim/auth-tokens" submit "POST" $ req & addJSONObject ["password" .= defPassword, "description" .= "integration test"] +-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_scim_auth_tokens +createScimTokenWithName :: (HasCallStack, MakesValue caller) => caller -> String -> App Response +createScimTokenWithName caller name = do + req <- baseRequest caller Spar Versioned "/scim/auth-tokens" + submit "POST" $ req & addJSONObject ["password" .= defPassword, "description" .= "integration test", "name" .= name] + +putScimTokenName :: (HasCallStack, MakesValue caller) => caller -> String -> String -> App Response +putScimTokenName caller token name = do + req <- baseRequest caller Spar Versioned $ joinHttpPath ["scim", "auth-tokens", token] + submit "PUT" $ req & addJSONObject ["name" .= name] + createScimUser :: (HasCallStack, MakesValue domain, MakesValue scimUser) => domain -> String -> scimUser -> App Response createScimUser domain token scimUser = do req <- baseRequest domain Spar Versioned "/scim/v2/Users" body <- make scimUser submit "POST" $ req & addJSON body . addHeader "Authorization" ("Bearer " <> token) + +findUsersByExternalId :: (HasCallStack, MakesValue domain) => domain -> String -> String -> App Response +findUsersByExternalId domain scimToken externalId = do + req <- baseRequest domain Spar Versioned "/scim/v2/Users" + submit "GET" $ req + & addQueryParams [("filter", "externalId eq \"" <> externalId <> "\"")] + & addHeader "Authorization" ("Bearer " <> scimToken) + & addHeader "Accept" "application/scim+json" + +getScimUser :: (HasCallStack, MakesValue domain) => domain -> String -> String -> App Response +getScimUser domain scimToken uid = do + req <- baseRequest domain Spar Versioned $ joinHttpPath ["scim", "v2", "Users", uid] + submit "GET" $ req + & addHeader "Authorization" ("Bearer " <> scimToken) + & addHeader "Accept" "application/scim+json" + +updateScimUser :: (HasCallStack, MakesValue domain, MakesValue scimUser) => domain -> String -> String -> scimUser -> App Response +updateScimUser domain scimToken userId scimUser = do + req <- baseRequest domain Spar Versioned $ joinHttpPath ["scim", "v2", "Users", userId] + body <- make scimUser + submit "PUT" $ req + & addJSON body . addHeader "Authorization" ("Bearer " <> scimToken) + & addHeader "Accept" "application/scim+json" + +createIdp :: (HasCallStack, MakesValue user) => user -> SAML.IdPMetadata -> App Response +createIdp user metadata = do + req <- baseRequest user Spar Unversioned "/identity-providers" + submit "POST" $ req + & addQueryParams [("api_version", "v2")] + & addXML (fromLT $ SAML.encode metadata) + & addHeader "Content-Type" "application/xml" diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index e8417123bad..f5e753cf88b 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -221,7 +221,14 @@ createSubConv cid subId = do resetGroup cid sub void $ createPendingProposalCommit cid >>= sendAndConsumeCommitBundle -resetGroup :: (MakesValue conv) => ClientIdentity -> conv -> App () +createOne2OneSubConv :: (HasCallStack, MakesValue keys) => ClientIdentity -> String -> keys -> App () +createOne2OneSubConv cid subId keys = do + mls <- getMLSState + sub <- getSubConversation cid mls.convId subId >>= getJSON 200 + resetOne2OneGroupGeneric cid sub keys + void $ createPendingProposalCommit cid >>= sendAndConsumeCommitBundle + +resetGroup :: (HasCallStack, MakesValue conv) => ClientIdentity -> conv -> App () resetGroup cid conv = do convId <- objSubConvObject conv groupId <- conv %. "group_id" & asString @@ -233,29 +240,46 @@ resetGroup cid conv = do epoch = 0, newMembers = mempty } - resetClientGroup cid groupId + keys <- getMLSPublicKeys cid.qualifiedUserId >>= getJSON 200 + resetClientGroup cid groupId keys + +resetOne2OneGroup :: (HasCallStack, MakesValue one2OneConv) => ClientIdentity -> one2OneConv -> App () +resetOne2OneGroup cid one2OneConv = + resetOne2OneGroupGeneric cid (one2OneConv %. "conversation") (one2OneConv %. "public_keys") + +-- | Useful when keys are to be taken from main conv and the conv here is the subconv +resetOne2OneGroupGeneric :: (HasCallStack, MakesValue conv, MakesValue keys) => ClientIdentity -> conv -> keys -> App () +resetOne2OneGroupGeneric cid conv keys = do + convId <- objSubConvObject conv + groupId <- conv %. "group_id" & asString + modifyMLSState $ \s -> + s + { groupId = Just groupId, + convId = Just convId, + members = Set.singleton cid, + epoch = 0, + newMembers = mempty + } + resetClientGroup cid groupId keys -resetClientGroup :: ClientIdentity -> String -> App () -resetClientGroup cid gid = do +resetClientGroup :: (HasCallStack, MakesValue keys) => ClientIdentity -> String -> keys -> App () +resetClientGroup cid gid keys = do mls <- getMLSState - removalKeyPaths <- asks (.removalKeyPaths) - removalKeyPath <- - assertOne $ - Map.lookup (csSignatureScheme mls.ciphersuite) removalKeyPaths + removalKey <- asByteString $ keys %. ("removal." <> csSignatureScheme mls.ciphersuite) void $ mlscli cid [ "group", "create", "--removal-key", - removalKeyPath, + "-", "--group-out", "", "--ciphersuite", mls.ciphersuite.code, gid ] - Nothing + (Just removalKey) keyPackageFile :: (HasCallStack) => ClientIdentity -> String -> App FilePath keyPackageFile cid ref = do @@ -757,17 +781,6 @@ createApplicationMessage cid messageContent = do setMLSCiphersuite :: Ciphersuite -> App () setMLSCiphersuite suite = modifyMLSState $ \mls -> mls {ciphersuite = suite} -withCiphersuite :: (HasCallStack) => Ciphersuite -> App a -> App a -withCiphersuite suite action = do - suite0 <- (.ciphersuite) <$> getMLSState - setMLSCiphersuiteIO <- appToIOKleisli setMLSCiphersuite - actionIO <- appToIO action - liftIO $ - bracket - (setMLSCiphersuiteIO suite) - (const (setMLSCiphersuiteIO suite0)) - (const actionIO) - leaveCurrentConv :: (HasCallStack) => ClientIdentity -> diff --git a/integration/test/Notifications.hs b/integration/test/Notifications.hs index 13dd5a0fb35..d99b46b8897 100644 --- a/integration/test/Notifications.hs +++ b/integration/test/Notifications.hs @@ -166,6 +166,9 @@ isConvDeleteNotif n = fieldEquals n "payload.0.type" "conversation.delete" notifTypeIsEqual :: (MakesValue a) => String -> a -> App Bool notifTypeIsEqual typ n = nPayload n %. "type" `isEqual` typ +isTeamMemberJoinNotif :: (MakesValue a) => a -> App Bool +isTeamMemberJoinNotif = notifTypeIsEqual "team.member-join" + isTeamMemberLeaveNotif :: (MakesValue a) => a -> App Bool isTeamMemberLeaveNotif = notifTypeIsEqual "team.member-leave" @@ -175,6 +178,9 @@ isUserActivateNotif = notifTypeIsEqual "user.activate" isUserClientAddNotif :: (MakesValue a) => a -> App Bool isUserClientAddNotif = notifTypeIsEqual "user.client-add" +isUserUpdatedNotif :: (MakesValue a) => a -> App Bool +isUserUpdatedNotif = notifTypeIsEqual "user.update" + isUserClientRemoveNotif :: (MakesValue a) => a -> App Bool isUserClientRemoveNotif = notifTypeIsEqual "user.client-remove" diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 63e4c61b786..4e19ae9b0a6 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -24,6 +24,7 @@ import qualified Data.Text as Text import Data.Text.Encoding (decodeUtf8) import Data.UUID.V1 (nextUUID) import Data.UUID.V4 (nextRandom) +import Data.Vector (fromList) import GHC.Stack import Testlib.MockIntegrationService (mkLegalHoldSettings) import Testlib.Prelude @@ -40,49 +41,45 @@ deleteUser user = bindResponse (API.Brig.deleteUser user) $ \resp -> do -- | returns (owner, team id, members) createTeam :: (HasCallStack, MakesValue domain) => domain -> Int -> App (Value, String, [Value]) createTeam domain memberCount = do - res <- createUser domain def {team = True} - owner <- res.json + owner <- createUser domain def {team = True} >>= getJSON 201 tid <- owner %. "team" & asString - members <- for [2 .. memberCount] $ \_ -> createTeamMember owner tid + members <- for [2 .. memberCount] $ \_ -> createTeamMember owner def pure (owner, tid, members) -createTeamMember :: - (HasCallStack, MakesValue inviter) => - inviter -> - String -> - App Value -createTeamMember inviter tid = createTeamMemberWithRole inviter tid "member" +data CreateTeamMember = CreateTeamMember + { role :: String + } + +instance Default CreateTeamMember where + def = CreateTeamMember {role = "member"} -createTeamMemberWithRole :: +createTeamMember :: (HasCallStack, MakesValue inviter) => inviter -> - String -> - String -> + CreateTeamMember -> App Value -createTeamMemberWithRole inviter tid role = do +createTeamMember inviter args = do newUserEmail <- randomEmail - let invitationJSON = ["role" .= role, "email" .= newUserEmail] - invitationReq <- - baseRequest inviter Brig Versioned $ - joinHttpPath ["teams", tid, "invitations"] - invitation <- getJSON 201 =<< submit "POST" (addJSONObject invitationJSON invitationReq) - invitationId <- objId invitation - invitationCodeReq <- - rawBaseRequest inviter Brig Unversioned "/i/teams/invitation-code" - <&> addQueryParams [("team", tid), ("invitation_id", invitationId)] - invitationCode <- bindResponse (submit "GET" invitationCodeReq) $ \res -> do - res.status `shouldMatchInt` 200 - res.json %. "code" & asString - let registerJSON = - [ "name" .= newUserEmail, - "email" .= newUserEmail, - "password" .= defPassword, - "team_code" .= invitationCode - ] - registerReq <- - rawBaseRequest inviter Brig Versioned "/register" - <&> addJSONObject registerJSON - getJSON 201 =<< submit "POST" registerReq + invitation <- + postInvitation + inviter + def + { email = Just newUserEmail, + role = Just args.role + } + >>= getJSON 201 + invitationCode <- + (getInvitationCode inviter invitation >>= getJSON 200) + %. "code" + & asString + let body = + AddUser + { name = Just newUserEmail, + email = Just newUserEmail, + password = Just defPassword, + teamCode = Just invitationCode + } + addUser inviter body >>= getJSON 201 connectTwoUsers :: ( HasCallStack, @@ -173,13 +170,22 @@ addUserToTeam u = do -- | Create a user on the given domain, such that the 1-1 conversation with -- 'other' resides on 'convDomain'. This connects the two users as a side-effect. -createMLSOne2OnePartner :: (MakesValue user) => Domain -> user -> Domain -> App Value +createMLSOne2OnePartner :: + (MakesValue user, MakesValue domain, MakesValue convDomain, HasCallStack) => + domain -> + user -> + convDomain -> + App Value createMLSOne2OnePartner domain other convDomain = loop where loop = do u <- randomUser domain def connectTwoUsers u other - conv <- getMLSOne2OneConversation other u >>= getJSON 200 + apiVersion <- getAPIVersionFor domain + conv <- + if apiVersion < 6 + then getMLSOne2OneConversation other u >>= getJSON 200 + else getMLSOne2OneConversation other u >>= getJSON 200 >>= (%. "conversation") desiredConvDomain <- make convDomain & asString actualConvDomain <- conv %. "qualified_id.domain" & asString @@ -227,14 +233,15 @@ withFederatingBackendsAllowDynamic k = do -- | Create two users on different domains such that the one-to-one -- conversation, once finalised, will be hosted on the backend given by the --- input domain. -createOne2OneConversation :: (HasCallStack) => Domain -> App (Value, Value, Value) -createOne2OneConversation owningDomain = do +-- first domain. +createOne2OneConversation :: + (HasCallStack, MakesValue domain1, MakesValue domain2) => + domain1 -> + domain2 -> + App (Value, Value, Value) +createOne2OneConversation owningDomain otherDomain = do owningUser <- randomUser owningDomain def domainName <- owningUser %. "qualified_id.domain" - let otherDomain = case owningDomain of - OwnDomain -> OtherDomain - OtherDomain -> OwnDomain let go = do otherUser <- randomUser otherDomain def otherUserId <- otherUser %. "qualified_id" @@ -341,11 +348,16 @@ lhDeviceIdOf bob = do randomScimUser :: App Value randomScimUser = do email <- randomEmail + randomScimUserWith email email + +randomScimUserWith :: (HasCallStack) => String -> String -> App Value +randomScimUserWith extId email = do handle <- randomHandleWithRange 12 128 pure $ object [ "schemas" .= ["urn:ietf:params:scim:schemas:core:2.0:User"], - "externalId" .= email, + "externalId" .= extId, + "emails" .= Array (fromList [object ["value" .= email]]), "userName" .= handle, "displayName" .= handle ] diff --git a/integration/test/Test/AccessUpdate.hs b/integration/test/Test/AccessUpdate.hs index ad2f12a978b..01113946788 100644 --- a/integration/test/Test/AccessUpdate.hs +++ b/integration/test/Test/AccessUpdate.hs @@ -22,6 +22,7 @@ import API.Galley import Control.Monad.Codensity import Control.Monad.Reader import GHC.Stack +import MLS.Util import Notifications import SetupHelpers import Testlib.Prelude @@ -38,28 +39,55 @@ testBaz :: HasCallStack => App () testBaz = pure () -} +data ConversationProtocol + = ConversationProtocolProteus + | ConversationProtocolMLS + +instance TestCases ConversationProtocol where + mkTestCases = + pure + [ MkTestCase "[proto=proteus]" ConversationProtocolProteus, + MkTestCase "[proto=mls]" ConversationProtocolMLS + ] + +-- | @SF.Federation @SF.Separation @TSFI.RESTfulAPI @S2 -- -- The test asserts that, among others, remote users are removed from a -- conversation when an access update occurs that disallows guests from -- accessing. -testAccessUpdateGuestRemoved :: (HasCallStack) => App () -testAccessUpdateGuestRemoved = do +testAccessUpdateGuestRemoved :: (HasCallStack) => ConversationProtocol -> App () +testAccessUpdateGuestRemoved proto = do (alice, tid, [bob]) <- createTeam OwnDomain 2 charlie <- randomUser OwnDomain def dee <- randomUser OtherDomain def mapM_ (connectTwoUsers alice) [charlie, dee] - [aliceClient, bobClient, charlieClient, deeClient] <- - mapM - (\user -> objId $ bindResponse (addClient user def) $ getJSON 201) - [alice, bob, charlie, dee] - conv <- - postConversation - alice - defProteus - { qualifiedUsers = [bob, charlie, dee], - team = Just tid - } - >>= getJSON 201 + + (conv, [aliceClient, bobClient, charlieClient, deeClient]) <- case proto of + ConversationProtocolProteus -> do + clients <- + mapM + (\user -> objId $ bindResponse (addClient user def) $ getJSON 201) + [alice, bob, charlie, dee] + conv <- + postConversation + alice + defProteus + { qualifiedUsers = [bob, charlie, dee], + team = Just tid + } + >>= getJSON 201 + pure (conv, clients) + ConversationProtocolMLS -> do + alice1 <- createMLSClient def alice + clients <- traverse (createMLSClient def) [bob, charlie, dee] + traverse_ uploadNewKeyPackage clients + + conv <- postConversation alice1 defMLS {team = Just tid} >>= getJSON 201 + createGroup alice1 conv + + void $ createAddCommit alice1 [bob, charlie, dee] >>= sendAndConsumeCommitBundle + convId <- conv %. "qualified_id" + pure (convId, map (.client) (alice1 : clients)) let update = ["access" .= ([] :: [String]), "access_role" .= ["team_member"]] void $ updateAccess alice conv update >>= getJSON 200 @@ -73,6 +101,8 @@ testAccessUpdateGuestRemoved = do res.status `shouldMatchInt` 200 res.json %. "members.others.0.qualified_id" `shouldMatch` objQidObject bob +-- @END + testAccessUpdateGuestRemovedUnreachableRemotes :: (HasCallStack) => App () testAccessUpdateGuestRemovedUnreachableRemotes = do resourcePool <- asks resourcePool diff --git a/integration/test/Test/Bot.hs b/integration/test/Test/Bot.hs index b635b9e0acd..8cf199ee9db 100644 --- a/integration/test/Test/Bot.hs +++ b/integration/test/Test/Bot.hs @@ -146,7 +146,6 @@ onBotCreate chan _headers _req k = do onBotMessage chan _headers req k = do body <- liftIO $ Wai.strictRequestBody req writeChan chan (BotMessage (cs body)) - liftIO $ putStrLn $ cs body k (responseLBS status200 mempty mempty) onBotAlive _chan _headers _req k = do k (responseLBS status200 mempty (cs "success")) diff --git a/integration/test/Test/Brig.hs b/integration/test/Test/Brig.hs index 4839e36b286..f55fc952b00 100644 --- a/integration/test/Test/Brig.hs +++ b/integration/test/Test/Brig.hs @@ -1,14 +1,20 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# OPTIONS_GHC -Wno-incomplete-patterns #-} + module Test.Brig where -import API.Brig +import API.Brig as BrigP import qualified API.BrigInternal as BrigI import API.Common +import API.GalleyInternal (setTeamFeatureStatus) +import API.Spar import Data.Aeson.Types hiding ((.=)) import Data.List.Split import Data.String.Conversions import qualified Data.UUID as UUID import qualified Data.UUID.V4 as UUID import GHC.Stack +import SAML2.WebSSO.Test.Util (SampleIdP (..), makeSampleIdPMetadata) import SetupHelpers import System.IO.Extra import Testlib.Assertions @@ -229,3 +235,37 @@ testSFTFederation = do maybe (assertFailure "is_federating missing") asBool =<< lookupField resp.json "is_federating" when isFederating $ assertFailure "is_federating should be false" + +testDeleteEmail :: (HasCallStack) => App () +testDeleteEmail = do + (owner, tid, [usr]) <- createTeam OwnDomain 2 + putSelf usr (PutSelf Nothing Nothing (Just "Alice") Nothing) >>= assertSuccess + email <- getSelf usr >>= getJSON 200 >>= (%. "email") >>= asString + + let associateUsrWithSSO :: (HasCallStack) => App () + associateUsrWithSSO = do + void $ setTeamFeatureStatus owner tid "sso" "enabled" + registerTestIdPWithMeta owner >>= assertSuccess + tok <- createScimToken owner >>= getJSON 200 >>= (%. "token") >>= asString + void $ findUsersByExternalId owner tok email + + searchShouldBe :: (HasCallStack) => String -> App () + searchShouldBe expected = do + BrigI.refreshIndex OwnDomain + bindResponse (BrigP.searchTeam owner email) $ \resp -> do + resp.status `shouldMatchInt` 200 + numDocs <- length <$> (resp.json %. "documents" >>= asList) + case expected of + "empty" -> numDocs `shouldMatchInt` 0 + "non-empty" -> numDocs `shouldMatchInt` 1 + + deleteSelfEmail usr >>= assertStatus 403 + searchShouldBe "non-empty" + associateUsrWithSSO + deleteSelfEmail usr >>= assertSuccess + searchShouldBe "empty" + +registerTestIdPWithMeta :: (HasCallStack, MakesValue owner) => owner -> App Response +registerTestIdPWithMeta owner = do + SampleIdP idpmeta _ _ _ <- makeSampleIdPMetadata + createIdp owner idpmeta diff --git a/integration/test/Test/Cargohold/API/Util.hs b/integration/test/Test/Cargohold/API/Util.hs index 8ffb512da7b..dffc4168bbe 100644 --- a/integration/test/Test/Cargohold/API/Util.hs +++ b/integration/test/Test/Cargohold/API/Util.hs @@ -98,16 +98,6 @@ header :: String -> String -> Request -> Request header name value req = req {requestHeaders = (mk $ cs name, cs value) : requestHeaders req} -downloadAssetWithAssetKey :: - (HasCallStack, MakesValue user) => - (HTTP.Request -> HTTP.Request) -> - user -> - String -> - App Response -downloadAssetWithAssetKey r user tok = do - req <- baseRequest user Cargohold (ExplicitVersion 1) $ "assets/v3/" <> tok - submit "GET" $ r $ req & tokenParam tok - class IsAssetToken tok where tokenParam :: tok -> Request -> Request diff --git a/integration/test/Test/Client.hs b/integration/test/Test/Client.hs index d00e8174710..8c3101737dd 100644 --- a/integration/test/Test/Client.hs +++ b/integration/test/Test/Client.hs @@ -4,6 +4,7 @@ module Test.Client where import API.Brig import qualified API.Brig as API +import API.BrigCommon import API.Gundeck import Control.Lens hiding ((.=)) import Control.Monad.Codensity @@ -70,3 +71,53 @@ testListClientsIfBackendIsOffline = do bindResponse (listUsersClients ownUser1 [ownUser1, ownUser2, downUser]) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "qualified_user_map" `shouldMatch` expectedResponse + +testCreateClientWithCapabilities :: App () +testCreateClientWithCapabilities = do + let allCapabilities = ["legalhold-implicit-consent", "consumable-notifications"] + alice <- randomUser OwnDomain def + addClient alice def {acapabilities = Just allCapabilities} `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.json %. "capabilities" `shouldMatchSet` allCapabilities + getSelfClients alice `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "0.capabilities" `shouldMatchSet` allCapabilities + +testUpdateClientWithConsumableNotificationsCapability :: App () +testUpdateClientWithConsumableNotificationsCapability = do + domain <- asString OwnDomain + let consumeCapability = "consumable-notifications" + alice <- randomUser domain def + aliceId <- alice %. "id" & asString + cid <- + addClient alice def {acapabilities = Nothing} `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.json %. "id" & asString + let cli = + ClientIdentity + { domain = domain, + user = aliceId, + client = cid + } + updateClient cli def {capabilities = Just [consumeCapability]} >>= assertSuccess + getSelfClients alice `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "0.capabilities" `shouldMatch` [consumeCapability] + +testGetClientCapabilitiesV6 :: App () +testGetClientCapabilitiesV6 = do + let allCapabilities = ["legalhold-implicit-consent", "consumable-notifications"] + alice <- randomUser OwnDomain def + addClient alice def {acapabilities = Just allCapabilities} `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.json %. "capabilities" `shouldMatchSet` allCapabilities + + getSelfClients alice `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "0.capabilities" `shouldMatchSet` allCapabilities + + -- In API v6 and below, the "capabilities" field is an enum, so having a new + -- value for this enum is a breaking change. + withAPIVersion 6 $ getSelfClients alice `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "0.capabilities.capabilities" `shouldMatchSet` ["legalhold-implicit-consent"] diff --git a/integration/test/Test/Connection.hs b/integration/test/Test/Connection.hs index d12feb41f01..c0468db6a39 100644 --- a/integration/test/Test/Connection.hs +++ b/integration/test/Test/Connection.hs @@ -22,11 +22,15 @@ import API.Galley import Notifications import SetupHelpers import Testlib.Prelude +import Testlib.VersionedFed import UnliftIO.Async (forConcurrently_) -testConnectWithRemoteUser :: (HasCallStack) => Domain -> App () +testConnectWithRemoteUser :: (HasCallStack) => OneOf Domain AnyFedDomain -> App () testConnectWithRemoteUser owningDomain = do - (alice, bob, one2oneId) <- createOne2OneConversation owningDomain + let otherDomain = case owningDomain of + OneOfA OwnDomain -> OtherDomain + _ -> OwnDomain + (alice, bob, one2oneId) <- createOne2OneConversation owningDomain otherDomain aliceId <- alice %. "qualified_id" getConversation alice one2oneId `bindResponse` \resp -> do resp.status `shouldMatchInt` 200 @@ -79,20 +83,21 @@ testRemoteUserGetsDeleted = do pure charlie - charlieUnconnected <- do - randomUser OtherDomain def + charlieUnconnected <- randomUser OtherDomain def - forConcurrently_ [charliePending, charlieConnected, charlieBlocked, charlieUnconnected] \charlie -> do - deleteUser charlie + forConcurrently_ + [charliePending, charlieConnected, charlieBlocked, charlieUnconnected] + \charlie -> do + deleteUser charlie - -- charlie is on their local backend, so asking should be instant - getConnection charlie alice `bindResponse` \resp -> - resp.status `shouldMatchInt` 404 + -- charlie is on their local backend, so asking should be instant + getConnection charlie alice `bindResponse` \resp -> + resp.status `shouldMatchInt` 404 - -- for alice, charlie is on the remote backend, so the status change - -- may not be instant - getConnection alice charlie `waitForResponse` \resp -> - resp.status `shouldMatchInt` 404 + -- for alice, charlie is on the remote backend, so the status change + -- may not be instant + getConnection alice charlie `waitForResponse` \resp -> + resp.status `shouldMatchInt` 404 testInternalGetConStatusesAll :: (HasCallStack) => App () testInternalGetConStatusesAll = @@ -149,9 +154,11 @@ assertConnectionStatus userFrom userTo connStatus = resp.status `shouldMatchInt` 200 resp.json %. "status" `shouldMatch` connStatus -testConnectFromIgnored :: (HasCallStack) => App () -testConnectFromIgnored = do - [alice, bob] <- forM [OwnDomain, OtherDomain] $ flip randomUser def +testConnectFromIgnored :: (HasCallStack) => StaticDomain -> App () +testConnectFromIgnored domain = do + alice <- randomUser OwnDomain def + bob <- randomUser domain def + void $ postConnection bob alice >>= getBody 201 -- set up an initial "ignored" state on Alice's side assertConnectionStatus alice bob "pending" @@ -168,9 +175,11 @@ testConnectFromIgnored = do resp.status `shouldMatchInt` 200 resp.json %. "status" `shouldMatch` "accepted" -testSentFromIgnored :: (HasCallStack) => App () -testSentFromIgnored = do - [alice, bob] <- forM [OwnDomain, OtherDomain] $ flip randomUser def +testSentFromIgnored :: (HasCallStack) => StaticDomain -> App () +testSentFromIgnored domain = do + alice <- randomUser OwnDomain def + bob <- randomUser domain def + -- set up an initial "ignored" state void $ postConnection bob alice >>= getBody 201 void $ putConnection alice bob "ignored" >>= getBody 200 @@ -185,9 +194,9 @@ testSentFromIgnored = do void $ putConnection alice bob "accepted" >>= getBody 200 assertConnectionStatus alice bob "sent" -testConnectFromBlocked :: (HasCallStack) => App () -testConnectFromBlocked = do - (alice, bob, one2oneId) <- createOne2OneConversation OwnDomain +testConnectFromBlocked :: (HasCallStack) => StaticDomain -> App () +testConnectFromBlocked domain = do + (alice, bob, one2oneId) <- createOne2OneConversation OwnDomain domain bobId <- bob %. "qualified_id" -- set up an initial "blocked" state @@ -211,9 +220,11 @@ testConnectFromBlocked = do qIds <- for others (%. "qualified_id") qIds `shouldMatchSet` [bobId] -testSentFromBlocked :: (HasCallStack) => App () -testSentFromBlocked = do - [alice, bob] <- forM [OwnDomain, OtherDomain] $ flip randomUser def +testSentFromBlocked :: (HasCallStack) => StaticDomain -> App () +testSentFromBlocked domain = do + alice <- randomUser OwnDomain def + bob <- randomUser domain def + -- set up an initial "blocked" state void $ postConnection bob alice >>= getBody 201 void $ putConnection alice bob "blocked" >>= getBody 200 @@ -228,9 +239,10 @@ testSentFromBlocked = do void $ putConnection alice bob "accepted" >>= getBody 200 assertConnectionStatus alice bob "sent" -testCancel :: (HasCallStack) => App () -testCancel = do - [alice, bob] <- forM [OwnDomain, OtherDomain] $ flip randomUser def +testCancel :: (HasCallStack) => StaticDomain -> App () +testCancel domain = do + alice <- randomUser OwnDomain def + bob <- randomUser domain def void $ postConnection alice bob >>= getBody 201 assertConnectionStatus alice bob "sent" @@ -238,16 +250,16 @@ testCancel = do void $ putConnection alice bob "cancelled" >>= getBody 200 assertConnectionStatus alice bob "cancelled" -testConnectionLimits :: (HasCallStack) => App () -testConnectionLimits = do +testConnectionLimits :: (HasCallStack) => StaticDomain -> App () +testConnectionLimits domain = do let connectionLimit = 16 alice <- randomUser OwnDomain def [charlie1, charlie2, charlie3, charlie4] <- replicateM 4 do - randomUser OtherDomain def + randomUser domain def -- connect to connectionLimit - 1 many users (charlie5 : _) <- replicateM (connectionLimit - 1) do - charlie <- randomUser OtherDomain def + charlie <- randomUser domain def postConnection alice charlie `bindResponse` \resp -> resp.status `shouldMatchInt` 201 pure charlie diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index f44eb9eea2f..a9ef7595714 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -35,6 +35,7 @@ import SetupHelpers hiding (deleteUser) import Testlib.One2One (generateRemoteAndConvIdWithDomain) import Testlib.Prelude import Testlib.ResourcePool +import Testlib.VersionedFed testDynamicBackendsFullyConnectedWhenAllowAll :: (HasCallStack) => App () testDynamicBackendsFullyConnectedWhenAllowAll = do @@ -113,10 +114,10 @@ testDynamicBackendsNotFullyConnected = do resp.json %. "status" `shouldMatch` "non-fully-connected" resp.json %. "not_connected" `shouldMatchSet` [domainB, domainC] -testFederationStatus :: (HasCallStack) => App () -testFederationStatus = do +testFederationStatus :: (HasCallStack) => StaticDomain -> App () +testFederationStatus domain = do uid <- randomUser OwnDomain def {BrigI.team = True} - federatingRemoteDomain <- asString OtherDomain + federatingRemoteDomain <- asString domain let invalidDomain = "c.example.com" -- Does not have any srv records bindResponse (getFederationStatus uid []) @@ -261,12 +262,12 @@ testAddMemberV1 domain = do users <- resp.json %. "data.users" >>= asList traverse (%. "qualified_id") users `shouldMatchSet` [bobId] -testConvWithUnreachableRemoteUsers :: (HasCallStack) => App () -testConvWithUnreachableRemoteUsers = do +testConvWithUnreachableRemoteUsers :: (HasCallStack) => StaticDomain -> App () +testConvWithUnreachableRemoteUsers domain = do ([alice, alex, bob, charlie, dylan], domains) <- startDynamicBackends [def, def] $ \domains -> do own <- make OwnDomain & asString - other <- make OtherDomain & asString + other <- make domain & asString users@(alice : others) <- createUsers $ [own, own, other] <> domains forM_ others $ connectTwoUsers alice pure (users, domains) @@ -280,11 +281,11 @@ testConvWithUnreachableRemoteUsers = do regConvs <- filterM (\c -> (==) <$> (c %. "type" & asInt) <*> pure 0) convs regConvs `shouldMatch` ([] :: [Value]) -testAddUserWithUnreachableRemoteUsers :: (HasCallStack) => App () -testAddUserWithUnreachableRemoteUsers = do +testAddUserWithUnreachableRemoteUsers :: (HasCallStack) => StaticDomain -> App () +testAddUserWithUnreachableRemoteUsers domain = do resourcePool <- asks resourcePool own <- make OwnDomain & asString - other <- make OtherDomain & asString + other <- make domain & asString runCodensity (acquireResources 1 resourcePool) $ \[cDom] -> do ([alex, bobId, bradId, chrisId], conv) <- runCodensity (startDynamicBackend cDom mempty) $ \_ -> do [alice, alex, bob, brad, charlie, chris] <- @@ -302,7 +303,7 @@ testAddUserWithUnreachableRemoteUsers = do runCodensity (startDynamicBackend cDom mempty) $ \_ -> void $ addMembers alex conv def {users = [bobId]} >>= getBody 200 - -- even though backend C is unreachable, we know B/OtherDomain and C + -- even though backend C is unreachable, we know B/domain and C -- federate because Bob joined when C was reachable, hence it is OK to add -- brad from B to the conversation. void $ addMembers alex conv def {users = [bradId]} >>= getBody 200 @@ -312,13 +313,13 @@ testAddUserWithUnreachableRemoteUsers = do resp.status `shouldMatchInt` 533 resp.jsonBody %. "unreachable_backends" `shouldMatchSet` [cDom.berDomain] -testAddUnreachableUserFromFederatingBackend :: (HasCallStack) => App () -testAddUnreachableUserFromFederatingBackend = do +testAddUnreachableUserFromFederatingBackend :: (HasCallStack) => StaticDomain -> App () +testAddUnreachableUserFromFederatingBackend domain = do resourcePool <- asks resourcePool runCodensity (acquireResources 1 resourcePool) $ \[cDom] -> do (alice, chadId, conv) <- runCodensity (startDynamicBackend cDom mempty) $ \_ -> do ownDomain <- make OwnDomain & asString - otherDomain <- make OtherDomain & asString + otherDomain <- make domain & asString [alice, bob, charlie, chad] <- createAndConnectUsers [ownDomain, otherDomain, cDom.berDomain, cDom.berDomain] @@ -355,11 +356,11 @@ testAddUnreachable = do -- need to be reachable so we can check that the graph for those domains is fully connected. resp.json %. "unreachable_backends" `shouldMatchSet` [charlieDomain, dylanDomain] -testGetOneOnOneConvInStatusSentFromRemote :: App () -testGetOneOnOneConvInStatusSentFromRemote = do +testGetOneOnOneConvInStatusSentFromRemote :: (HasCallStack) => StaticDomain -> App () +testGetOneOnOneConvInStatusSentFromRemote domain = do d1User <- randomUser OwnDomain def let shouldBeLocal = True - (d2Usr, d2ConvId) <- generateRemoteAndConvIdWithDomain OtherDomain (not shouldBeLocal) d1User + (d2Usr, d2ConvId) <- generateRemoteAndConvIdWithDomain domain (not shouldBeLocal) d1User bindResponse (postConnection d1User d2Usr) $ \r -> do r.status `shouldMatchInt` 201 r.json %. "status" `shouldMatch` "sent" @@ -373,8 +374,8 @@ testGetOneOnOneConvInStatusSentFromRemote = do resp <- getConversation d1User d2ConvId resp.status `shouldMatchInt` 200 -testAddingUserNonFullyConnectedFederation :: (HasCallStack) => App () -testAddingUserNonFullyConnectedFederation = do +testAddingUserNonFullyConnectedFederation :: (HasCallStack) => StaticDomain -> App () +testAddingUserNonFullyConnectedFederation domain = do let overrides = def { brigCfg = @@ -382,7 +383,7 @@ testAddingUserNonFullyConnectedFederation = do } startDynamicBackends [overrides] $ \[dynBackend] -> do own <- asString OwnDomain - other <- asString OtherDomain + other <- asString domain -- Ensure that dynamic backend only federates with own domain, but not other -- domain. @@ -484,10 +485,12 @@ testAddUserWhenOtherBackendOffline = do bindResponse (addMembers alice conv def {users = [alex]}) $ \resp -> do resp.status `shouldMatchInt` 200 -testSynchroniseUserRemovalNotification :: (HasCallStack) => App () -testSynchroniseUserRemovalNotification = do +testSynchroniseUserRemovalNotification :: (HasCallStack) => StaticDomain -> App () +testSynchroniseUserRemovalNotification domain = do resourcePool <- asks resourcePool - [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + ownDomain <- make OwnDomain + otherDomain <- make domain + [alice, bob] <- createAndConnectUsers [ownDomain, otherDomain] runCodensity (acquireResources 1 resourcePool) $ \[dynBackend] -> do (conv, charlie, client) <- runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do @@ -664,7 +667,7 @@ testDeleteTeamMemberLimitedEventFanout :: (HasCallStack) => App () testDeleteTeamMemberLimitedEventFanout = do -- Alex will get removed from the team (alice, team, [alex, alison]) <- createTeam OwnDomain 3 - ana <- createTeamMemberWithRole alice team "admin" + ana <- createTeamMember alice def {role = "admin"} [amy, bob] <- for [OwnDomain, OtherDomain] $ flip randomUser def forM_ [amy, bob] $ connectTwoUsers alice [aliceId, alexId, amyId, alisonId, anaId, bobId] <- do @@ -851,10 +854,10 @@ testGuestLinksExpired = do bindResponse (getJoinCodeConv tm k v) $ \resp -> do resp.status `shouldMatchInt` 404 -testConversationWithFedV0 :: (HasCallStack) => App () -testConversationWithFedV0 = do +testConversationWithLegacyFed :: (HasCallStack) => AnyFedDomain -> App () +testConversationWithLegacyFed domain = do alice <- randomUser OwnDomain def - bob <- randomUser FedV0Domain def + bob <- randomUser domain def withAPIVersion 4 $ connectTwoUsers alice bob conv <- @@ -871,3 +874,62 @@ testConversationWithoutFederation = withModifiedBackend $ \domain -> do [alice, bob] <- createAndConnectUsers [domain, domain] void $ postConversation alice (defProteus {qualifiedUsers = [bob]}) >>= getJSON 201 + +testPostConvWithUnreachableRemoteUsers :: App () +testPostConvWithUnreachableRemoteUsers = do + [alice, alex] <- createAndConnectUsers [OwnDomain, OtherDomain] + resourcePool <- asks resourcePool + runCodensity (acquireResources 2 resourcePool) $ \[unreachableBackend, reachableBackend] -> do + runCodensity (startDynamicBackend reachableBackend mempty) $ \_ -> do + unreachableUsers <- runCodensity (startDynamicBackend unreachableBackend mempty) $ \_ -> do + let downDomain = unreachableBackend.berDomain + ownDomain <- asString OwnDomain + otherDomain <- asString OtherDomain + void $ BrigI.createFedConn downDomain (BrigI.FedConn ownDomain "full_search" Nothing) + void $ BrigI.createFedConn downDomain (BrigI.FedConn otherDomain "full_search" Nothing) + users <- replicateM 3 (randomUser downDomain def) + for_ users $ \user -> do + connectUsers [alice, user] + connectUsers [alex, user] + -- creating the conv here would work. + pure users + + reachableUsers <- replicateM 2 (randomUser reachableBackend.berDomain def) + for_ reachableUsers $ \user -> do + connectUsers [alice, user] + connectUsers [alex, user] + + withWebSockets [alice, alex] $ \[wssAlice, wssAlex] -> do + -- unreachableBackend is still allocated, but the backend is down. creating the conv here doesn't work. + let payload = defProteus {name = Just "some chat", qualifiedUsers = [alex] <> reachableUsers <> unreachableUsers} + postConversation alice payload >>= assertStatus 533 + + convs <- getAllConvs alice + for_ convs $ \conv -> conv %. "type" `shouldNotMatchInt` 0 + assertNoEvent 2 wssAlice + assertNoEvent 2 wssAlex + +testNoFederationWithProteus :: (HasCallStack) => App () +testNoFederationWithProteus = do + withModifiedBackend + ( def + { galleyCfg = \conf -> + conf & setField "settings.federationProtocols" ["mls"] + } + ) + $ \domain -> do + charlieDomain <- asString $ make OwnDomain + [alice, alex, arnold, bob] <- createAndConnectUsers [domain, domain, domain, charlieDomain] + + do + conv <- postConversation alice defProteus {qualifiedUsers = [alex]} >>= getJSON 201 + bindResponse (addMembers alice conv def {users = [bob]}) $ \resp -> do + resp.status `shouldMatchInt` 409 + resp.json %. "label" `shouldMatch` "federation-disabled-for-protocol" + void $ addMembers alice conv def {users = [arnold]} >>= getJSON 200 + + bindResponse (postConversation alice defProteus {qualifiedUsers = [bob]}) $ \resp -> do + resp.status `shouldMatchInt` 409 + resp.json %. "label" `shouldMatch` "federation-disabled-for-protocol" + + void $ postConversation bob defProteus {qualifiedUsers = [alice]} >>= getJSON 201 diff --git a/integration/test/Test/Demo.hs b/integration/test/Test/Demo.hs index 85f67354f3c..376e5fd3258 100644 --- a/integration/test/Test/Demo.hs +++ b/integration/test/Test/Demo.hs @@ -10,6 +10,7 @@ import qualified API.Nginz as Nginz import GHC.Stack import SetupHelpers import Testlib.Prelude +import Testlib.VersionedFed -- | Deleting unknown clients should fail with 404. testDeleteUnknownClient :: (HasCallStack) => App () @@ -194,15 +195,15 @@ testUnrace = do -} retryT $ True `shouldMatch` True -testFedV0Instance :: (HasCallStack) => App () -testFedV0Instance = do - res <- BrigP.getAPIVersion FedV0Domain >>= getJSON 200 - res %. "domain" `shouldMatch` FedV0Domain +testLegacyFedInstance :: (HasCallStack) => AnyFedDomain -> App () +testLegacyFedInstance domain = do + res <- BrigP.getAPIVersion domain >>= getJSON 200 + res %. "domain" `shouldMatch` domain -testFedV0Federation :: (HasCallStack) => App () -testFedV0Federation = do +testLegacyFedFederation :: (HasCallStack) => AnyFedDomain -> App () +testLegacyFedFederation domain = do alice <- randomUser OwnDomain def - bob <- randomUser FedV0Domain def + bob <- randomUser domain def bob' <- BrigP.getUser alice bob >>= getJSON 200 bob' %. "qualified_id" `shouldMatch` (bob %. "qualified_id") diff --git a/integration/test/Test/ExternalPartner.hs b/integration/test/Test/ExternalPartner.hs index ae6381f4187..01bdd629834 100644 --- a/integration/test/Test/ExternalPartner.hs +++ b/integration/test/Test/ExternalPartner.hs @@ -29,7 +29,7 @@ testExternalPartnerPermissions :: (HasCallStack) => App () testExternalPartnerPermissions = do (owner, tid, u1 : u2 : u3 : _) <- createTeam OwnDomain 4 - partner <- createTeamMemberWithRole owner tid "partner" + partner <- createTeamMember owner def {role = "partner"} -- a partner should not be able to create conversation with 2 additional users or more void $ postConversation partner (defProteus {team = Just tid, qualifiedUsers = [u1, u2]}) >>= getJSON 403 @@ -58,23 +58,23 @@ testExternalPartnerPermissions = do testExternalPartnerPermissionsMls :: (HasCallStack) => App () testExternalPartnerPermissionsMls = do -- external partners should not be able to create (MLS) conversations - (owner, tid, _) <- createTeam OwnDomain 2 - bobExt <- createTeamMemberWithRole owner tid "partner" + (owner, _, _) <- createTeam OwnDomain 2 + bobExt <- createTeamMember owner def {role = "partner"} bobExtClient <- createMLSClient def bobExt bindResponse (postConversation bobExtClient defMLS) $ \resp -> do resp.status `shouldMatchInt` 403 testExternalPartnerPermissionMlsOne2One :: (HasCallStack) => App () testExternalPartnerPermissionMlsOne2One = do - (owner, tid, alice : _) <- createTeam OwnDomain 2 - bobExternal <- createTeamMemberWithRole owner tid "partner" + (owner, _, alice : _) <- createTeam OwnDomain 2 + bobExternal <- createTeamMember owner def {role = "partner"} void $ getMLSOne2OneConversation alice bobExternal >>= getJSON 200 testExternalPartnerPermissionsConvName :: (HasCallStack) => App () testExternalPartnerPermissionsConvName = do (owner, tid, u1 : _) <- createTeam OwnDomain 2 - partner <- createTeamMemberWithRole owner tid "partner" + partner <- createTeamMember owner def {role = "partner"} conv <- postConversation partner (defProteus {team = Just tid, qualifiedUsers = [u1]}) >>= getJSON 201 diff --git a/integration/test/Test/FeatureFlags.hs b/integration/test/Test/FeatureFlags.hs index e0943931f9e..e1ecdae4da2 100644 --- a/integration/test/Test/FeatureFlags.hs +++ b/integration/test/Test/FeatureFlags.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2023 Wire Swiss GmbH @@ -19,19 +21,13 @@ module Test.FeatureFlags where import qualified API.Galley as Public import qualified API.GalleyInternal as Internal -import Control.Concurrent (threadDelay) -import Control.Monad.Codensity (Codensity (runCodensity)) -import Control.Monad.Reader import qualified Data.Aeson as A import qualified Data.Aeson.Key as A import qualified Data.Aeson.KeyMap as KM import qualified Data.Set as Set -import Data.String.Conversions (cs) -import Notifications import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -import Testlib.ResourcePool (acquireResources) testLimitedEventFanout :: (HasCallStack) => App () testLimitedEventFanout = do @@ -46,443 +42,14 @@ testLimitedEventFanout = do resp.status `shouldMatchInt` 200 resp.json %. "status" `shouldMatch` "enabled" -testLegalholdDisabledByDefault :: (HasCallStack) => App () -testLegalholdDisabledByDefault = do - let put uid tid st = Internal.setTeamFeatureConfig uid tid "legalhold" (object ["status" .= st]) >>= assertSuccess - let patch uid tid st = Internal.setTeamFeatureStatus uid tid "legalhold" st >>= assertSuccess - forM_ [put, patch] $ \setFeatureStatus -> do - withModifiedBackend - def {galleyCfg = setField "settings.featureFlags.legalhold" "disabled-by-default"} - $ \domain -> do - (owner, tid, m : _) <- createTeam domain 2 - nonMember <- randomUser domain def - assertForbidden =<< Public.getTeamFeature nonMember tid "legalhold" - -- Test default - checkFeature "legalhold" m tid disabled - -- Test override - setFeatureStatus owner tid "enabled" - checkFeature "legalhold" owner tid enabled - setFeatureStatus owner tid "disabled" - checkFeature "legalhold" owner tid disabled - --- always disabled -testLegalholdDisabledPermanently :: (HasCallStack) => App () -testLegalholdDisabledPermanently = do - let cfgLhDisabledPermanently = - def - { galleyCfg = setField "settings.featureFlags.legalhold" "disabled-permanently" - } - cfgLhDisabledByDefault = - def - { galleyCfg = setField "settings.featureFlags.legalhold" "disabled-by-default" - } - resourcePool <- asks (.resourcePool) - runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do - let domain = testBackend.berDomain - - -- Happy case: DB has no config for the team - runCodensity (startDynamicBackend testBackend cfgLhDisabledPermanently) $ \_ -> do - (owner, tid, _) <- createTeam domain 1 - checkFeature "legalhold" owner tid disabled - assertStatus 403 =<< Internal.setTeamFeatureStatus domain tid "legalhold" "enabled" - assertStatus 403 =<< Internal.setTeamFeatureConfig domain tid "legalhold" (object ["status" .= "enabled"]) - - -- Interesting case: The team had LH enabled before backend config was - -- changed to disabled-permanently - (owner, tid) <- runCodensity (startDynamicBackend testBackend cfgLhDisabledByDefault) $ \_ -> do - (owner, tid, _) <- createTeam domain 1 - checkFeature "legalhold" owner tid disabled - assertSuccess =<< Internal.setTeamFeatureStatus domain tid "legalhold" "enabled" - checkFeature "legalhold" owner tid enabled - pure (owner, tid) - - runCodensity (startDynamicBackend testBackend cfgLhDisabledPermanently) $ \_ -> do - checkFeature "legalhold" owner tid disabled - --- enabled if team is allow listed, disabled in any other case -testLegalholdWhitelistTeamsAndImplicitConsent :: (HasCallStack) => App () -testLegalholdWhitelistTeamsAndImplicitConsent = do - let cfgLhWhitelistTeamsAndImplicitConsent = - def - { galleyCfg = setField "settings.featureFlags.legalhold" "whitelist-teams-and-implicit-consent" - } - cfgLhDisabledByDefault = - def - { galleyCfg = setField "settings.featureFlags.legalhold" "disabled-by-default" - } - resourcePool <- asks (.resourcePool) - runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do - let domain = testBackend.berDomain - - -- Happy case: DB has no config for the team - (owner, tid) <- runCodensity (startDynamicBackend testBackend cfgLhWhitelistTeamsAndImplicitConsent) $ \_ -> do - (owner, tid, _) <- createTeam domain 1 - checkFeature "legalhold" owner tid disabled - Internal.legalholdWhitelistTeam tid owner >>= assertSuccess - checkFeature "legalhold" owner tid enabled - - -- Disabling it doesn't work - assertStatus 403 =<< Internal.setTeamFeatureStatus domain tid "legalhold" "disabled" - assertStatus 403 =<< Internal.setTeamFeatureConfig domain tid "legalhold" (object ["status" .= "disabled"]) - checkFeature "legalhold" owner tid enabled - pure (owner, tid) - - -- Interesting case: The team had LH disabled before backend config was - -- changed to "whitelist-teams-and-implicit-consent". It should still show - -- enabled when the config gets changed. - runCodensity (startDynamicBackend testBackend cfgLhDisabledByDefault) $ \_ -> do - checkFeature "legalhold" owner tid disabled - assertSuccess =<< Internal.setTeamFeatureStatus domain tid "legalhold" "disabled" - checkFeature "legalhold" owner tid disabled - - runCodensity (startDynamicBackend testBackend cfgLhWhitelistTeamsAndImplicitConsent) $ \_ -> do - checkFeature "legalhold" owner tid enabled - -testExposeInvitationURLsToTeamAdminConfig :: (HasCallStack) => App () -testExposeInvitationURLsToTeamAdminConfig = do - let cfgExposeInvitationURLsTeamAllowlist tids = - def - { galleyCfg = setField "settings.exposeInvitationURLsTeamAllowlist" tids - } - resourcePool <- asks (.resourcePool) - runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do - let domain = testBackend.berDomain - - let testNoAllowlistEntry = runCodensity (startDynamicBackend testBackend $ cfgExposeInvitationURLsTeamAllowlist ([] :: [String])) $ \_ -> do - (owner, tid, _) <- createTeam domain 1 - checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabledLocked - -- here we get a response with HTTP status 200 and feature status unchanged (disabled), which we find weird, but we're just testing the current behavior - -- a team that is not in the allow list cannot enable the feature, it will always be disabled and locked - -- even though the internal API request to enable it succeeds - assertSuccess =<< Internal.setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "enabled" - checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabledLocked - -- however, a request to the public API will fail - assertStatus 409 =<< Public.setTeamFeatureConfig owner tid "exposeInvitationURLsToTeamAdmin" (object ["status" .= "enabled"]) - assertSuccess =<< Internal.setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "disabled" - pure (owner, tid) - - -- Happy case: DB has no config for the team - (owner, tid) <- testNoAllowlistEntry - - -- Interesting case: The team is in the allow list - runCodensity (startDynamicBackend testBackend $ cfgExposeInvitationURLsTeamAllowlist [tid]) $ \_ -> do - -- when the team is in the allow list the lock status is implicitly unlocked - checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabled - assertSuccess =<< Internal.setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "enabled" - checkFeature "exposeInvitationURLsToTeamAdmin" owner tid enabled - assertSuccess =<< Internal.setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "disabled" - checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabled - assertSuccess =<< Internal.setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "enabled" - checkFeature "exposeInvitationURLsToTeamAdmin" owner tid enabled - - -- Interesting case: The team had the feature enabled but is not in allow list - void testNoAllowlistEntry - -testMlsE2EConfigCrlProxyRequired :: (HasCallStack) => App () -testMlsE2EConfigCrlProxyRequired = do - (owner, tid, _) <- createTeam OwnDomain 1 - let configWithoutCrlProxy = - object - [ "config" - .= object - [ "useProxyOnMobile" .= False, - "verificationExpiration" .= A.Number 86400 - ], - "status" .= "enabled" - ] - - -- From API version 6 onwards, the CRL proxy is required, so the request should fail when it's not provided - bindResponse (Public.setTeamFeatureConfig owner tid "mlsE2EId" configWithoutCrlProxy) $ \resp -> do - resp.status `shouldMatchInt` 400 - resp.json %. "label" `shouldMatch` "mls-e2eid-missing-crl-proxy" - - configWithCrlProxy <- - configWithoutCrlProxy - & setField "config.useProxyOnMobile" True - & setField "config.crlProxy" "https://crl-proxy.example.com" - & setField "status" "enabled" - - -- The request should succeed when the CRL proxy is provided - bindResponse (Public.setTeamFeatureConfig owner tid "mlsE2EId" configWithCrlProxy) $ \resp -> do - resp.status `shouldMatchInt` 200 - - -- Assert that the feature config got updated correctly - expectedResponse <- configWithCrlProxy & setField "lockStatus" "unlocked" & setField "ttl" "unlimited" - checkFeature "mlsE2EId" owner tid expectedResponse - -testMlsE2EConfigCrlProxyNotRequiredInV5 :: (HasCallStack) => App () -testMlsE2EConfigCrlProxyNotRequiredInV5 = do - (owner, tid, _) <- createTeam OwnDomain 1 - let configWithoutCrlProxy = - object - [ "config" - .= object - [ "useProxyOnMobile" .= False, - "verificationExpiration" .= A.Number 86400 - ], - "status" .= "enabled" - ] - - -- In API version 5, the CRL proxy is not required, so the request should succeed - bindResponse (Public.setTeamFeatureConfigVersioned (ExplicitVersion 5) owner tid "mlsE2EId" configWithoutCrlProxy) $ \resp -> do - resp.status `shouldMatchInt` 200 - - -- Assert that the feature config got updated correctly - expectedResponse <- configWithoutCrlProxy & setField "lockStatus" "unlocked" & setField "ttl" "unlimited" - checkFeature "mlsE2EId" owner tid expectedResponse - -testSSODisabledByDefault :: (HasCallStack) => App () -testSSODisabledByDefault = do - let put uid tid = Internal.setTeamFeatureConfig uid tid "sso" (object ["status" .= "enabled"]) >>= assertSuccess - let patch uid tid = Internal.setTeamFeatureStatus uid tid "sso" "enabled" >>= assertSuccess - forM_ [put, patch] $ \enableFeature -> do - withModifiedBackend - def {galleyCfg = setField "settings.featureFlags.sso" "disabled-by-default"} - $ \domain -> do - (owner, tid, m : _) <- createTeam domain 2 - nonMember <- randomUser domain def - assertForbidden =<< Public.getTeamFeature nonMember tid "sso" - -- Test default - checkFeature "sso" m tid disabled - -- Test override - enableFeature owner tid - checkFeature "sso" owner tid enabled - -testSSOEnabledByDefault :: (HasCallStack) => App () -testSSOEnabledByDefault = do - withModifiedBackend - def {galleyCfg = setField "settings.featureFlags.sso" "enabled-by-default"} - $ \domain -> do - (owner, tid, _m : _) <- createTeam domain 2 - nonMember <- randomUser domain def - assertForbidden =<< Public.getTeamFeature nonMember tid "sso" - checkFeature "sso" owner tid enabled - -- check that the feature cannot be disabled - assertLabel 403 "not-implemented" =<< Internal.setTeamFeatureConfig owner tid "sso" (object ["status" .= "disabled"]) - -testSearchVisibilityDisabledByDefault :: (HasCallStack) => App () -testSearchVisibilityDisabledByDefault = do - withModifiedBackend def {galleyCfg = setField "settings.featureFlags.teamSearchVisibility" "disabled-by-default"} $ \domain -> do - (owner, tid, m : _) <- createTeam domain 2 - nonMember <- randomUser domain def - assertForbidden =<< Public.getTeamFeature nonMember tid "searchVisibility" - -- Test default - checkFeature "searchVisibility" m tid disabled - assertSuccess =<< Internal.setTeamFeatureStatus owner tid "searchVisibility" "enabled" - checkFeature "searchVisibility" owner tid enabled - assertSuccess =<< Internal.setTeamFeatureStatus owner tid "searchVisibility" "disabled" - checkFeature "searchVisibility" owner tid disabled - -testSearchVisibilityEnabledByDefault :: (HasCallStack) => App () -testSearchVisibilityEnabledByDefault = do - withModifiedBackend def {galleyCfg = setField "settings.featureFlags.teamSearchVisibility" "enabled-by-default"} $ \domain -> do - (owner, tid, m : _) <- createTeam domain 2 - nonMember <- randomUser domain def - assertForbidden =<< Public.getTeamFeature nonMember tid "searchVisibility" - -- Test default - checkFeature "searchVisibility" m tid enabled - assertSuccess =<< Internal.setTeamFeatureStatus owner tid "searchVisibility" "disabled" - checkFeature "searchVisibility" owner tid disabled - assertSuccess =<< Internal.setTeamFeatureStatus owner tid "searchVisibility" "enabled" - checkFeature "searchVisibility" owner tid enabled - -testSearchVisibilityInbound :: (HasCallStack) => App () -testSearchVisibilityInbound = _testSimpleFlag "searchVisibilityInbound" Public.setTeamFeatureConfig False - -testDigitalSignaturesInternal :: (HasCallStack) => App () -testDigitalSignaturesInternal = _testSimpleFlag "digitalSignatures" Internal.setTeamFeatureConfig False - -testValidateSAMLEmailsInternal :: (HasCallStack) => App () -testValidateSAMLEmailsInternal = _testSimpleFlag "validateSAMLemails" Internal.setTeamFeatureConfig True - -testConferenceCallingInternal :: (HasCallStack) => App () -testConferenceCallingInternal = _testSimpleFlag "conferenceCalling" Internal.setTeamFeatureConfig True - -testSearchVisibilityInboundInternal :: (HasCallStack) => App () -testSearchVisibilityInboundInternal = _testSimpleFlag "searchVisibilityInbound" Internal.setTeamFeatureConfig False - -_testSimpleFlag :: (HasCallStack) => String -> (Value -> String -> String -> Value -> App Response) -> Bool -> App () -_testSimpleFlag featureName setFeatureConfig featureEnabledByDefault = do - let defaultStatus = if featureEnabledByDefault then "enabled" else "disabled" - let defaultValue = if featureEnabledByDefault then enabled else disabled - let otherStatus = if featureEnabledByDefault then "disabled" else "enabled" - let otherValue = if featureEnabledByDefault then disabled else enabled - - (owner, tid, m : _) <- createTeam OwnDomain 2 - nonTeamMember <- randomUser OwnDomain def - assertForbidden =<< Public.getTeamFeature nonTeamMember tid featureName - checkFeature featureName m tid defaultValue - -- should receive an event - void $ withWebSockets [m] $ \wss -> do - assertSuccess =<< setFeatureConfig owner tid featureName (object ["status" .= otherStatus]) - for_ wss $ \ws -> do - notif <- awaitMatch isFeatureConfigUpdateNotif ws - notif %. "payload.0.name" `shouldMatch` featureName - notif %. "payload.0.data" `shouldMatch` otherValue - - checkFeature featureName m tid otherValue - assertSuccess =<< setFeatureConfig owner tid featureName (object ["status" .= defaultStatus]) - for_ wss $ \ws -> do - notif <- awaitMatch isFeatureConfigUpdateNotif ws - notif %. "payload.0.name" `shouldMatch` featureName - notif %. "payload.0.data" `shouldMatch` defaultValue - checkFeature featureName m tid defaultValue - -testConversationGuestLinks :: (HasCallStack) => App () -testConversationGuestLinks = _testSimpleFlagWithLockStatus "conversationGuestLinks" Public.setTeamFeatureConfig True True - -testFileSharing :: (HasCallStack) => App () -testFileSharing = _testSimpleFlagWithLockStatus "fileSharing" Public.setTeamFeatureConfig True True - -testSndFactorPasswordChallenge :: (HasCallStack) => App () -testSndFactorPasswordChallenge = _testSimpleFlagWithLockStatus "sndFactorPasswordChallenge" Public.setTeamFeatureConfig False False - -testOutlookCalIntegration :: (HasCallStack) => App () -testOutlookCalIntegration = _testSimpleFlagWithLockStatus "outlookCalIntegration" Public.setTeamFeatureConfig False False - -testConversationGuestLinksInternal :: (HasCallStack) => App () -testConversationGuestLinksInternal = _testSimpleFlagWithLockStatus "conversationGuestLinks" Internal.setTeamFeatureConfig True True - -testFileSharingInternal :: (HasCallStack) => App () -testFileSharingInternal = _testSimpleFlagWithLockStatus "fileSharing" Internal.setTeamFeatureConfig True True - -testSndFactorPasswordChallengeInternal :: (HasCallStack) => App () -testSndFactorPasswordChallengeInternal = _testSimpleFlagWithLockStatus "sndFactorPasswordChallenge" Internal.setTeamFeatureConfig False False - -testOutlookCalIntegrationInternal :: (HasCallStack) => App () -testOutlookCalIntegrationInternal = _testSimpleFlagWithLockStatus "outlookCalIntegration" Internal.setTeamFeatureConfig False False - -_testSimpleFlagWithLockStatus :: - (HasCallStack) => - String -> - (Value -> String -> String -> Value -> App Response) -> - Bool -> - Bool -> - App () -_testSimpleFlagWithLockStatus featureName setFeatureConfig featureEnabledByDefault featureUnlockedByDefault = do - -- let defaultStatus = if featureEnabledByDefault then "enabled" else "disabled" - defaultValue <- (if featureEnabledByDefault then enabled else disabled) & setField "lockStatus" (if featureUnlockedByDefault then "unlocked" else "locked") - let thisStatus = if featureEnabledByDefault then "enabled" else "disabled" - let otherStatus = if featureEnabledByDefault then "disabled" else "enabled" - - (owner, tid, m : _) <- createTeam OwnDomain 2 - nonTeamMember <- randomUser OwnDomain def - assertForbidden =<< Public.getTeamFeature nonTeamMember tid featureName - - checkFeature featureName m tid defaultValue - - -- unlock feature if it is locked - unless featureUnlockedByDefault $ Internal.setTeamFeatureLockStatus OwnDomain tid featureName "unlocked" - - -- change the status - let otherValue = if featureEnabledByDefault then disabled else enabled - void $ withWebSockets [m] $ \wss -> do - assertSuccess =<< setFeatureConfig owner tid featureName (object ["status" .= otherStatus]) - for_ wss $ \ws -> do - notif <- awaitMatch isFeatureConfigUpdateNotif ws - notif %. "payload.0.name" `shouldMatch` featureName - notif %. "payload.0.data" `shouldMatch` otherValue - - checkFeature featureName m tid otherValue - - bindResponse (setFeatureConfig owner tid featureName (object ["status" .= thisStatus])) $ \resp -> do - resp.status `shouldMatchInt` 200 - checkFeature featureName m tid (object ["status" .= thisStatus, "lockStatus" .= "unlocked", "ttl" .= "unlimited"]) - - bindResponse (setFeatureConfig owner tid featureName (object ["status" .= otherStatus])) $ \resp -> do - resp.status `shouldMatchInt` 200 - checkFeature featureName m tid (object ["status" .= otherStatus, "lockStatus" .= "unlocked", "ttl" .= "unlimited"]) - - -- lock feature - Internal.setTeamFeatureLockStatus OwnDomain tid featureName "locked" - - -- feature status should be the default again - checkFeature featureName m tid =<< setField "lockStatus" "locked" defaultValue - assertStatus 409 =<< setFeatureConfig owner tid featureName (object ["status" .= otherStatus]) - - -- unlock again - Internal.setTeamFeatureLockStatus OwnDomain tid featureName "unlocked" - - -- feature status should be the previously set status again - checkFeature featureName m tid =<< setField "lockStatus" "unlocked" otherValue - -testClassifiedDomainsEnabled :: (HasCallStack) => App () -testClassifiedDomainsEnabled = do - (_, tid, m : _) <- createTeam OwnDomain 2 - expected <- enabled & setField "config.domains" ["example.com"] - checkFeature "classifiedDomains" m tid expected - -testClassifiedDomainsDisabled :: (HasCallStack) => App () -testClassifiedDomainsDisabled = do - withModifiedBackend def {galleyCfg = setField "settings.featureFlags.classifiedDomains" (object ["status" .= "disabled", "config" .= object ["domains" .= ["example.com"]]])} $ \domain -> do - (_, tid, m : _) <- createTeam domain 2 - expected <- disabled & setField "config.domains" ["example.com"] - checkFeature "classifiedDomains" m tid expected - -- | Call 'GET /teams/:tid/features' and 'GET /feature-configs', and check if all -- features are there. testAllFeatures :: (HasCallStack) => App () testAllFeatures = do (_, tid, m : _) <- createTeam OwnDomain 2 - let expected = - object - $ [ "legalhold" .= disabled, - "sso" .= disabled, - "searchVisibility" .= disabled, - "validateSAMLemails" .= enabled, - "digitalSignatures" .= disabled, - "appLock" .= object ["lockStatus" .= "unlocked", "status" .= "enabled", "ttl" .= "unlimited", "config" .= object ["enforceAppLock" .= False, "inactivityTimeoutSecs" .= A.Number 60]], - "fileSharing" .= enabled, - "classifiedDomains" .= object ["lockStatus" .= "unlocked", "status" .= "enabled", "ttl" .= "unlimited", "config" .= object ["domains" .= ["example.com"]]], - "conferenceCalling" .= enabled, - "selfDeletingMessages" .= object ["lockStatus" .= "unlocked", "status" .= "enabled", "ttl" .= "unlimited", "config" .= object ["enforcedTimeoutSeconds" .= A.Number 0]], - "conversationGuestLinks" .= enabled, - "sndFactorPasswordChallenge" .= disabledLocked, - "mls" - .= object - [ "lockStatus" .= "unlocked", - "status" .= "disabled", - "ttl" .= "unlimited", - "config" - .= object - [ "protocolToggleUsers" .= ([] :: [String]), - "defaultProtocol" .= "proteus", - "supportedProtocols" .= ["proteus", "mls"], - "allowedCipherSuites" .= ([1] :: [Int]), - "defaultCipherSuite" .= A.Number 1 - ] - ], - "searchVisibilityInbound" .= disabled, - "exposeInvitationURLsToTeamAdmin" .= disabledLocked, - "outlookCalIntegration" .= disabledLocked, - "mlsE2EId" - .= object - [ "lockStatus" .= "unlocked", - "status" .= "disabled", - "ttl" .= "unlimited", - "config" - .= object - [ "verificationExpiration" .= A.Number 86400, - "useProxyOnMobile" .= False - ] - ], - "mlsMigration" - .= object - [ "lockStatus" .= "locked", - "status" .= "enabled", - "ttl" .= "unlimited", - "config" - .= object - [ "startTime" .= "2029-05-16T10:11:12.123Z", - "finaliseRegardlessAfter" .= "2029-10-17T00:00:00Z" - ] - ], - "enforceFileDownloadLocation" .= object ["lockStatus" .= "locked", "status" .= "disabled", "ttl" .= "unlimited", "config" .= object []], - "limitedEventFanout" .= disabled - ] bindResponse (Public.getTeamFeatures m tid) $ \resp -> do resp.status `shouldMatchInt` 200 - expected `shouldMatch` resp.json + defAllFeatures `shouldMatch` resp.json -- This block catches potential errors in the logic that reverts to default if there is a distinction made between -- 1. there is no row for a team_id in galley.team_features @@ -491,17 +58,17 @@ testAllFeatures = do bindResponse (Public.getTeamFeatures m tid) $ \resp -> do resp.status `shouldMatchInt` 200 - expected `shouldMatch` resp.json + defAllFeatures `shouldMatch` resp.json bindResponse (Public.getFeatureConfigs m) $ \resp -> do resp.status `shouldMatchInt` 200 - expected `shouldMatch` resp.json + defAllFeatures `shouldMatch` resp.json randomPersonalUser <- randomUser OwnDomain def bindResponse (Public.getFeatureConfigs randomPersonalUser) $ \resp -> do resp.status `shouldMatchInt` 200 - expected `shouldMatch` resp.json + defAllFeatures `shouldMatch` resp.json testFeatureConfigConsistency :: (HasCallStack) => App () testFeatureConfigConsistency = do @@ -521,675 +88,9 @@ testFeatureConfigConsistency = do (A.Object hm) -> pure (Set.fromList . map (show . A.toText) . KM.keys $ hm) x -> assertFailure ("JSON was not an object, but " <> show x) -testSelfDeletingMessages :: (HasCallStack) => App () -testSelfDeletingMessages = - _testLockStatusWithConfig - "selfDeletingMessages" - Public.setTeamFeatureConfig - (object ["lockStatus" .= "unlocked", "status" .= "enabled", "ttl" .= "unlimited", "config" .= object ["enforcedTimeoutSeconds" .= A.Number 0]]) - (object ["status" .= "disabled", "config" .= object ["enforcedTimeoutSeconds" .= A.Number 0]]) - (object ["status" .= "enabled", "config" .= object ["enforcedTimeoutSeconds" .= A.Number 30]]) - (object ["status" .= "enabled", "config" .= object ["enforcedTimeoutSeconds" .= ""]]) - -testSelfDeletingMessagesInternal :: (HasCallStack) => App () -testSelfDeletingMessagesInternal = - _testLockStatusWithConfig - "selfDeletingMessages" - Internal.setTeamFeatureConfig - (object ["lockStatus" .= "unlocked", "status" .= "enabled", "ttl" .= "unlimited", "config" .= object ["enforcedTimeoutSeconds" .= A.Number 0]]) - (object ["status" .= "disabled", "config" .= object ["enforcedTimeoutSeconds" .= A.Number 0]]) - (object ["status" .= "enabled", "config" .= object ["enforcedTimeoutSeconds" .= A.Number 30]]) - (object ["status" .= "enabled", "config" .= object ["enforcedTimeoutSeconds" .= ""]]) - -testMls :: (HasCallStack) => App () -testMls = do - user <- randomUser OwnDomain def - uid <- asString $ user %. "id" - _testLockStatusWithConfig - "mls" - Public.setTeamFeatureConfig - mlsDefaultConfig - (mlsConfig1 uid) - mlsConfig2 - mlsInvalidConfig - -testMlsInternal :: (HasCallStack) => App () -testMlsInternal = do - user <- randomUser OwnDomain def - uid <- asString $ user %. "id" - _testLockStatusWithConfig - "mls" - Internal.setTeamFeatureConfig - mlsDefaultConfig - (mlsConfig1 uid) - mlsConfig2 - mlsInvalidConfig - -mlsDefaultConfig :: Value -mlsDefaultConfig = - object - [ "lockStatus" .= "unlocked", - "status" .= "disabled", - "ttl" .= "unlimited", - "config" - .= object - [ "protocolToggleUsers" .= ([] :: [String]), - "defaultProtocol" .= "proteus", - "supportedProtocols" .= ["proteus", "mls"], - "allowedCipherSuites" .= ([1] :: [Int]), - "defaultCipherSuite" .= A.Number 1 - ] - ] - -mlsConfig1 :: String -> Value -mlsConfig1 uid = - object - [ "status" .= "enabled", - "config" - .= object - [ "protocolToggleUsers" .= [uid], - "defaultProtocol" .= "mls", - "supportedProtocols" .= ["proteus", "mls"], - "allowedCipherSuites" .= ([1] :: [Int]), - "defaultCipherSuite" .= A.Number 1 - ] - ] - -mlsConfig2 :: Value -mlsConfig2 = - object - [ "status" .= "enabled", - "config" - .= object - [ "protocolToggleUsers" .= ([] :: [String]), - "defaultProtocol" .= "mls", - "supportedProtocols" .= ["mls"], - "allowedCipherSuites" .= ([1] :: [Int]), - "defaultCipherSuite" .= A.Number 1 - ] - ] - -mlsInvalidConfig :: Value -mlsInvalidConfig = - object - [ "status" .= "enabled", - "config" - .= object - [ "protocolToggleUsers" .= ([] :: [String]), - "defaultProtocol" .= "mls", - "supportedProtocols" .= ["proteus"], - "allowedCipherSuites" .= ([1] :: [Int]), - "defaultCipherSuite" .= A.Number 1 - ] - ] - -testEnforceDownloadLocation :: (HasCallStack) => App () -testEnforceDownloadLocation = - _testLockStatusWithConfig - "enforceFileDownloadLocation" - Public.setTeamFeatureConfig - (object ["lockStatus" .= "locked", "status" .= "disabled", "ttl" .= "unlimited", "config" .= object []]) - (object ["status" .= "enabled", "config" .= object ["enforcedDownloadLocation" .= "/tmp"]]) - (object ["status" .= "disabled", "config" .= object []]) - (object ["status" .= "enabled", "config" .= object ["enforcedDownloadLocation" .= object []]]) - -testEnforceDownloadLocationInternal :: (HasCallStack) => App () -testEnforceDownloadLocationInternal = - _testLockStatusWithConfig - "enforceFileDownloadLocation" - Internal.setTeamFeatureConfig - (object ["lockStatus" .= "locked", "status" .= "disabled", "ttl" .= "unlimited", "config" .= object []]) - (object ["status" .= "enabled", "config" .= object ["enforcedDownloadLocation" .= "/tmp"]]) - (object ["status" .= "disabled", "config" .= object []]) - (object ["status" .= "enabled", "config" .= object ["enforcedDownloadLocation" .= object []]]) - -testMlsMigration :: (HasCallStack) => App () -testMlsMigration = do - -- first we have to enable mls - (owner, tid, m : _) <- createTeam OwnDomain 2 - assertSuccess =<< Public.setTeamFeatureConfig owner tid "mls" mlsEnableConfig - _testLockStatusWithConfigWithTeam - (owner, tid, m) - "mlsMigration" - Public.setTeamFeatureConfig - mlsMigrationDefaultConfig - mlsMigrationConfig1 - mlsMigrationConfig2 - mlsMigrationInvalidConfig - -testMlsMigrationInternal :: (HasCallStack) => App () -testMlsMigrationInternal = do - -- first we have to enable mls - (owner, tid, m : _) <- createTeam OwnDomain 2 - assertSuccess =<< Public.setTeamFeatureConfig owner tid "mls" mlsEnableConfig - _testLockStatusWithConfigWithTeam - (owner, tid, m) - "mlsMigration" - Internal.setTeamFeatureConfig - mlsMigrationDefaultConfig - mlsMigrationConfig1 - mlsMigrationConfig2 - mlsMigrationInvalidConfig - -mlsEnableConfig :: Value -mlsEnableConfig = - object - [ "status" .= "enabled", - "config" - .= object - [ "protocolToggleUsers" .= ([] :: [String]), - "defaultProtocol" .= "mls", - "supportedProtocols" .= ["mls"], - "allowedCipherSuites" .= ([1] :: [Int]), - "defaultCipherSuite" .= A.Number 1 - ] - ] - -mlsMigrationDefaultConfig :: Value -mlsMigrationDefaultConfig = - object - [ "lockStatus" .= "locked", - "status" .= "enabled", - "ttl" .= "unlimited", - "config" - .= object - [ "startTime" .= "2029-05-16T10:11:12.123Z", - "finaliseRegardlessAfter" .= "2029-10-17T00:00:00Z" - ] - ] - -mlsMigrationConfig1 :: Value -mlsMigrationConfig1 = - object - [ "status" .= "enabled", - "config" - .= object - [ "startTime" .= "2029-05-16T10:11:12.123Z", - "finaliseRegardlessAfter" .= "2030-10-17T00:00:00Z" - ] - ] - -mlsMigrationConfig2 :: Value -mlsMigrationConfig2 = - object - [ "status" .= "enabled", - "config" - .= object - [ "startTime" .= "2030-05-16T10:11:12.123Z", - "finaliseRegardlessAfter" .= "2031-10-17T00:00:00Z" - ] - ] - -mlsMigrationInvalidConfig :: Value -mlsMigrationInvalidConfig = - object - [ "status" .= "enabled", - "config" - .= object - [ "startTime" .= A.Number 1 - ] - ] - -mlsE2EIdConfig :: App (Value, Value, Value, Value) -mlsE2EIdConfig = do - cfg2 <- - mlsE2EIdConfig1 - & setField "config.verificationExpiration" (A.Number 86401) - & setField "config.useProxyOnMobile" True - invalidConfig <- cfg2 & removeField "config.crlProxy" - pure (mlsE2EIdDefConfig, mlsE2EIdConfig1, cfg2, invalidConfig) - where - mlsE2EIdDefConfig :: Value - mlsE2EIdDefConfig = - object - [ "lockStatus" .= "unlocked", - "status" .= "disabled", - "ttl" .= "unlimited", - "config" - .= object - [ "verificationExpiration" .= A.Number 86400, - "useProxyOnMobile" .= False - ] - ] - mlsE2EIdConfig1 :: Value - mlsE2EIdConfig1 = - object - [ "status" .= "enabled", - "config" - .= object - [ "crlProxy" .= "https://example.com", - "verificationExpiration" .= A.Number 86400, - "useProxyOnMobile" .= False - ] - ] - -testMLSE2EId :: (HasCallStack) => App () -testMLSE2EId = do - (defCfg, cfg1, cfg2, invalidCfg) <- mlsE2EIdConfig - _testLockStatusWithConfig - "mlsE2EId" - Public.setTeamFeatureConfig - defCfg - cfg1 - cfg2 - invalidCfg - -testMLSE2EIdInternal :: (HasCallStack) => App () -testMLSE2EIdInternal = do - (defCfg, cfg1, cfg2, invalidCfg) <- mlsE2EIdConfig - -- the internal API is not as strict as the public one, so we need to tweak the invalid config some more - invalidCfg' <- invalidCfg & setField "config.crlProxy" (object []) - _testLockStatusWithConfig - "mlsE2EId" - Internal.setTeamFeatureConfig - defCfg - cfg1 - cfg2 - invalidCfg' - -_testLockStatusWithConfig :: - (HasCallStack) => - String -> - (Value -> String -> String -> Value -> App Response) -> - -- | the default feature config (should include the lock status and ttl, as it is returned by the API) - Value -> - -- | a valid config used to update the feature setting (should not include the lock status and ttl, as these are not part of the request payload) - Value -> - -- | another valid config - Value -> - -- | an invalid config - Value -> - App () -_testLockStatusWithConfig featureName setTeamFeatureConfig defaultFeatureConfig config1 config2 invalidConfig = do - (owner, tid, m : _) <- createTeam OwnDomain 2 - _testLockStatusWithConfigWithTeam (owner, tid, m) featureName setTeamFeatureConfig defaultFeatureConfig config1 config2 invalidConfig - -_testLockStatusWithConfigWithTeam :: - (HasCallStack) => - -- | (owner, tid, member) - (Value, String, Value) -> - String -> - (Value -> String -> String -> Value -> App Response) -> - -- | the default feature config (should include the lock status and ttl, as it is returned by the API) - Value -> - -- | a valid config used to update the feature setting (should not include the lock status and ttl, as these are not part of the request payload) - Value -> - -- | another valid config - Value -> - -- | an invalid config - Value -> - App () -_testLockStatusWithConfigWithTeam (owner, tid, m) featureName setTeamFeatureConfig defaultFeatureConfig config1 config2 invalidConfig = do - -- personal user - randomPersonalUser <- randomUser OwnDomain def - - bindResponse (Public.getFeatureConfigs randomPersonalUser) $ \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. featureName `shouldMatch` defaultFeatureConfig - - -- team user - nonTeamMember <- randomUser OwnDomain def - assertForbidden =<< Public.getTeamFeature nonTeamMember tid featureName - - checkFeature featureName m tid defaultFeatureConfig - - -- lock the feature - Internal.setTeamFeatureLockStatus OwnDomain tid featureName "locked" - assertStatus 409 =<< setTeamFeatureConfig owner tid featureName config1 - Internal.setTeamFeatureLockStatus OwnDomain tid featureName "unlocked" - - void $ withWebSockets [m] $ \wss -> do - assertSuccess =<< setTeamFeatureConfig owner tid featureName config1 - for_ wss $ \ws -> do - notif <- awaitMatch isFeatureConfigUpdateNotif ws - notif %. "payload.0.name" `shouldMatch` featureName - notif %. "payload.0.data" `shouldMatch` (config1 & setField "lockStatus" "unlocked" & setField "ttl" "unlimited") - - checkFeature featureName m tid =<< (config1 & setField "lockStatus" "unlocked" & setField "ttl" "unlimited") - - Internal.setTeamFeatureLockStatus OwnDomain tid featureName "locked" - checkFeature featureName m tid =<< setField "lockStatus" "locked" defaultFeatureConfig - Internal.setTeamFeatureLockStatus OwnDomain tid featureName "unlocked" - - void $ withWebSockets [m] $ \wss -> do - assertStatus 400 =<< setTeamFeatureConfig owner tid featureName invalidConfig - for_ wss $ assertNoEvent 2 - - checkFeature featureName m tid =<< (config1 & setField "lockStatus" "unlocked" & setField "ttl" "unlimited") - - void $ withWebSockets [m] $ \wss -> do - assertSuccess =<< setTeamFeatureConfig owner tid featureName config2 - for_ wss $ \ws -> do - notif <- awaitMatch isFeatureConfigUpdateNotif ws - notif %. "payload.0.name" `shouldMatch` featureName - notif %. "payload.0.data" `shouldMatch` (config2 & setField "lockStatus" "unlocked" & setField "ttl" "unlimited") - - checkFeature featureName m tid =<< (config2 & setField "lockStatus" "unlocked" & setField "ttl" "unlimited") - -testFeatureNoConfigMultiSearchVisibilityInbound :: (HasCallStack) => App () -testFeatureNoConfigMultiSearchVisibilityInbound = do - (_owner1, team1, _) <- createTeam OwnDomain 0 - (_owner2, team2, _) <- createTeam OwnDomain 0 - - assertSuccess =<< Internal.setTeamFeatureStatus OwnDomain team2 "searchVisibilityInbound" "enabled" - - response <- Internal.getFeatureStatusMulti OwnDomain "searchVisibilityInbound" [team1, team2] - - statuses <- response.json %. "default_status" >>= asList - length statuses `shouldMatchInt` 2 - statuses `shouldMatchSet` [object ["team" .= team1, "status" .= "disabled"], object ["team" .= team2, "status" .= "enabled"]] - -testConferenceCallingTTLIncreaseToUnlimited :: (HasCallStack) => App () -testConferenceCallingTTLIncreaseToUnlimited = _testSimpleFlagTTLOverride "conferenceCalling" True (Just 2) Nothing - -testConferenceCallingTTLIncrease :: (HasCallStack) => App () -testConferenceCallingTTLIncrease = _testSimpleFlagTTLOverride "conferenceCalling" True (Just 2) (Just 4) - -testConferenceCallingTTLReduceFromUnlimited :: (HasCallStack) => App () -testConferenceCallingTTLReduceFromUnlimited = _testSimpleFlagTTLOverride "conferenceCalling" True Nothing (Just 2) - -testConferenceCallingTTLReduce :: (HasCallStack) => App () -testConferenceCallingTTLReduce = _testSimpleFlagTTLOverride "conferenceCalling" True (Just 5) (Just 2) - -testConferenceCallingTTLUnlimitedToUnlimited :: (HasCallStack) => App () -testConferenceCallingTTLUnlimitedToUnlimited = _testSimpleFlagTTLOverride "conferenceCalling" True Nothing Nothing - -_testSimpleFlagTTLOverride :: (HasCallStack) => String -> Bool -> Maybe Int -> Maybe Int -> App () -_testSimpleFlagTTLOverride featureName enabledByDefault mTtl mTtlAfter = do - let ttl = maybe (A.String . cs $ "unlimited") (A.Number . fromIntegral) mTtl - let ttlAfter = maybe (A.String . cs $ "unlimited") (A.Number . fromIntegral) mTtlAfter - (owner, tid, _) <- createTeam OwnDomain 0 - let (defaultValue, otherValue) = if enabledByDefault then ("enabled", "disabled") else ("disabled", "enabled") - - -- Initial value should be the default value - let defFeatureStatus = object ["status" .= defaultValue, "ttl" .= "unlimited", "lockStatus" .= "unlocked"] - checkFeature featureName owner tid defFeatureStatus - - -- Setting should work - assertSuccess =<< Internal.setTeamFeatureConfig OwnDomain tid featureName (object ["status" .= otherValue, "ttl" .= ttl]) - checkFeatureLenientTtl featureName owner tid (object ["status" .= otherValue, "ttl" .= ttl, "lockStatus" .= "unlocked"]) - - case (mTtl, mTtlAfter) of - (Just d, Just d') -> do - -- wait less than expiration, override and recheck. - liftIO $ threadDelay (d * 1000000 `div` 2) -- waiting half of TTL - -- setFlagInternal otherValue ttlAfter - assertSuccess =<< Internal.setTeamFeatureConfig OwnDomain tid featureName (object ["status" .= otherValue, "ttl" .= ttlAfter]) - -- value is still correct - checkFeatureLenientTtl featureName owner tid (object ["status" .= otherValue, "ttl" .= ttlAfter, "lockStatus" .= "unlocked"]) - - liftIO $ threadDelay (d' * 1000000) -- waiting for new TTL - checkFeatureLenientTtl featureName owner tid defFeatureStatus - (Just d, Nothing) -> do - -- wait less than expiration, override and recheck. - liftIO $ threadDelay (d * 1000000 `div` 2) -- waiting half of TTL - assertSuccess =<< Internal.setTeamFeatureConfig OwnDomain tid featureName (object ["status" .= otherValue, "ttl" .= ttlAfter]) - -- value is still correct - checkFeatureLenientTtl featureName owner tid (object ["status" .= otherValue, "ttl" .= ttlAfter, "lockStatus" .= "unlocked"]) - (Nothing, Nothing) -> do - -- overriding in this case should have no effect. - assertSuccess =<< Internal.setTeamFeatureConfig OwnDomain tid featureName (object ["status" .= otherValue, "ttl" .= ttl]) - checkFeatureLenientTtl featureName owner tid (object ["status" .= otherValue, "ttl" .= ttl, "lockStatus" .= "unlocked"]) - (Nothing, Just d) -> do - assertSuccess =<< Internal.setTeamFeatureConfig OwnDomain tid featureName (object ["status" .= otherValue, "ttl" .= ttlAfter]) - checkFeatureLenientTtl featureName owner tid (object ["status" .= otherValue, "ttl" .= ttlAfter, "lockStatus" .= "unlocked"]) - liftIO $ threadDelay (d * 1000000) -- waiting it out - -- value reverts back - checkFeatureLenientTtl featureName owner tid defFeatureStatus - --------------------------------------------------------------------------------- --- Simple flags with implicit lock status - -testPatchSearchVisibility :: (HasCallStack) => App () -testPatchSearchVisibility = _testPatch "searchVisibility" False disabled enabled - -testPatchValidateSAMLEmails :: (HasCallStack) => App () -testPatchValidateSAMLEmails = _testPatch "validateSAMLemails" False enabled disabled - -testPatchDigitalSignatures :: (HasCallStack) => App () -testPatchDigitalSignatures = _testPatch "digitalSignatures" False disabled enabled - -testPatchConferenceCalling :: (HasCallStack) => App () -testPatchConferenceCalling = _testPatch "conferenceCalling" False enabled disabled - --------------------------------------------------------------------------------- --- Simple flags with explicit lock status - -testPatchFileSharing :: (HasCallStack) => App () -testPatchFileSharing = _testPatch "fileSharing" True enabled disabled - -testPatchGuestLinks :: (HasCallStack) => App () -testPatchGuestLinks = _testPatch "conversationGuestLinks" True enabled disabled - -testPatchSndFactorPasswordChallenge :: (HasCallStack) => App () -testPatchSndFactorPasswordChallenge = _testPatch "sndFactorPasswordChallenge" True disabledLocked enabled - -testPatchOutlookCalIntegration :: (HasCallStack) => App () -testPatchOutlookCalIntegration = _testPatch "outlookCalIntegration" True disabledLocked enabled - --------------------------------------------------------------------------------- --- Flags with config & implicit lock status - -testPatchAppLock :: (HasCallStack) => App () -testPatchAppLock = do - let defCfg = - object - [ "lockStatus" .= "unlocked", - "status" .= "enabled", - "ttl" .= "unlimited", - "config" .= object ["enforceAppLock" .= False, "inactivityTimeoutSecs" .= A.Number 60] - ] - _testPatch "appLock" False defCfg (object ["lockStatus" .= "locked"]) - _testPatch "appLock" False defCfg (object ["status" .= "disabled"]) - _testPatch "appLock" False defCfg (object ["lockStatus" .= "locked", "status" .= "disabled"]) - _testPatch "appLock" False defCfg (object ["lockStatus" .= "unlocked", "config" .= object ["enforceAppLock" .= True, "inactivityTimeoutSecs" .= A.Number 120]]) - _testPatch "appLock" False defCfg (object ["config" .= object ["enforceAppLock" .= True, "inactivityTimeoutSecs" .= A.Number 240]]) - --------------------------------------------------------------------------------- --- Flags with config & explicit lock status - -testPatchSelfDeletingMessages :: (HasCallStack) => App () -testPatchSelfDeletingMessages = do - let defCfg = - object - [ "lockStatus" .= "unlocked", - "status" .= "enabled", - "ttl" .= "unlimited", - "config" .= object ["enforcedTimeoutSeconds" .= A.Number 0] - ] - _testPatch "selfDeletingMessages" True defCfg (object ["lockStatus" .= "locked"]) - _testPatch "selfDeletingMessages" True defCfg (object ["status" .= "disabled"]) - _testPatch "selfDeletingMessages" True defCfg (object ["lockStatus" .= "locked", "status" .= "disabled"]) - _testPatch "selfDeletingMessages" True defCfg (object ["lockStatus" .= "unlocked", "config" .= object ["enforcedTimeoutSeconds" .= A.Number 30]]) - _testPatch "selfDeletingMessages" True defCfg (object ["config" .= object ["enforcedTimeoutSeconds" .= A.Number 60]]) - -testPatchEnforceFileDownloadLocation :: (HasCallStack) => App () -testPatchEnforceFileDownloadLocation = do - let defCfg = - object - [ "lockStatus" .= "locked", - "status" .= "disabled", - "ttl" .= "unlimited", - "config" .= object [] - ] - _testPatch "enforceFileDownloadLocation" True defCfg (object ["lockStatus" .= "unlocked"]) - _testPatch "enforceFileDownloadLocation" True defCfg (object ["status" .= "enabled"]) - _testPatch "enforceFileDownloadLocation" True defCfg (object ["lockStatus" .= "unlocked", "status" .= "enabled"]) - _testPatch "enforceFileDownloadLocation" True defCfg (object ["lockStatus" .= "locked", "config" .= object []]) - _testPatch "enforceFileDownloadLocation" True defCfg (object ["config" .= object ["enforcedDownloadLocation" .= "/tmp"]]) - -testPatchE2EId :: (HasCallStack) => App () -testPatchE2EId = do - let defCfg = - object - [ "lockStatus" .= "unlocked", - "status" .= "disabled", - "ttl" .= "unlimited", - "config" - .= object - [ "verificationExpiration" .= A.Number 86400, - "useProxyOnMobile" .= False - ] - ] - _testPatch "mlsE2EId" True defCfg (object ["lockStatus" .= "locked"]) - _testPatch "mlsE2EId" True defCfg (object ["status" .= "enabled"]) - _testPatch "mlsE2EId" True defCfg (object ["lockStatus" .= "locked", "status" .= "enabled"]) - _testPatch - "mlsE2EId" - True - defCfg - ( object - [ "lockStatus" .= "unlocked", - "config" - .= object - [ "crlProxy" .= "https://example.com", - "verificationExpiration" .= A.Number 86401, - "useProxyOnMobile" .= True - ] - ] - ) - _testPatch - "mlsE2EId" - True - defCfg - ( object - [ "config" - .= object - [ "crlProxy" .= "https://example.com", - "verificationExpiration" .= A.Number 86401, - "useProxyOnMobile" .= True - ] - ] - ) - -testPatchMLS :: (HasCallStack) => App () -testPatchMLS = do - dom <- asString OwnDomain - (_, tid, _) <- createTeam dom 0 - assertSuccess - =<< Internal.patchTeamFeature - dom - tid - "mlsMigration" - (object ["status" .= "disabled", "lockStatus" .= "unlocked"]) - let defCfg = - object - [ "lockStatus" .= "unlocked", - "status" .= "disabled", - "ttl" .= "unlimited", - "config" - .= object - [ "protocolToggleUsers" .= ([] :: [String]), - "defaultProtocol" .= "proteus", - "supportedProtocols" .= ["proteus", "mls"], - "allowedCipherSuites" .= ([1] :: [Int]), - "defaultCipherSuite" .= A.Number 1 - ] - ] - _testPatchWithSetup mlsMigrationSetup dom "mls" True defCfg (object ["lockStatus" .= "locked"]) - _testPatchWithSetup mlsMigrationSetup dom "mls" True defCfg (object ["status" .= "enabled"]) - _testPatchWithSetup mlsMigrationSetup dom "mls" True defCfg (object ["lockStatus" .= "locked", "status" .= "enabled"]) - _testPatchWithSetup - mlsMigrationSetup - dom - "mls" - True - defCfg - ( object - [ "status" .= "enabled", - "config" - .= object - [ "protocolToggleUsers" .= ([] :: [String]), - "defaultProtocol" .= "mls", - "supportedProtocols" .= ["proteus", "mls"], - "allowedCipherSuites" .= ([1] :: [Int]), - "defaultCipherSuite" .= A.Number 1 - ] - ] - ) - _testPatchWithSetup - mlsMigrationSetup - dom - "mls" - True - defCfg - ( object - [ "config" - .= object - [ "protocolToggleUsers" .= ([] :: [String]), - "defaultProtocol" .= "mls", - "supportedProtocols" .= ["proteus", "mls"], - "allowedCipherSuites" .= ([1] :: [Int]), - "defaultCipherSuite" .= A.Number 1 - ] - ] - ) - where - mlsMigrationSetup :: (HasCallStack) => String -> String -> App () - mlsMigrationSetup dom tid = - assertSuccess - =<< Internal.patchTeamFeature - dom - tid - "mlsMigration" - (object ["status" .= "disabled", "lockStatus" .= "unlocked"]) - -_testPatch :: (HasCallStack) => String -> Bool -> Value -> Value -> App () -_testPatch featureName hasExplicitLockStatus defaultFeatureConfig patch = do - dom <- asString OwnDomain - _testPatchWithSetup - (\_ _ -> pure ()) - dom - featureName - hasExplicitLockStatus - defaultFeatureConfig - patch - -_testPatchWithSetup :: - (HasCallStack) => - (String -> String -> App ()) -> - String -> - String -> - Bool -> - Value -> - Value -> - App () -_testPatchWithSetup setup domain featureName hasExplicitLockStatus defaultFeatureConfig patch = do - (owner, tid, _) <- createTeam domain 0 - -- run a feature-specific setup. For most features this is a no-op. - setup domain tid - - checkFeature featureName owner tid defaultFeatureConfig - assertSuccess =<< Internal.patchTeamFeature domain tid featureName patch - patched <- (.json) =<< Internal.getTeamFeature domain tid featureName - checkFeature featureName owner tid patched - lockStatus <- patched %. "lockStatus" >>= asString - if lockStatus == "locked" - then do - -- if lock status is locked the feature status should fall back to the default - patched `shouldMatch` (defaultFeatureConfig & setField "lockStatus" "locked") - -- if lock status is locked, it was either locked before or changed by the patch - mPatchedLockStatus <- lookupField patch "lockStatus" - case mPatchedLockStatus of - Just ls -> ls `shouldMatch` "locked" - Nothing -> defaultFeatureConfig %. "lockStatus" `shouldMatch` "locked" - else do - patched %. "status" `shouldMatch` valueOrDefault "status" - mPatchedConfig <- lookupField patched "config" - case mPatchedConfig of - Just patchedConfig -> patchedConfig `shouldMatch` valueOrDefault "config" - Nothing -> do - mDefConfig <- lookupField defaultFeatureConfig "config" - assertBool "patch had an unexpected config field" (isNothing mDefConfig) - - when hasExplicitLockStatus $ do - -- if lock status is unlocked, it was either unlocked before or changed by the patch - mPatchedLockStatus <- lookupField patch "lockStatus" - case mPatchedLockStatus of - Just ls -> ls `shouldMatch` "unlocked" - Nothing -> defaultFeatureConfig %. "lockStatus" `shouldMatch` "unlocked" - where - valueOrDefault :: String -> App Value - valueOrDefault key = do - mValue <- lookupField patch key - maybe (defaultFeatureConfig %. key) pure mValue +testNonMemberAccess :: (HasCallStack) => Feature -> App () +testNonMemberAccess (Feature featureName) = do + (_, tid, _) <- createTeam OwnDomain 0 + nonMember <- randomUser OwnDomain def + Public.getTeamFeature nonMember tid featureName + >>= assertForbidden diff --git a/integration/test/Test/FeatureFlags/AppLock.hs b/integration/test/Test/FeatureFlags/AppLock.hs new file mode 100644 index 00000000000..f031403a98d --- /dev/null +++ b/integration/test/Test/FeatureFlags/AppLock.hs @@ -0,0 +1,31 @@ +module Test.FeatureFlags.AppLock where + +import qualified Data.Aeson as A +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchAppLock :: (HasCallStack) => App () +testPatchAppLock = do + checkPatch OwnDomain "appLock" + $ object ["lockStatus" .= "locked"] + checkPatch OwnDomain "appLock" + $ object ["status" .= "disabled"] + checkPatch OwnDomain "appLock" + $ object ["lockStatus" .= "locked", "status" .= "disabled"] + checkPatch OwnDomain "appLock" + $ object + [ "lockStatus" .= "unlocked", + "config" + .= object + [ "enforceAppLock" .= True, + "inactivityTimeoutSecs" .= A.Number 120 + ] + ] + checkPatch OwnDomain "appLock" + $ object + [ "config" + .= object + [ "enforceAppLock" .= True, + "inactivityTimeoutSecs" .= A.Number 240 + ] + ] diff --git a/integration/test/Test/FeatureFlags/ClassifiedDomains.hs b/integration/test/Test/FeatureFlags/ClassifiedDomains.hs new file mode 100644 index 00000000000..4fe8043f70b --- /dev/null +++ b/integration/test/Test/FeatureFlags/ClassifiedDomains.hs @@ -0,0 +1,18 @@ +module Test.FeatureFlags.ClassifiedDomains where + +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude + +testClassifiedDomainsEnabled :: (HasCallStack) => App () +testClassifiedDomainsEnabled = do + (_, tid, m : _) <- createTeam OwnDomain 2 + expected <- enabled & setField "config.domains" ["example.com"] + checkFeature "classifiedDomains" m tid expected + +testClassifiedDomainsDisabled :: (HasCallStack) => App () +testClassifiedDomainsDisabled = do + withModifiedBackend def {galleyCfg = setField "settings.featureFlags.classifiedDomains" (object ["status" .= "disabled", "config" .= object ["domains" .= ["example.com"]]])} $ \domain -> do + (_, tid, m : _) <- createTeam domain 2 + expected <- disabled & setField "config.domains" ["example.com"] + checkFeature "classifiedDomains" m tid expected diff --git a/integration/test/Test/FeatureFlags/ConferenceCalling.hs b/integration/test/Test/FeatureFlags/ConferenceCalling.hs new file mode 100644 index 00000000000..30cc5621bcc --- /dev/null +++ b/integration/test/Test/FeatureFlags/ConferenceCalling.hs @@ -0,0 +1,26 @@ +module Test.FeatureFlags.ConferenceCalling where + +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchConferenceCalling :: (HasCallStack) => App () +testPatchConferenceCalling = do + checkPatch OwnDomain "conferenceCalling" + $ object ["lockStatus" .= "locked"] + checkPatch OwnDomain "conferenceCalling" + $ object ["status" .= "disabled"] + checkPatch OwnDomain "conferenceCalling" + $ object ["lockStatus" .= "locked", "status" .= "disabled"] + checkPatch OwnDomain "conferenceCalling" + $ object + [ "lockStatus" .= "unlocked", + "config" .= object ["useSFTForOneToOneCalls" .= toJSON True] + ] + +testConferenceCalling :: (HasCallStack) => APIAccess -> App () +testConferenceCalling access = do + runFeatureTests OwnDomain access + $ mkFeatureTests "conferenceCalling" + & addUpdate (confCalling def {sft = toJSON True}) + & addUpdate (confCalling def {sft = toJSON False}) + & addInvalidUpdate (confCalling def {sft = toJSON (0 :: Int)}) diff --git a/integration/test/Test/FeatureFlags/DigitalSignatures.hs b/integration/test/Test/FeatureFlags/DigitalSignatures.hs new file mode 100644 index 00000000000..0a00bc33926 --- /dev/null +++ b/integration/test/Test/FeatureFlags/DigitalSignatures.hs @@ -0,0 +1,15 @@ +module Test.FeatureFlags.DigitalSignatures where + +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchDigitalSignatures :: (HasCallStack) => App () +testPatchDigitalSignatures = checkPatch OwnDomain "digitalSignatures" enabled + +testDigitalSignaturesInternal :: (HasCallStack) => App () +testDigitalSignaturesInternal = do + (alice, tid, _) <- createTeam OwnDomain 0 + withWebSocket alice $ \ws -> do + setFlag InternalAPI ws tid "digitalSignatures" disabled + setFlag InternalAPI ws tid "digitalSignatures" enabled diff --git a/integration/test/Test/FeatureFlags/EnforceFileDownloadLocation.hs b/integration/test/Test/FeatureFlags/EnforceFileDownloadLocation.hs new file mode 100644 index 00000000000..9bb1a608b4c --- /dev/null +++ b/integration/test/Test/FeatureFlags/EnforceFileDownloadLocation.hs @@ -0,0 +1,55 @@ +module Test.FeatureFlags.EnforceFileDownloadLocation where + +import qualified API.GalleyInternal as Internal +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchEnforceFileDownloadLocation :: (HasCallStack) => App () +testPatchEnforceFileDownloadLocation = do + checkPatch OwnDomain "enforceFileDownloadLocation" + $ object ["lockStatus" .= "unlocked"] + checkPatch OwnDomain "enforceFileDownloadLocation" + $ object ["status" .= "enabled"] + checkPatch OwnDomain "enforceFileDownloadLocation" + $ object ["lockStatus" .= "unlocked", "status" .= "enabled"] + checkPatch OwnDomain "enforceFileDownloadLocation" + $ object ["lockStatus" .= "locked", "config" .= object []] + checkPatch OwnDomain "enforceFileDownloadLocation" + $ object ["config" .= object ["enforcedDownloadLocation" .= "/tmp"]] + + do + (user, tid, _) <- createTeam OwnDomain 0 + bindResponse + ( Internal.patchTeamFeature + user + tid + "enforceFileDownloadLocation" + (object ["config" .= object ["enforcedDownloadLocation" .= ""]]) + ) + $ \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "empty-download-location" + +testEnforceDownloadLocation :: (HasCallStack) => APIAccess -> App () +testEnforceDownloadLocation access = do + mkFeatureTests + "enforceFileDownloadLocation" + & addUpdate + ( object + [ "status" .= "enabled", + "config" .= object ["enforcedDownloadLocation" .= "/tmp"] + ] + ) + & addUpdate + (object ["status" .= "disabled", "config" .= object []]) + & addInvalidUpdate + ( object + [ "status" .= "enabled", + "config" + .= object + [ "enforcedDownloadLocation" .= object [] + ] + ] + ) + & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/FileSharing.hs b/integration/test/Test/FeatureFlags/FileSharing.hs new file mode 100644 index 00000000000..7cc761e64ef --- /dev/null +++ b/integration/test/Test/FeatureFlags/FileSharing.hs @@ -0,0 +1,14 @@ +module Test.FeatureFlags.FileSharing where + +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchFileSharing :: (HasCallStack) => App () +testPatchFileSharing = checkPatch OwnDomain "fileSharing" disabled + +testFileSharing :: (HasCallStack) => APIAccess -> App () +testFileSharing access = + mkFeatureTests "fileSharing" + & addUpdate disabled + & addUpdate enabled + & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/GuestLinks.hs b/integration/test/Test/FeatureFlags/GuestLinks.hs new file mode 100644 index 00000000000..0c0c84ae387 --- /dev/null +++ b/integration/test/Test/FeatureFlags/GuestLinks.hs @@ -0,0 +1,14 @@ +module Test.FeatureFlags.GuestLinks where + +import Test.FeatureFlags.Util +import Testlib.Prelude + +testConversationGuestLinks :: (HasCallStack) => APIAccess -> App () +testConversationGuestLinks access = + mkFeatureTests "conversationGuestLinks" + & addUpdate disabled + & addUpdate enabled + & runFeatureTests OwnDomain access + +testPatchGuestLinks :: (HasCallStack) => App () +testPatchGuestLinks = checkPatch OwnDomain "conversationGuestLinks" disabled diff --git a/integration/test/Test/FeatureFlags/Initialisation.hs b/integration/test/Test/FeatureFlags/Initialisation.hs new file mode 100644 index 00000000000..ac84a57ac91 --- /dev/null +++ b/integration/test/Test/FeatureFlags/Initialisation.hs @@ -0,0 +1,68 @@ +module Test.FeatureFlags.Initialisation where + +import API.GalleyInternal +import Control.Monad.Codensity +import Control.Monad.Extra +import Control.Monad.Reader +import SetupHelpers +import Testlib.Prelude +import Testlib.ResourcePool + +testMLSInitialisation :: (HasCallStack) => App () +testMLSInitialisation = do + let override = + def + { galleyCfg = + setField + "settings.featureFlags.mls" + ( object + [ "initialConfig" + .= object + [ "protocolToggleUsers" .= ([] :: [Int]), + "defaultProtocol" .= "mls", + "allowedCipherSuites" .= [1, 2 :: Int], + "defaultCipherSuite" .= (1 :: Int), + "supportedProtocols" .= ["mls", "proteus"] + ] + ] + ) + >=> removeField "settings.featureFlags.mlsMigration" + } + + pool <- asks (.resourcePool) + lowerCodensity do + [resource] <- acquireResources 1 pool + + (alice, aliceTeam) <- lift $ lowerCodensity do + -- start a dynamic backend with default configuration + domain <- startDynamicBackend resource def + + -- create a team + lift do + (alice, tid, _) <- createTeam domain 0 + feat <- getTeamFeature alice tid "mls" >>= getJSON 200 + feat %. "config.defaultProtocol" `shouldMatch` "proteus" + pure (alice, tid) + + lift $ lowerCodensity do + -- now start the backend again, this time with an initial mls + -- configuration set + domain <- startDynamicBackend resource override + + -- a pre-existing team should get the default configuration + lift do + feat <- getTeamFeature alice aliceTeam "mls" >>= getJSON 200 + feat %. "config.defaultProtocol" `shouldMatch` "proteus" + + -- a new team should get the initial mls configuration + lift do + (bob, tid, _) <- createTeam domain 0 + feat <- getTeamFeature bob tid "mls" >>= getJSON 200 + feat %. "config.defaultProtocol" `shouldMatch` "mls" + + -- if the mls feature is locked, the config reverts back to default + void + $ patchTeamFeature bob tid "mls" (object ["lockStatus" .= "locked"]) + >>= getJSON 200 + feat' <- getTeamFeature bob tid "mls" >>= getJSON 200 + feat' %. "config.defaultProtocol" `shouldMatch` "proteus" diff --git a/integration/test/Test/FeatureFlags/LegalHold.hs b/integration/test/Test/FeatureFlags/LegalHold.hs new file mode 100644 index 00000000000..55743ec4f91 --- /dev/null +++ b/integration/test/Test/FeatureFlags/LegalHold.hs @@ -0,0 +1,142 @@ +module Test.FeatureFlags.LegalHold where + +import qualified API.Galley as Public +import qualified API.GalleyInternal as Internal +import Control.Monad.Codensity (Codensity (runCodensity)) +import Control.Monad.Reader +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude +import Testlib.ResourcePool (acquireResources) + +testLegalholdDisabledByDefault :: (HasCallStack) => App () +testLegalholdDisabledByDefault = do + let put uid tid st = Internal.setTeamFeatureConfig uid tid "legalhold" (object ["status" .= st]) >>= assertSuccess + let patch uid tid st = Internal.setTeamFeatureStatus uid tid "legalhold" st >>= assertSuccess + forM_ [put, patch] $ \setFeatureStatus -> do + withModifiedBackend + def {galleyCfg = setField "settings.featureFlags.legalhold" "disabled-by-default"} + $ \domain -> do + (owner, tid, m : _) <- createTeam domain 2 + nonMember <- randomUser domain def + assertForbidden =<< Public.getTeamFeature nonMember tid "legalhold" + -- Test default + checkFeature "legalhold" m tid disabled + -- Test override + setFeatureStatus owner tid "enabled" + checkFeature "legalhold" owner tid enabled + setFeatureStatus owner tid "disabled" + checkFeature "legalhold" owner tid disabled + +-- always disabled +testLegalholdDisabledPermanently :: (HasCallStack) => App () +testLegalholdDisabledPermanently = do + let cfgLhDisabledPermanently = + def + { galleyCfg = setField "settings.featureFlags.legalhold" "disabled-permanently" + } + cfgLhDisabledByDefault = + def + { galleyCfg = setField "settings.featureFlags.legalhold" "disabled-by-default" + } + resourcePool <- asks (.resourcePool) + runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do + let domain = testBackend.berDomain + + -- Happy case: DB has no config for the team + runCodensity (startDynamicBackend testBackend cfgLhDisabledPermanently) $ \_ -> do + (owner, tid, _) <- createTeam domain 1 + checkFeature "legalhold" owner tid disabled + assertStatus 403 =<< Internal.setTeamFeatureStatus domain tid "legalhold" "enabled" + assertStatus 403 =<< Internal.setTeamFeatureConfig domain tid "legalhold" (object ["status" .= "enabled"]) + + -- Interesting case: The team had LH enabled before backend config was + -- changed to disabled-permanently + (owner, tid) <- runCodensity (startDynamicBackend testBackend cfgLhDisabledByDefault) $ \_ -> do + (owner, tid, _) <- createTeam domain 1 + checkFeature "legalhold" owner tid disabled + assertSuccess =<< Internal.setTeamFeatureStatus domain tid "legalhold" "enabled" + checkFeature "legalhold" owner tid enabled + pure (owner, tid) + + runCodensity (startDynamicBackend testBackend cfgLhDisabledPermanently) $ \_ -> do + checkFeature "legalhold" owner tid disabled + +-- enabled if team is allow listed, disabled in any other case +testLegalholdWhitelistTeamsAndImplicitConsent :: (HasCallStack) => App () +testLegalholdWhitelistTeamsAndImplicitConsent = do + let cfgLhWhitelistTeamsAndImplicitConsent = + def + { galleyCfg = setField "settings.featureFlags.legalhold" "whitelist-teams-and-implicit-consent" + } + cfgLhDisabledByDefault = + def + { galleyCfg = setField "settings.featureFlags.legalhold" "disabled-by-default" + } + resourcePool <- asks (.resourcePool) + runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do + let domain = testBackend.berDomain + + -- Happy case: DB has no config for the team + (owner, tid) <- runCodensity (startDynamicBackend testBackend cfgLhWhitelistTeamsAndImplicitConsent) $ \_ -> do + (owner, tid, _) <- createTeam domain 1 + checkFeature "legalhold" owner tid disabled + Internal.legalholdWhitelistTeam tid owner >>= assertSuccess + checkFeature "legalhold" owner tid enabled + + -- Disabling it doesn't work + assertStatus 403 =<< Internal.setTeamFeatureStatus domain tid "legalhold" "disabled" + assertStatus 403 =<< Internal.setTeamFeatureConfig domain tid "legalhold" (object ["status" .= "disabled"]) + checkFeature "legalhold" owner tid enabled + pure (owner, tid) + + -- Interesting case: The team had LH disabled before backend config was + -- changed to "whitelist-teams-and-implicit-consent". It should still show + -- enabled when the config gets changed. + runCodensity (startDynamicBackend testBackend cfgLhDisabledByDefault) $ \_ -> do + checkFeature "legalhold" owner tid disabled + assertSuccess =<< Internal.setTeamFeatureStatus domain tid "legalhold" "disabled" + checkFeature "legalhold" owner tid disabled + + runCodensity (startDynamicBackend testBackend cfgLhWhitelistTeamsAndImplicitConsent) $ \_ -> do + checkFeature "legalhold" owner tid enabled + +testExposeInvitationURLsToTeamAdminConfig :: (HasCallStack) => App () +testExposeInvitationURLsToTeamAdminConfig = do + let cfgExposeInvitationURLsTeamAllowlist tids = + def + { galleyCfg = setField "settings.exposeInvitationURLsTeamAllowlist" tids + } + resourcePool <- asks (.resourcePool) + runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do + let domain = testBackend.berDomain + + let testNoAllowlistEntry = runCodensity (startDynamicBackend testBackend $ cfgExposeInvitationURLsTeamAllowlist ([] :: [String])) $ \_ -> do + (owner, tid, _) <- createTeam domain 1 + checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabledLocked + -- here we get a response with HTTP status 200 and feature status unchanged (disabled), which we find weird, but we're just testing the current behavior + -- a team that is not in the allow list cannot enable the feature, it will always be disabled and locked + -- even though the internal API request to enable it succeeds + assertSuccess =<< Internal.setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "enabled" + checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabledLocked + -- however, a request to the public API will fail + assertStatus 409 =<< Public.setTeamFeatureConfig owner tid "exposeInvitationURLsToTeamAdmin" (object ["status" .= "enabled"]) + assertSuccess =<< Internal.setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "disabled" + pure (owner, tid) + + -- Happy case: DB has no config for the team + (owner, tid) <- testNoAllowlistEntry + + -- Interesting case: The team is in the allow list + runCodensity (startDynamicBackend testBackend $ cfgExposeInvitationURLsTeamAllowlist [tid]) $ \_ -> do + -- when the team is in the allow list the lock status is implicitly unlocked + checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabled + assertSuccess =<< Internal.setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "enabled" + checkFeature "exposeInvitationURLsToTeamAdmin" owner tid enabled + assertSuccess =<< Internal.setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "disabled" + checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabled + assertSuccess =<< Internal.setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "enabled" + checkFeature "exposeInvitationURLsToTeamAdmin" owner tid enabled + + -- Interesting case: The team had the feature enabled but is not in allow list + void testNoAllowlistEntry diff --git a/integration/test/Test/FeatureFlags/Mls.hs b/integration/test/Test/FeatureFlags/Mls.hs new file mode 100644 index 00000000000..73cc96eaf12 --- /dev/null +++ b/integration/test/Test/FeatureFlags/Mls.hs @@ -0,0 +1,102 @@ +module Test.FeatureFlags.Mls where + +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude + +testMls :: (HasCallStack) => APIAccess -> App () +testMls access = + do + user <- randomUser OwnDomain def + uid <- asString $ user %. "id" + mkFeatureTests "mls" + & addUpdate (mls1 uid) + & addUpdate mls2 + & addInvalidUpdate mlsInvalidConfig + & runFeatureTests OwnDomain access + +testMlsPatch :: (HasCallStack) => App () +testMlsPatch = do + mlsMigrationDefaultConfig <- defAllFeatures %. "mlsMigration.config" + withModifiedBackend + def + { galleyCfg = + setField + "settings.featureFlags.mlsMigration.defaults" + ( object + [ "lockStatus" .= "locked", + "status" .= "disabled", + "config" .= mlsMigrationDefaultConfig + ] + ) + } + $ \domain -> do + checkPatch domain "mls" $ object ["lockStatus" .= "locked"] + checkPatch domain "mls" $ object ["status" .= "enabled"] + checkPatch domain "mls" + $ object ["lockStatus" .= "locked", "status" .= "enabled"] + checkPatch domain "mls" + $ object + [ "status" .= "enabled", + "config" + .= object + [ "protocolToggleUsers" .= ([] :: [String]), + "defaultProtocol" .= "mls", + "supportedProtocols" .= ["proteus", "mls"], + "allowedCipherSuites" .= ([1] :: [Int]), + "defaultCipherSuite" .= toJSON (1 :: Int) + ] + ] + checkPatch domain "mls" + $ object + [ "config" + .= object + [ "protocolToggleUsers" .= ([] :: [String]), + "defaultProtocol" .= "mls", + "supportedProtocols" .= ["proteus", "mls"], + "allowedCipherSuites" .= ([1] :: [Int]), + "defaultCipherSuite" .= toJSON (1 :: Int) + ] + ] + +mls1 :: String -> Value +mls1 uid = + object + [ "status" .= "enabled", + "config" + .= object + [ "protocolToggleUsers" .= [uid], + "defaultProtocol" .= "mls", + "supportedProtocols" .= ["proteus", "mls"], + "allowedCipherSuites" .= ([1] :: [Int]), + "defaultCipherSuite" .= toJSON (1 :: Int) + ] + ] + +mls2 :: Value +mls2 = + object + [ "status" .= "enabled", + "config" + .= object + [ "protocolToggleUsers" .= ([] :: [String]), + "defaultProtocol" .= "mls", + "supportedProtocols" .= ["mls"], + "allowedCipherSuites" .= ([1] :: [Int]), + "defaultCipherSuite" .= toJSON (1 :: Int) + ] + ] + +mlsInvalidConfig :: Value +mlsInvalidConfig = + object + [ "status" .= "enabled", + "config" + .= object + [ "protocolToggleUsers" .= ([] :: [String]), + "defaultProtocol" .= "mls", + "supportedProtocols" .= ["proteus"], + "allowedCipherSuites" .= ([1] :: [Int]), + "defaultCipherSuite" .= toJSON (1 :: Int) + ] + ] diff --git a/integration/test/Test/FeatureFlags/MlsE2EId.hs b/integration/test/Test/FeatureFlags/MlsE2EId.hs new file mode 100644 index 00000000000..dee32f94be2 --- /dev/null +++ b/integration/test/Test/FeatureFlags/MlsE2EId.hs @@ -0,0 +1,121 @@ +module Test.FeatureFlags.MlsE2EId where + +import qualified API.Galley as Public +import qualified Data.Aeson as A +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude + +mlsE2EId1 :: Value +mlsE2EId1 = + object + [ "status" .= "enabled", + "config" + .= object + [ "crlProxy" .= "https://example.com", + "verificationExpiration" .= A.Number 86400, + "useProxyOnMobile" .= False + ] + ] + +testMLSE2EId :: (HasCallStack) => APIAccess -> App () +testMLSE2EId access = do + invalid <- + mlsE2EId1 + & if (access == InternalAPI) + then -- the internal API is not as strict as the public one, so we need to tweak the invalid config some more + setField "config.crlProxy" (object []) + else removeField "config.crlProxy" + mlsE2EId2 <- + mlsE2EId1 + & setField "config.verificationExpiration" (A.Number 86401) + & setField "config.useProxyOnMobile" True + mkFeatureTests "mlsE2EId" + & addUpdate mlsE2EId1 + & addUpdate mlsE2EId2 + & addInvalidUpdate invalid + & runFeatureTests OwnDomain access + +testPatchE2EId :: (HasCallStack) => App () +testPatchE2EId = do + checkPatch OwnDomain "mlsE2EId" (object ["lockStatus" .= "locked"]) + checkPatch OwnDomain "mlsE2EId" (object ["status" .= "enabled"]) + checkPatch OwnDomain "mlsE2EId" + $ object ["lockStatus" .= "locked", "status" .= "enabled"] + checkPatch OwnDomain "mlsE2EId" + $ object + [ "lockStatus" .= "unlocked", + "config" + .= object + [ "crlProxy" .= "https://example.com", + "verificationExpiration" .= A.Number 86401, + "useProxyOnMobile" .= True + ] + ] + + checkPatch OwnDomain "mlsE2EId" + $ object + [ "config" + .= object + [ "crlProxy" .= "https://example.com", + "verificationExpiration" .= A.Number 86401, + "useProxyOnMobile" .= True + ] + ] + +testMlsE2EConfigCrlProxyRequired :: (HasCallStack) => App () +testMlsE2EConfigCrlProxyRequired = do + (owner, tid, _) <- createTeam OwnDomain 1 + let configWithoutCrlProxy = + object + [ "config" + .= object + [ "useProxyOnMobile" .= False, + "verificationExpiration" .= A.Number 86400 + ], + "status" .= "enabled" + ] + + -- From API version 6 onwards, the CRL proxy is required, so the request should fail when it's not provided + bindResponse (Public.setTeamFeatureConfig owner tid "mlsE2EId" configWithoutCrlProxy) $ \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "mls-e2eid-missing-crl-proxy" + + configWithCrlProxy <- + configWithoutCrlProxy + & setField "config.useProxyOnMobile" True + & setField "config.crlProxy" "https://crl-proxy.example.com" + & setField "status" "enabled" + + -- The request should succeed when the CRL proxy is provided + bindResponse (Public.setTeamFeatureConfig owner tid "mlsE2EId" configWithCrlProxy) $ \resp -> do + resp.status `shouldMatchInt` 200 + + -- Assert that the feature config got updated correctly + expectedResponse <- configWithCrlProxy & setField "lockStatus" "unlocked" & setField "ttl" "unlimited" + checkFeature "mlsE2EId" owner tid expectedResponse + +testMlsE2EConfigCrlProxyNotRequiredInV5 :: (HasCallStack) => App () +testMlsE2EConfigCrlProxyNotRequiredInV5 = do + (owner, tid, _) <- createTeam OwnDomain 1 + let configWithoutCrlProxy = + object + [ "config" + .= object + [ "useProxyOnMobile" .= False, + "verificationExpiration" .= A.Number 86400 + ], + "status" .= "enabled" + ] + + -- In API version 5, the CRL proxy is not required, so the request should succeed + bindResponse (Public.setTeamFeatureConfigVersioned (ExplicitVersion 5) owner tid "mlsE2EId" configWithoutCrlProxy) $ \resp -> do + resp.status `shouldMatchInt` 200 + + -- Assert that the feature config got updated correctly + expectedResponse <- + configWithoutCrlProxy + & setField "lockStatus" "unlocked" + & setField "ttl" "unlimited" + & setField "config.crlProxy" "https://crlproxy.example.com" + checkFeature "mlsE2EId" owner tid expectedResponse diff --git a/integration/test/Test/FeatureFlags/MlsMigration.hs b/integration/test/Test/FeatureFlags/MlsMigration.hs new file mode 100644 index 00000000000..bac309fa5bb --- /dev/null +++ b/integration/test/Test/FeatureFlags/MlsMigration.hs @@ -0,0 +1,79 @@ +module Test.FeatureFlags.MlsMigration where + +import qualified API.Galley as Public +import qualified API.GalleyInternal as Internal +import qualified Data.Aeson as A +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude + +testMlsMigration :: (HasCallStack) => APIAccess -> App () +testMlsMigration access = do + -- first we have to enable mls + (owner, tid, _) <- createTeam OwnDomain 0 + void $ Public.setTeamFeatureConfig owner tid "mls" mlsEnable >>= getJSON 200 + mkFeatureTests "mlsMigration" + & addUpdate mlsMigrationConfig1 + & addUpdate mlsMigrationConfig2 + & setOwner owner + >>= runFeatureTests OwnDomain access + +testMlsMigrationDefaults :: (HasCallStack) => App () +testMlsMigrationDefaults = do + withModifiedBackend + def + { galleyCfg = setField "settings.featureFlags.mlsMigration.defaults.lockStatus" "unlocked" + } + $ \domain -> do + (owner, tid, _) <- createTeam domain 0 + void + $ Internal.patchTeamFeature owner tid "mls" (object ["status" .= "enabled"]) + >>= getJSON 200 + feat <- Internal.getTeamFeature owner tid "mlsMigration" >>= getJSON 200 + feat %. "config" `shouldMatch` mlsMigrationDefaultConfig + +mlsEnableConfig :: Value +mlsEnableConfig = + object + [ "protocolToggleUsers" .= ([] :: [String]), + "defaultProtocol" .= "mls", + "supportedProtocols" .= ["mls"], + "allowedCipherSuites" .= ([1] :: [Int]), + "defaultCipherSuite" .= A.Number 1 + ] + +mlsEnable :: Value +mlsEnable = + object + [ "status" .= "enabled", + "config" .= mlsEnableConfig + ] + +mlsMigrationDefaultConfig :: Value +mlsMigrationDefaultConfig = + object + [ "startTime" .= "2029-05-16T10:11:12.123Z", + "finaliseRegardlessAfter" .= "2029-10-17T00:00:00Z" + ] + +mlsMigrationConfig1 :: Value +mlsMigrationConfig1 = + object + [ "status" .= "enabled", + "config" + .= object + [ "startTime" .= "2029-05-16T10:11:12.123Z", + "finaliseRegardlessAfter" .= "2030-10-17T00:00:00Z" + ] + ] + +mlsMigrationConfig2 :: Value +mlsMigrationConfig2 = + object + [ "status" .= "enabled", + "config" + .= object + [ "startTime" .= "2030-05-16T10:11:12.123Z", + "finaliseRegardlessAfter" .= "2031-10-17T00:00:00Z" + ] + ] diff --git a/integration/test/Test/FeatureFlags/OutlookCalIntegration.hs b/integration/test/Test/FeatureFlags/OutlookCalIntegration.hs new file mode 100644 index 00000000000..8db8464a8d1 --- /dev/null +++ b/integration/test/Test/FeatureFlags/OutlookCalIntegration.hs @@ -0,0 +1,14 @@ +module Test.FeatureFlags.OutlookCalIntegration where + +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchOutlookCalIntegration :: (HasCallStack) => App () +testPatchOutlookCalIntegration = checkPatch OwnDomain "outlookCalIntegration" enabled + +testOutlookCalIntegration :: (HasCallStack) => APIAccess -> App () +testOutlookCalIntegration access = + mkFeatureTests "outlookCalIntegration" + & addUpdate enabled + & addUpdate disabled + & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/SSO.hs b/integration/test/Test/FeatureFlags/SSO.hs new file mode 100644 index 00000000000..7b633ddcb10 --- /dev/null +++ b/integration/test/Test/FeatureFlags/SSO.hs @@ -0,0 +1,36 @@ +module Test.FeatureFlags.SSO where + +import qualified API.Galley as Public +import qualified API.GalleyInternal as Internal +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude + +testSSODisabledByDefault :: (HasCallStack) => App () +testSSODisabledByDefault = do + let put uid tid = Internal.setTeamFeatureConfig uid tid "sso" (object ["status" .= "enabled"]) >>= assertSuccess + let patch uid tid = Internal.setTeamFeatureStatus uid tid "sso" "enabled" >>= assertSuccess + forM_ [put, patch] $ \enableFeature -> do + withModifiedBackend + def {galleyCfg = setField "settings.featureFlags.sso" "disabled-by-default"} + $ \domain -> do + (owner, tid, m : _) <- createTeam domain 2 + nonMember <- randomUser domain def + assertForbidden =<< Public.getTeamFeature nonMember tid "sso" + -- Test default + checkFeature "sso" m tid disabled + -- Test override + enableFeature owner tid + checkFeature "sso" owner tid enabled + +testSSOEnabledByDefault :: (HasCallStack) => App () +testSSOEnabledByDefault = do + withModifiedBackend + def {galleyCfg = setField "settings.featureFlags.sso" "enabled-by-default"} + $ \domain -> do + (owner, tid, _m : _) <- createTeam domain 2 + nonMember <- randomUser domain def + assertForbidden =<< Public.getTeamFeature nonMember tid "sso" + checkFeature "sso" owner tid enabled + -- check that the feature cannot be disabled + assertLabel 403 "not-implemented" =<< Internal.setTeamFeatureConfig owner tid "sso" (object ["status" .= "disabled"]) diff --git a/integration/test/Test/FeatureFlags/SearchVisibilityAvailable.hs b/integration/test/Test/FeatureFlags/SearchVisibilityAvailable.hs new file mode 100644 index 00000000000..a2ce39cd44e --- /dev/null +++ b/integration/test/Test/FeatureFlags/SearchVisibilityAvailable.hs @@ -0,0 +1,34 @@ +module Test.FeatureFlags.SearchVisibilityAvailable where + +import qualified API.Galley as Public +import qualified API.GalleyInternal as Internal +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchSearchVisibility :: (HasCallStack) => App () +testPatchSearchVisibility = checkPatch OwnDomain "searchVisibility" enabled + +testSearchVisibilityDisabledByDefault :: (HasCallStack) => App () +testSearchVisibilityDisabledByDefault = do + withModifiedBackend def {galleyCfg = setField "settings.featureFlags.teamSearchVisibility" "disabled-by-default"} $ \domain -> do + (owner, tid, m : _) <- createTeam domain 2 + -- Test default + checkFeature "searchVisibility" m tid disabled + assertSuccess =<< Internal.setTeamFeatureStatus owner tid "searchVisibility" "enabled" + checkFeature "searchVisibility" owner tid enabled + assertSuccess =<< Internal.setTeamFeatureStatus owner tid "searchVisibility" "disabled" + checkFeature "searchVisibility" owner tid disabled + +testSearchVisibilityEnabledByDefault :: (HasCallStack) => App () +testSearchVisibilityEnabledByDefault = do + withModifiedBackend def {galleyCfg = setField "settings.featureFlags.teamSearchVisibility" "enabled-by-default"} $ \domain -> do + (owner, tid, m : _) <- createTeam domain 2 + nonMember <- randomUser domain def + assertForbidden =<< Public.getTeamFeature nonMember tid "searchVisibility" + -- Test default + checkFeature "searchVisibility" m tid enabled + assertSuccess =<< Internal.setTeamFeatureStatus owner tid "searchVisibility" "disabled" + checkFeature "searchVisibility" owner tid disabled + assertSuccess =<< Internal.setTeamFeatureStatus owner tid "searchVisibility" "enabled" + checkFeature "searchVisibility" owner tid enabled diff --git a/integration/test/Test/FeatureFlags/SearchVisibilityInbound.hs b/integration/test/Test/FeatureFlags/SearchVisibilityInbound.hs new file mode 100644 index 00000000000..55d40c5c2f7 --- /dev/null +++ b/integration/test/Test/FeatureFlags/SearchVisibilityInbound.hs @@ -0,0 +1,32 @@ +module Test.FeatureFlags.SearchVisibilityInbound where + +import qualified API.Galley as Public +import qualified API.GalleyInternal as Internal +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude + +testFeatureNoConfigMultiSearchVisibilityInbound :: (HasCallStack) => App () +testFeatureNoConfigMultiSearchVisibilityInbound = do + (_owner1, team1, _) <- createTeam OwnDomain 0 + (_owner2, team2, _) <- createTeam OwnDomain 0 + + assertSuccess =<< Internal.setTeamFeatureStatus OwnDomain team2 "searchVisibilityInbound" "enabled" + + response <- Internal.getFeatureStatusMulti OwnDomain "searchVisibilityInbound" [team1, team2] + + statuses <- response.json %. "default_status" >>= asList + length statuses `shouldMatchInt` 2 + statuses `shouldMatchSet` [object ["team" .= team1, "status" .= "disabled"], object ["team" .= team2, "status" .= "enabled"]] + +testSearchVisibilityInboundInternal :: (HasCallStack) => APIAccess -> App () +testSearchVisibilityInboundInternal access = do + let featureName = "searchVisibilityInbound" + (alice, tid, _) <- createTeam OwnDomain 2 + eve <- randomUser OwnDomain def + assertForbidden =<< Public.getTeamFeature eve tid featureName + checkFeature featureName alice tid disabled + + void $ withWebSocket alice $ \ws -> do + setFlag access ws tid featureName enabled + setFlag access ws tid featureName disabled diff --git a/integration/test/Test/FeatureFlags/SelfDeletingMessages.hs b/integration/test/Test/FeatureFlags/SelfDeletingMessages.hs new file mode 100644 index 00000000000..019bed20341 --- /dev/null +++ b/integration/test/Test/FeatureFlags/SelfDeletingMessages.hs @@ -0,0 +1,35 @@ +module Test.FeatureFlags.SelfDeletingMessages where + +import qualified Data.Aeson.Types as A +import Test.FeatureFlags.Util +import Testlib.Prelude + +feature :: (ToJSON timeout) => [A.Pair] -> timeout -> Value +feature ps timeout = + object + ( ps + <> [ "ttl" .= "unlimited", + "config" .= object ["enforcedTimeoutSeconds" .= toJSON timeout] + ] + ) + +testSelfDeletingMessages :: (HasCallStack) => APIAccess -> App () +testSelfDeletingMessages access = + mkFeatureTests "selfDeletingMessages" + & addUpdate (feature ["status" .= "disabled"] (0 :: Int)) + & addUpdate (feature ["status" .= "enabled"] (30 :: Int)) + & addInvalidUpdate (feature ["status" .= "enabled"] "") + & runFeatureTests OwnDomain access + +testPatchSelfDeletingMessages :: (HasCallStack) => App () +testPatchSelfDeletingMessages = do + checkPatch OwnDomain "selfDeletingMessages" + $ object ["lockStatus" .= "locked"] + checkPatch OwnDomain "selfDeletingMessages" + $ object ["status" .= "disabled"] + checkPatch OwnDomain "selfDeletingMessages" + $ object ["lockStatus" .= "locked", "status" .= "disabled"] + checkPatch OwnDomain "selfDeletingMessages" + $ object ["lockStatus" .= "unlocked", "config" .= object ["enforcedTimeoutSeconds" .= A.Number 30]] + checkPatch OwnDomain "selfDeletingMessages" + $ object ["config" .= object ["enforcedTimeoutSeconds" .= A.Number 60]] diff --git a/integration/test/Test/FeatureFlags/SndFactorPasswordChallenge.hs b/integration/test/Test/FeatureFlags/SndFactorPasswordChallenge.hs new file mode 100644 index 00000000000..7acc3621f4e --- /dev/null +++ b/integration/test/Test/FeatureFlags/SndFactorPasswordChallenge.hs @@ -0,0 +1,16 @@ +module Test.FeatureFlags.SndFactorPasswordChallenge where + +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchSndFactorPasswordChallenge :: (HasCallStack) => App () +testPatchSndFactorPasswordChallenge = + checkPatch OwnDomain "sndFactorPasswordChallenge" enabled + +testSndFactorPasswordChallenge :: (HasCallStack) => APIAccess -> App () +testSndFactorPasswordChallenge access = + do + mkFeatureTests "sndFactorPasswordChallenge" + & addUpdate enabled + & addUpdate disabled + & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/User.hs b/integration/test/Test/FeatureFlags/User.hs new file mode 100644 index 00000000000..a6ebffbb25c --- /dev/null +++ b/integration/test/Test/FeatureFlags/User.hs @@ -0,0 +1,78 @@ +module Test.FeatureFlags.User where + +import qualified API.BrigInternal as I +import API.Galley +import qualified API.GalleyInternal as I +import SetupHelpers +import Testlib.Prelude + +testFeatureConferenceCallingForUser :: App () +testFeatureConferenceCallingForUser = do + (alice, tid, _) <- createTeam OwnDomain 0 -- team user + bob <- randomUser OwnDomain def -- non-team user + let featureName = "conferenceCalling" + + -- set initial value at the team level + let patch = + object + [ "lockStatus" .= "unlocked", + "status" .= "enabled", + "config" .= object ["useSFTForOneToOneCalls" .= True] + ] + + assertSuccess =<< I.patchTeamFeatureConfig OwnDomain tid featureName patch + + -- set user value for both users + for_ [alice, bob] $ \u -> do + void + $ I.putFeatureForUser + u + featureName + ( object + [ "status" .= "disabled" + ] + ) + >>= getBody 200 + I.getFeatureForUser u featureName `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + config <- resp.json + config %. "status" `shouldMatch` "disabled" + + -- this config is just made up by brig, it does not reflect the actual value + -- that will be returned to the user + config %. "config.useSFTForOneToOneCalls" `shouldMatch` False + + -- alice + do + getFeaturesForUser alice `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + config <- resp.json %. featureName + + -- alice is a team user, so her config reflects that of the team + config %. "status" `shouldMatch` "enabled" + config %. "config.useSFTForOneToOneCalls" `shouldMatch` True + + do + void $ I.deleteFeatureForUser alice featureName >>= getBody 200 + getFeaturesForUser alice `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + config <- resp.json %. featureName + config %. "status" `shouldMatch` "enabled" + config %. "config.useSFTForOneToOneCalls" `shouldMatch` True + + -- bob + do + getFeaturesForUser bob `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + config <- resp.json %. featureName + -- bob is not in a team, so we get his own personal settings here + config %. "status" `shouldMatch` "disabled" + -- but only for status, config is the server defaults + config %. "config.useSFTForOneToOneCalls" `shouldMatch` False + do + void $ I.deleteFeatureForUser bob featureName >>= getBody 200 + getFeaturesForUser bob `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + config <- resp.json %. featureName + config %. "status" `shouldMatch` "disabled" + config %. "config.useSFTForOneToOneCalls" `shouldMatch` False diff --git a/integration/test/Test/FeatureFlags/Util.hs b/integration/test/Test/FeatureFlags/Util.hs index 92426fd5f4f..bca97070383 100644 --- a/integration/test/Test/FeatureFlags/Util.hs +++ b/integration/test/Test/FeatureFlags/Util.hs @@ -20,8 +20,37 @@ module Test.FeatureFlags.Util where import qualified API.Galley as Public import qualified API.GalleyInternal as Internal import qualified Data.Aeson as A +import qualified Data.Aeson.KeyMap as KM +import qualified Data.Text as Text +import Notifications +import SetupHelpers import Testlib.Prelude +data APIAccess = InternalAPI | PublicAPI + deriving (Show, Eq) + +instance TestCases APIAccess where + mkTestCases = + pure + [ MkTestCase "[api=internal]" InternalAPI, + MkTestCase "[api=public]" PublicAPI + ] + +newtype Feature = Feature String + +instance TestCases Feature where + mkTestCases = pure $ case defAllFeatures of + Object obj -> do + feat <- KM.keys obj + let A.String nameT = toJSON feat + name = Text.unpack nameT + pure $ MkTestCase ("[feature=" <> name <> "]") (Feature name) + _ -> [] + +setFeature :: APIAccess -> Value -> String -> String -> Value -> App Response +setFeature InternalAPI = Internal.setTeamFeatureConfig +setFeature PublicAPI = Public.setTeamFeatureConfig + disabled :: Value disabled = object ["lockStatus" .= "unlocked", "status" .= "disabled", "ttl" .= "unlimited"] @@ -31,59 +60,286 @@ disabledLocked = object ["lockStatus" .= "locked", "status" .= "disabled", "ttl" enabled :: Value enabled = object ["lockStatus" .= "unlocked", "status" .= "enabled", "ttl" .= "unlimited"] -checkFeature :: (HasCallStack, MakesValue user, MakesValue tid) => String -> user -> tid -> Value -> App () -checkFeature = checkFeatureWith shouldMatch +defEnabledObj :: Value -> Value +defEnabledObj conf = + object + [ "lockStatus" .= "unlocked", + "status" .= "enabled", + "ttl" .= "unlimited", + "config" .= conf + ] + +defAllFeatures :: Value +defAllFeatures = + object + [ "legalhold" .= disabled, + "sso" .= disabled, + "searchVisibility" .= disabled, + "validateSAMLemails" .= enabled, + "digitalSignatures" .= disabled, + "appLock" .= defEnabledObj (object ["enforceAppLock" .= False, "inactivityTimeoutSecs" .= A.Number 60]), + "fileSharing" .= enabled, + "classifiedDomains" .= defEnabledObj (object ["domains" .= ["example.com"]]), + "conferenceCalling" .= confCalling def {lockStatus = Just "locked"}, + "selfDeletingMessages" + .= defEnabledObj (object ["enforcedTimeoutSeconds" .= A.Number 0]), + "conversationGuestLinks" .= enabled, + "sndFactorPasswordChallenge" .= disabledLocked, + "mls" + .= object + [ "lockStatus" .= "unlocked", + "status" .= "disabled", + "ttl" .= "unlimited", + "config" + .= object + [ "protocolToggleUsers" .= ([] :: [String]), + "defaultProtocol" .= "proteus", + "supportedProtocols" .= ["proteus", "mls"], + "allowedCipherSuites" .= ([1] :: [Int]), + "defaultCipherSuite" .= A.Number 1 + ] + ], + "searchVisibilityInbound" .= disabled, + "exposeInvitationURLsToTeamAdmin" .= disabledLocked, + "outlookCalIntegration" .= disabledLocked, + "mlsE2EId" + .= object + [ "lockStatus" .= "unlocked", + "status" .= "disabled", + "ttl" .= "unlimited", + "config" + .= object + [ "verificationExpiration" .= A.Number 86400, + "useProxyOnMobile" .= False, + "crlProxy" .= "https://crlproxy.example.com" + ] + ], + "mlsMigration" + .= object + [ "lockStatus" .= "locked", + "status" .= "enabled", + "ttl" .= "unlimited", + "config" + .= object + [ "startTime" .= "2029-05-16T10:11:12.123Z", + "finaliseRegardlessAfter" .= "2029-10-17T00:00:00Z" + ] + ], + "enforceFileDownloadLocation" + .= object + [ "lockStatus" .= "unlocked", + "status" .= "disabled", + "ttl" .= "unlimited", + "config" + .= object + [ "enforcedDownloadLocation" .= "downloads" + ] + ], + "limitedEventFanout" .= disabled + ] -checkFeatureWith :: (HasCallStack, MakesValue user, MakesValue tid, MakesValue expected) => (App Value -> expected -> App ()) -> String -> user -> tid -> expected -> App () -checkFeatureWith shouldMatch' feature user tid expected = do +hasExplicitLockStatus :: String -> Bool +hasExplicitLockStatus "fileSharing" = True +hasExplicitLockStatus "conferenceCalling" = True +hasExplicitLockStatus "selfDeletingMessages" = True +hasExplicitLockStatus "guestLinks" = True +hasExplicitLockStatus "sndFactorPasswordChallenge" = True +hasExplicitLockStatus "outlookCalIntegration" = True +hasExplicitLockStatus "enforceFileDownloadLocation" = True +hasExplicitLockStatus _ = False + +checkFeature :: (HasCallStack, MakesValue user, MakesValue tid) => String -> user -> tid -> Value -> App () +checkFeature feature user tid expected = do tidStr <- asString tid domain <- objDomain user bindResponse (Internal.getTeamFeature domain tidStr feature) $ \resp -> do resp.status `shouldMatchInt` 200 - resp.json `shouldMatch'` expected + resp.json `shouldMatch` expected bindResponse (Public.getTeamFeatures user tid) $ \resp -> do resp.status `shouldMatchInt` 200 - resp.json %. feature `shouldMatch'` expected + resp.json %. feature `shouldMatch` expected bindResponse (Public.getTeamFeature user tid feature) $ \resp -> do resp.status `shouldMatchInt` 200 - resp.json `shouldMatch'` expected + resp.json `shouldMatch` expected bindResponse (Public.getFeatureConfigs user) $ \resp -> do resp.status `shouldMatchInt` 200 - resp.json %. feature `shouldMatch'` expected - -checkFeatureLenientTtl :: (HasCallStack, MakesValue user, MakesValue tid) => String -> user -> tid -> Value -> App () -checkFeatureLenientTtl = checkFeatureWith shouldMatchLenientTtl - where - shouldMatchLenientTtl :: App Value -> Value -> App () - shouldMatchLenientTtl actual expected = do - expectedLockStatus <- expected %. "lockStatus" - actual %. "lockStatus" `shouldMatch` expectedLockStatus - expectedStatus <- expected %. "status" - actual %. "status" `shouldMatch` expectedStatus - mExpectedConfig <- lookupField expected "config" - mActualConfig <- lookupField actual "config" - mActualConfig `shouldMatch` mExpectedConfig - expectedTtl <- expected %. "ttl" - actualTtl <- actual %. "ttl" - checkTtl actualTtl expectedTtl - - checkTtl :: Value -> Value -> App () - checkTtl (A.String a) (A.String b) = do - a `shouldMatch` "unlimited" - b `shouldMatch` "unlimited" - checkTtl _ (A.String _) = assertFailure "expected the actual ttl to be unlimited, but it was limited" - checkTtl (A.String _) _ = assertFailure "expected the actual ttl to be limited, but it was unlimited" - checkTtl (A.Number actualTtl) (A.Number expectedTtl) = do - assertBool - ("expected the actual TTL to be greater than 0 and equal to or no more than 2 seconds less than " <> show expectedTtl <> ", but it was " <> show actualTtl) - ( actualTtl - > 0 - && actualTtl - <= expectedTtl - && abs (actualTtl - expectedTtl) - <= 2 - ) - checkTtl _ _ = assertFailure "unexpected ttl value(s)" + resp.json %. feature `shouldMatch` expected assertForbidden :: (HasCallStack) => Response -> App () assertForbidden = assertLabel 403 "no-team-member" + +data ConfCalling = ConfCalling + { lockStatus :: Maybe String, + status :: String, + sft :: Value + } + +instance Default ConfCalling where + def = + ConfCalling + { lockStatus = Nothing, + status = "disabled", + sft = toJSON False + } + +confCalling :: ConfCalling -> Value +confCalling args = + object + $ ["lockStatus" .= s | s <- toList args.lockStatus] + <> ["ttl" .= "unlimited"] + <> [ "status" .= args.status, + "config" + .= object ["useSFTForOneToOneCalls" .= args.sft] + ] + +setFlag :: (HasCallStack) => APIAccess -> WebSocket -> String -> String -> Value -> App () +setFlag access ws tid featureName value = do + update <- removeField "ttl" value + void + $ setFeature access ws.user tid featureName update + >>= getJSON 200 + expected <- + setField "ttl" "unlimited" + =<< setField "lockStatus" "unlocked" value + + -- should receive an event + do + notif <- awaitMatch isFeatureConfigUpdateNotif ws + notif %. "payload.0.name" `shouldMatch` featureName + notif %. "payload.0.data" `shouldMatch` expected + + checkFeature featureName ws.user tid expected + +checkPatch :: + (HasCallStack, MakesValue domain) => + domain -> + String -> + Value -> + App () +checkPatch domain featureName patch = do + (owner, tid, _) <- createTeam domain 0 + defFeature <- defAllFeatures %. featureName + + let valueOrDefault :: String -> App Value + valueOrDefault key = do + mValue <- lookupField patch key + maybe (defFeature %. key) pure mValue + + checkFeature featureName owner tid defFeature + void + $ Internal.patchTeamFeature domain tid featureName patch + >>= getJSON 200 + patched <- Internal.getTeamFeature domain tid featureName >>= getJSON 200 + checkFeature featureName owner tid patched + lockStatus <- patched %. "lockStatus" >>= asString + if lockStatus == "locked" + then do + -- if lock status is locked the feature status should fall back to the default + patched `shouldMatch` (defFeature & setField "lockStatus" "locked") + -- if lock status is locked, it was either locked before or changed by the patch + mPatchedLockStatus <- lookupField patch "lockStatus" + case mPatchedLockStatus of + Just ls -> ls `shouldMatch` "locked" + Nothing -> defFeature %. "lockStatus" `shouldMatch` "locked" + else do + patched %. "status" `shouldMatch` valueOrDefault "status" + mPatchedConfig <- lookupField patched "config" + case mPatchedConfig of + Just patchedConfig -> patchedConfig `shouldMatch` valueOrDefault "config" + Nothing -> do + mDefConfig <- lookupField defFeature "config" + assertBool "patch had an unexpected config field" (isNothing mDefConfig) + + when (hasExplicitLockStatus featureName) $ do + -- if lock status is unlocked, it was either unlocked before or changed + -- by the patch + mPatchedLockStatus <- lookupField patch "lockStatus" + case mPatchedLockStatus of + Just ls -> ls `shouldMatch` "unlocked" + Nothing -> defFeature %. "lockStatus" `shouldMatch` "unlocked" + +data FeatureTests = FeatureTests + { name :: String, + -- | valid config values used to update the feature setting (should not + -- include the lock status and ttl, as these are not part of the request + -- payload) + updates :: [Value], + invalidUpdates :: [Value], + owner :: Maybe Value + } + +mkFeatureTests :: String -> FeatureTests +mkFeatureTests name = FeatureTests name [] [] Nothing + +addUpdate :: Value -> FeatureTests -> FeatureTests +addUpdate up ft = ft {updates = ft.updates <> [up]} + +addInvalidUpdate :: Value -> FeatureTests -> FeatureTests +addInvalidUpdate up ft = ft {invalidUpdates = ft.invalidUpdates <> [up]} + +setOwner :: (MakesValue user) => user -> FeatureTests -> App FeatureTests +setOwner owner ft = do + x <- make owner + pure ft {owner = Just x} + +runFeatureTests :: + (HasCallStack, MakesValue domain) => + domain -> + APIAccess -> + FeatureTests -> + App () +runFeatureTests domain access ft = do + defFeature <- defAllFeatures %. ft.name + -- personal user + do + user <- randomUser domain def + bindResponse (Public.getFeatureConfigs user) $ \resp -> do + resp.status `shouldMatchInt` 200 + feat <- resp.json %. ft.name + lockStatus <- feat %. "lockStatus" + expected <- setField "lockStatus" lockStatus defFeature + feat `shouldMatch` expected + + -- make team + (owner, tid) <- case ft.owner of + Nothing -> do + (owner, tid, _) <- createTeam domain 0 + pure (owner, tid) + Just owner -> do + tid <- owner %. "team" & asString + pure (owner, tid) + checkFeature ft.name owner tid defFeature + + -- lock the feature + Internal.setTeamFeatureLockStatus owner tid ft.name "locked" + bindResponse (Public.getTeamFeature owner tid ft.name) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "lockStatus" `shouldMatch` "locked" + expected <- setField "lockStatus" "locked" defFeature + checkFeature ft.name owner tid expected + + for_ ft.updates $ (setFeature access owner tid ft.name >=> getJSON 409) + + -- unlock the feature + Internal.setTeamFeatureLockStatus owner tid ft.name "unlocked" + void $ withWebSocket owner $ \ws -> do + for_ ft.updates $ \update -> do + setFlag access ws tid ft.name update + + for_ ft.invalidUpdates $ \update -> do + void $ setFeature access owner tid ft.name update >>= getJSON 400 + assertNoEvent 2 ws + + -- lock again, should be set to default value + Internal.setTeamFeatureLockStatus owner tid ft.name "locked" + do + expected <- setField "lockStatus" "locked" defFeature + checkFeature ft.name owner tid expected + + -- unlock again, should be set to the last update + Internal.setTeamFeatureLockStatus owner tid ft.name "unlocked" + for_ (take 1 (reverse ft.updates)) $ \update -> do + expected <- + setField "ttl" "unlimited" + =<< setField "lockStatus" "unlocked" update + checkFeature ft.name owner tid expected diff --git a/integration/test/Test/FeatureFlags/ValidateSAMLEmails.hs b/integration/test/Test/FeatureFlags/ValidateSAMLEmails.hs new file mode 100644 index 00000000000..6177c52be87 --- /dev/null +++ b/integration/test/Test/FeatureFlags/ValidateSAMLEmails.hs @@ -0,0 +1,17 @@ +module Test.FeatureFlags.ValidateSAMLEmails where + +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchValidateSAMLEmails :: (HasCallStack) => App () +testPatchValidateSAMLEmails = + checkPatch OwnDomain "validateSAMLemails" + $ object ["status" .= "disabled"] + +testValidateSAMLEmailsInternal :: (HasCallStack) => App () +testValidateSAMLEmailsInternal = do + (alice, tid, _) <- createTeam OwnDomain 0 + withWebSocket alice $ \ws -> do + setFlag InternalAPI ws tid "validateSAMLemails" disabled + setFlag InternalAPI ws tid "validateSAMLemails" enabled diff --git a/integration/test/Test/LegalHold.hs b/integration/test/Test/LegalHold.hs index 22195f3afdb..e8cc0b22743 100644 --- a/integration/test/Test/LegalHold.hs +++ b/integration/test/Test/LegalHold.hs @@ -35,6 +35,7 @@ import Data.ProtoLens.Labels () import qualified Data.Set as Set import qualified Data.Text as T import GHC.Stack +import MLS.Util import Network.Wai (Request (pathInfo, requestMethod)) import Notifications import Numeric.Lens (hex) @@ -46,9 +47,9 @@ import Testlib.Prekeys import Testlib.Prelude import UnliftIO (Chan, readChan, timeout) -testLHPreventAddingNonConsentingUsers :: App () -testLHPreventAddingNonConsentingUsers = do - withMockServer def lhMockApp $ \lhDomAndPort _chan -> do +testLHPreventAddingNonConsentingUsers :: LhApiVersion -> App () +testLHPreventAddingNonConsentingUsers v = do + withMockServer def (lhMockAppV v) $ \lhDomAndPort _chan -> do (owner, tid, [alice, alex]) <- createTeam OwnDomain 3 legalholdWhitelistTeam tid owner >>= assertSuccess @@ -277,8 +278,8 @@ testLHDeleteClientManually = do -- other unspecific client error. resp.json %. "message" `shouldMatch` "LegalHold clients cannot be deleted. LegalHold must be disabled on this user by an admin" -testLHRequestDevice :: App () -testLHRequestDevice = do +testLHRequestDevice :: LhApiVersion -> App () +testLHRequestDevice v = do (alice, tid, [bob]) <- createTeam OwnDomain 2 let reqNotEnabled requester requestee = requestLegalHoldDevice tid requester requestee @@ -289,7 +290,7 @@ testLHRequestDevice = do lpk <- getLastPrekey pks <- replicateM 3 getPrekey - withMockServer def (lhMockAppWithPrekeys MkCreateMock {nextLastPrey = pure lpk, somePrekeys = pure pks}) \lhDomAndPort _chan -> do + withMockServer def (lhMockAppWithPrekeys v MkCreateMock {nextLastPrey = pure lpk, somePrekeys = pure pks}) \lhDomAndPort _chan -> do let statusShouldBe :: String -> App () statusShouldBe status = legalholdUserStatus tid alice bob `bindResponse` \resp -> do @@ -424,8 +425,8 @@ testLHApproveDevice = do outsiderClient <- objId $ addClient outsider def `bindResponse` getJSON 201 assertNoNotifications outsider outsiderClient Nothing isUserLegalholdEnabledNotif -testLHGetDeviceStatus :: App () -testLHGetDeviceStatus = do +testLHGetDeviceStatus :: LhApiVersion -> App () +testLHGetDeviceStatus v = do -- team users -- alice (team owner) and bob (member) (alice, tid, [bob]) <- createTeam OwnDomain 2 @@ -439,7 +440,7 @@ testLHGetDeviceStatus = do withMockServer def - do lhMockAppWithPrekeys MkCreateMock {nextLastPrey = pure lpk, somePrekeys = pure pks} + do lhMockAppWithPrekeys v MkCreateMock {nextLastPrey = pure lpk, somePrekeys = pure pks} \lhDomAndPort _chan -> do legalholdWhitelistTeam tid alice >>= assertStatus 200 @@ -526,8 +527,8 @@ testLHDisableForUser = do shouldBeEmpty lhClients -testLHEnablePerTeam :: App () -testLHEnablePerTeam = do +testLHEnablePerTeam :: LhApiVersion -> App () +testLHEnablePerTeam v = do -- team users -- alice (team owner) and bob (member) (alice, tid, [bob]) <- createTeam OwnDomain 2 @@ -536,7 +537,7 @@ testLHEnablePerTeam = do resp.json %. "lockStatus" `shouldMatch` "unlocked" resp.json %. "status" `shouldMatch` "disabled" - withMockServer def lhMockApp \lhDomAndPort _chan -> do + withMockServer def (lhMockAppV v) \lhDomAndPort _chan -> do setUpLHDevice tid alice bob lhDomAndPort legalholdUserStatus tid alice bob `bindResponse` \resp -> do @@ -551,8 +552,8 @@ testLHEnablePerTeam = do resp.status `shouldMatchInt` 200 resp.json %. "status" `shouldMatch` "enabled" -testLHGetMembersIncludesStatus :: App () -testLHGetMembersIncludesStatus = do +testLHGetMembersIncludesStatus :: LhApiVersion -> App () +testLHGetMembersIncludesStatus v = do -- team users -- alice (team owner) and bob (member) (alice, tid, [bob]) <- createTeam OwnDomain 2 @@ -568,7 +569,7 @@ testLHGetMembersIncludesStatus = do bobMember %. "legalhold_status" `shouldMatch` status statusShouldBe "no_consent" - withMockServer def lhMockApp \lhDomAndPort _chan -> do + withMockServer def (lhMockAppV v) \lhDomAndPort _chan -> do statusShouldBe "no_consent" legalholdWhitelistTeam tid alice @@ -593,26 +594,8 @@ testLHGetMembersIncludesStatus = do -- bob has accepted the legalhold device statusShouldBe "enabled" -type TB s = TaggedBool s - -enableLH :: (MakesValue tid, MakesValue teamAdmin, MakesValue targetUser, HasCallStack) => tid -> teamAdmin -> targetUser -> Bool -> App (Maybe String) -enableLH tid teamAdmin targetUser approveLH = do - -- alice requests a legalhold device for herself - requestLegalHoldDevice tid teamAdmin targetUser - >>= assertStatus 201 - - when approveLH do - approveLegalHoldDevice tid targetUser defPassword - >>= assertStatus 200 - legalholdUserStatus tid targetUser targetUser `bindResponse` \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "status" `shouldMatch` if approveLH then "enabled" else "pending" - if approveLH - then Just <$> lhDeviceIdOf targetUser - else pure Nothing - -testLHConnectionsWithNonConsentingUsers :: App () -testLHConnectionsWithNonConsentingUsers = do +testLHConnectionsWithNonConsentingUsers :: LhApiVersion -> App () +testLHConnectionsWithNonConsentingUsers v = do (alice, tid, []) <- createTeam OwnDomain 1 bob <- randomUser OwnDomain def carl <- randomUser OwnDomain def @@ -621,7 +604,7 @@ testLHConnectionsWithNonConsentingUsers = do legalholdWhitelistTeam tid alice >>= assertStatus 200 - withMockServer def lhMockApp \lhDomAndPort _chan -> do + withMockServer def (lhMockAppV v) \lhDomAndPort _chan -> do postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 @@ -672,8 +655,8 @@ testLHConnectionsWithNonConsentingUsers = do resp.status `shouldMatchInt` 200 resp.json %. "members.others.0.qualified_id" `shouldMatch` objQidObject alice -testLHConnectionsWithConsentingUsers :: App () -testLHConnectionsWithConsentingUsers = do +testLHConnectionsWithConsentingUsers :: LhApiVersion -> App () +testLHConnectionsWithConsentingUsers v = do (alice, teamA, []) <- createTeam OwnDomain 1 (bob, teamB, [barbara]) <- createTeam OwnDomain 2 @@ -682,7 +665,7 @@ testLHConnectionsWithConsentingUsers = do legalholdWhitelistTeam teamB bob >>= assertStatus 200 - withMockServer def lhMockApp \lhDomAndPort _chan -> do + withMockServer def (lhMockAppV v) \lhDomAndPort _chan -> do postLegalHoldSettings teamA alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 @@ -782,8 +765,8 @@ testLHNoConsentRemoveFromGroup approvedOrPending admin = do LHApproved -> assertLabel 403 "access-denied" LHPending -> assertStatus 200 -testLHHappyFlow :: App () -testLHHappyFlow = do +testLHHappyFlow :: LhApiVersion -> App () +testLHHappyFlow v = do (alice, tid, [bob]) <- createTeam OwnDomain 2 let statusShouldBe :: String -> App () statusShouldBe status = @@ -795,7 +778,7 @@ testLHHappyFlow = do lpk <- getLastPrekey pks <- replicateM 3 getPrekey - withMockServer def (lhMockAppWithPrekeys MkCreateMock {nextLastPrey = pure lpk, somePrekeys = pure pks}) \lhDomAndPort _chan -> do + withMockServer def (lhMockAppWithPrekeys v MkCreateMock {nextLastPrey = pure lpk, somePrekeys = pure pks}) \lhDomAndPort _chan -> do postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 -- implicit consent @@ -827,8 +810,8 @@ testLHHappyFlow = do >>= assertJust "client id is present" resp.json %. "last_prekey" `shouldMatch` lpk -testLHGetStatus :: App () -testLHGetStatus = do +testLHGetStatus :: LhApiVersion -> App () +testLHGetStatus v = do (alice, tid, [bob]) <- createTeam OwnDomain 2 (charlie, _tidCharlie, [debora]) <- createTeam OwnDomain 2 emil <- randomUser OwnDomain def @@ -843,7 +826,7 @@ testLHGetStatus = do check u bob "no_consent" check u emil "no_consent" legalholdWhitelistTeam tid alice >>= assertStatus 200 - withMockServer def lhMockApp \lhDomAndPort _chan -> do + withMockServer def (lhMockAppV v) \lhDomAndPort _chan -> do postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 for_ [alice, bob, charlie, debora, emil] \u -> do check u bob "disabled" @@ -852,14 +835,14 @@ testLHGetStatus = do approveLegalHoldDevice tid bob defPassword >>= assertStatus 200 check debora bob "enabled" -testLHCannotCreateGroupWithUsersInConflict :: App () -testLHCannotCreateGroupWithUsersInConflict = do +testLHCannotCreateGroupWithUsersInConflict :: LhApiVersion -> App () +testLHCannotCreateGroupWithUsersInConflict v = do (alice, tidAlice, [bob]) <- createTeam OwnDomain 2 (charlie, _tidCharlie, [debora]) <- createTeam OwnDomain 2 legalholdWhitelistTeam tidAlice alice >>= assertStatus 200 connectTwoUsers bob charlie connectTwoUsers bob debora - withMockServer def lhMockApp \lhDomAndPort _chan -> do + withMockServer def (lhMockAppV v) \lhDomAndPort _chan -> do postLegalHoldSettings tidAlice alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 postConversation bob defProteus {qualifiedUsers = [charlie, alice], newUsersRole = "wire_member", team = Just tidAlice} >>= assertStatus 201 @@ -873,8 +856,8 @@ testLHCannotCreateGroupWithUsersInConflict = do postConversation bob defProteus {qualifiedUsers = [debora, alice], newUsersRole = "wire_member", team = Just tidAlice} >>= assertLabel 403 "missing-legalhold-consent" -testLHNoConsentCannotBeInvited :: (HasCallStack) => App () -testLHNoConsentCannotBeInvited = do +testLHNoConsentCannotBeInvited :: (HasCallStack) => LhApiVersion -> App () +testLHNoConsentCannotBeInvited v = do -- team that is legalhold whitelisted (legalholder, tidLH, userLHNotActivated : _) <- createTeam OwnDomain 2 legalholdWhitelistTeam tidLH legalholder >>= assertStatus 200 @@ -885,7 +868,7 @@ testLHNoConsentCannotBeInvited = do connectUsers [peer, userLHNotActivated] connectUsers [peer2, userLHNotActivated] - withMockServer def lhMockApp \lhDomAndPort _chan -> do + withMockServer def (lhMockAppV v) \lhDomAndPort _chan -> do postLegalHoldSettings tidLH legalholder (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 cid <- postConversation userLHNotActivated defProteus {qualifiedUsers = [legalholder], newUsersRole = "wire_admin", team = Just tidLH} >>= getJSON 201 addMembers userLHNotActivated cid (def {users = [peer], role = Just "wire_admin"}) >>= assertSuccess @@ -905,12 +888,12 @@ testLHNoConsentCannotBeInvited = do addMembers userLHNotActivated cid (def {users = [peer3]}) >>= assertLabel 403 "not-connected" -testLHDisableBeforeApproval :: (HasCallStack) => App () -testLHDisableBeforeApproval = do +testLHDisableBeforeApproval :: (HasCallStack) => LhApiVersion -> App () +testLHDisableBeforeApproval v = do (alice, tid, [bob]) <- createTeam OwnDomain 2 legalholdWhitelistTeam tid alice >>= assertStatus 200 - withMockServer def lhMockApp \lhDomAndPort _chan -> do + withMockServer def (lhMockAppV v) \lhDomAndPort _chan -> do postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 -- alice requests a legalhold device for bob and sets his status to "pending" @@ -922,3 +905,160 @@ testLHDisableBeforeApproval = do disableLegalHold tid alice bob defPassword >>= assertStatus 200 getBob'sStatus `shouldMatch` "disabled" + +-- --------- +-- WPB-10783 +-- --------- +testBlockLHForMLSUsers :: (HasCallStack) => App () +testBlockLHForMLSUsers = do + -- scenario 1: + -- if charlie is in any MLS conversation, he cannot approve to be put under legalhold + (charlie, tid, []) <- createTeam OwnDomain 1 + [charlie1] <- traverse (createMLSClient def) [charlie] + void $ createNewGroup charlie1 + void $ createAddCommit charlie1 [charlie] >>= sendAndConsumeCommitBundle + + legalholdWhitelistTeam tid charlie >>= assertStatus 200 + withMockServer def lhMockApp \lhDomAndPort _chan -> do + postLegalHoldSettings tid charlie (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 + requestLegalHoldDevice tid charlie charlie `bindResponse` do + assertLabel 409 "mls-legal-hold-not-allowed" + +-- --------- +-- WPB-10772 +-- --------- + +-- | scenario 2.1: +-- charlie first is put under legalhold and after that wants to join an MLS conversation +-- claiming a keypackage of charlie to add them to a conversation should not be possible +testBlockClaimingKeyPackageForLHUsers :: (HasCallStack) => App () +testBlockClaimingKeyPackageForLHUsers = do + (alice, tid, [charlie]) <- createTeam OwnDomain 2 + [alice1, charlie1] <- traverse (createMLSClient def) [alice, charlie] + _ <- uploadNewKeyPackage charlie1 + _ <- createNewGroup alice1 + legalholdWhitelistTeam tid alice >>= assertStatus 200 + withMockServer def lhMockApp \lhDomAndPort _chan -> do + postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 + requestLegalHoldDevice tid alice charlie >>= assertSuccess + approveLegalHoldDevice tid (charlie %. "qualified_id") defPassword >>= assertSuccess + profile <- getUser alice charlie >>= getJSON 200 + pStatus <- profile %. "legalhold_status" & asString + pStatus `shouldMatch` "enabled" + + mls <- getMLSState + claimKeyPackages mls.ciphersuite alice1 charlie + `bindResponse` assertLabel 409 "mls-legal-hold-not-allowed" + +-- | scenario 2.2: +-- charlie is put under legalhold but creates an MLS Group himself +-- since he doesn't need to claim his own keypackage to do so, this would succeed +-- we need to check upon group creation if the user is under legalhold and reject +-- the operation if they are +testBlockCreateMLSConvForLHUsers :: (HasCallStack) => LhApiVersion -> App () +testBlockCreateMLSConvForLHUsers v = do + (alice, tid, [charlie]) <- createTeam OwnDomain 2 + [alice1, charlie1] <- traverse (createMLSClient def) [alice, charlie] + _ <- uploadNewKeyPackage alice1 + legalholdWhitelistTeam tid alice >>= assertStatus 200 + withMockServer def (lhMockAppV v) \lhDomAndPort _chan -> do + postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 + requestLegalHoldDevice tid alice charlie >>= assertSuccess + approveLegalHoldDevice tid (charlie %. "qualified_id") defPassword >>= assertSuccess + profile <- getUser alice charlie >>= getJSON 200 + pStatus <- profile %. "legalhold_status" & asString + pStatus `shouldMatch` "enabled" + + -- charlie tries to create a group and should fail when POSTing the add commit + _ <- createNewGroup charlie1 + + void + -- we try to add alice since adding charlie himself would trigger 2.1 + -- since he'd try to claim his own keypackages + $ createAddCommit charlie1 [alice] + >>= \mp -> + postMLSCommitBundle mp.sender (mkBundle mp) + `bindResponse` assertLabel 409 "mls-legal-hold-not-allowed" + + -- (unsurprisingly) this same thing should also work in the one2one case + + respJson <- getMLSOne2OneConversation alice charlie >>= getJSON 200 + resetGroup alice1 (respJson %. "conversation") + + void + -- we try to add alice since adding charlie himself would trigger 2.1 + -- since he'd try to claim his own keypackages + $ createAddCommit charlie1 [alice] + >>= \mp -> + postMLSCommitBundle mp.sender (mkBundle mp) + `bindResponse` assertLabel 409 "mls-legal-hold-not-allowed" + +testLHApiV1 :: App () +testLHApiV1 = do + (alice, tid, [bob]) <- createTeam OwnDomain 2 + + legalholdWhitelistTeam tid alice >>= assertSuccess + + withMockServer def (lhMockAppV V1) \lhDomAndPort chan -> do + postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 + + checkChan chan \(req, _) -> runMaybeT . lift $ do + BS8.unpack req.requestMethod `shouldMatch` "GET" + req.pathInfo `shouldMatch` (T.pack <$> ["legalhold", "status"]) + + requestLegalHoldDevice tid alice bob >>= assertStatus 201 + + checkChan chan \(req, _) -> runMaybeT . lift $ do + BS8.unpack req.requestMethod `shouldMatch` "GET" + req.pathInfo `shouldMatch` (T.pack <$> ["legalhold", "api-version"]) + + checkChan chan \(req, body) -> runMaybeT . lift $ do + BS8.unpack req.requestMethod `shouldMatch` "POST" + req.pathInfo `shouldMatch` (T.pack <$> ["legalhold", "v1", "initiate"]) + let (Just (value :: Value)) = decode body + value %. "team_id" `shouldMatch` tid + value %. "qualified_user_id.id" `shouldMatch` objId bob + value %. "qualified_user_id.domain" `shouldMatch` objDomain bob + + approveLegalHoldDevice tid (bob %. "qualified_id") defPassword >>= assertSuccess + + checkChan chan \(req, _) -> runMaybeT . lift $ do + BS8.unpack req.requestMethod `shouldMatch` "GET" + req.pathInfo `shouldMatch` (T.pack <$> ["legalhold", "api-version"]) + + checkChan chan \(req, body) -> runMaybeT . lift $ do + BS8.unpack req.requestMethod `shouldMatch` "POST" + req.pathInfo `shouldMatch` (T.pack <$> ["legalhold", "v1", "confirm"]) + let (Just (value :: Value)) = decode body + value %. "team_id" `shouldMatch` tid + value %. "qualified_user_id.id" `shouldMatch` objId bob + value %. "qualified_user_id.domain" `shouldMatch` objDomain bob + (isJust <$> value `lookupField` "client_id") `shouldMatch` True + + disableLegalHold tid alice bob defPassword >>= assertStatus 200 + + checkChan chan \(req, _) -> runMaybeT . lift $ do + BS8.unpack req.requestMethod `shouldMatch` "GET" + req.pathInfo `shouldMatch` (T.pack <$> ["legalhold", "api-version"]) + + checkChan chan \(req, body) -> runMaybeT . lift $ do + BS8.unpack req.requestMethod `shouldMatch` "POST" + req.pathInfo `shouldMatch` (T.pack <$> ["legalhold", "v1", "remove"]) + let (Just (value :: Value)) = decode body + value %. "team_id" `shouldMatch` tid + value %. "qualified_user_id.id" `shouldMatch` objId bob + value %. "qualified_user_id.domain" `shouldMatch` objDomain bob + +testNoCommonVersion :: App () +testNoCommonVersion = do + (alice, tid, [bob]) <- createTeam OwnDomain 2 + + legalholdWhitelistTeam tid alice >>= assertSuccess + + withMockServer def lhMockNoCommonVersion \lhDomAndPort _ -> do + legalholdWhitelistTeam tid alice >>= assertStatus 200 + postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertSuccess + + bindResponse (requestLegalHoldDevice tid alice bob) $ \resp -> do + resp.status `shouldMatchInt` 500 + resp.json %. "label" `shouldMatch` "server-error" diff --git a/integration/test/Test/Login.hs b/integration/test/Test/Login.hs index 096f441a50f..56ded5e6026 100644 --- a/integration/test/Test/Login.hs +++ b/integration/test/Test/Login.hs @@ -23,6 +23,7 @@ testLoginVerify6DigitEmailCodeSuccess = do bindResponse (loginWith2ndFactor owner email defPassword code) $ \resp -> do resp.status `shouldMatchInt` 200 +-- @SF.Channel @TSFI.RESTfulAPI @S2 -- -- Test that login fails with wrong second factor email verification code testLoginVerify6DigitWrongCodeFails :: (HasCallStack) => App () @@ -38,6 +39,9 @@ testLoginVerify6DigitWrongCodeFails = do resp.status `shouldMatchInt` 403 resp.json %. "label" `shouldMatch` "code-authentication-failed" +-- @END + +-- @SF.Channel @TSFI.RESTfulAPI @S2 -- -- Test that login without verification code fails if SndFactorPasswordChallenge feature is enabled in team testLoginVerify6DigitMissingCodeFails :: (HasCallStack) => App () @@ -50,6 +54,9 @@ testLoginVerify6DigitMissingCodeFails = do resp.status `shouldMatchInt` 403 resp.json %. "label" `shouldMatch` "code-authentication-required" +-- @END + +-- @SF.Channel @TSFI.RESTfulAPI @S2 -- -- Test that login fails with expired second factor email verification code testLoginVerify6DigitExpiredCodeFails :: (HasCallStack) => App () @@ -73,6 +80,8 @@ testLoginVerify6DigitExpiredCodeFails = do resp.status `shouldMatchInt` 403 resp.json %. "label" `shouldMatch` "code-authentication-failed" +-- @END + testLoginVerify6DigitResendCodeSuccessAndRateLimiting :: (HasCallStack) => App () testLoginVerify6DigitResendCodeSuccessAndRateLimiting = do (owner, team, []) <- createTeam OwnDomain 0 diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 07534701b85..f721f9ad06b 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -430,10 +430,47 @@ testRemoteRemoveClient suite = do shouldMatch (nPayload n %. "conversation") (objId conv) shouldMatch (nPayload n %. "from") (objId bob) - msg <- asByteString (nPayload n %. "data") >>= showMessage alice1 + mlsMsg <- asByteString (nPayload n %. "data") + + -- Checks that the remove proposal is consumable by alice + void $ mlsCliConsume alice1 mlsMsg + -- This doesn't work because `sendAndConsumeCommitBundle` doesn't like + -- remove proposals from the backend. We should fix that in future. + -- void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + + parsedMsg <- showMessage alice1 mlsMsg let leafIndexBob = 1 - msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexBob - msg %. "message.content.sender.External" `shouldMatchInt` 0 + parsedMsg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexBob + parsedMsg %. "message.content.sender.External" `shouldMatchInt` 0 + +testRemoteRemoveCreatorClient :: (HasCallStack) => Ciphersuite -> App () +testRemoteRemoveCreatorClient suite = do + setMLSCiphersuite suite + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] + void $ uploadNewKeyPackage bob1 + (_, conv) <- createNewGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + + withWebSocket bob $ \wsBob -> do + void $ deleteClient alice alice1.client >>= getBody 200 + let predicate n = nPayload n %. "type" `isEqual` "conversation.mls-message-add" + n <- awaitMatch predicate wsBob + shouldMatch (nPayload n %. "conversation") (objId conv) + shouldMatch (nPayload n %. "from") (objId alice) + + mlsMsg <- asByteString (nPayload n %. "data") + + -- Checks that the remove proposal is consumable by alice + void $ mlsCliConsume alice1 mlsMsg + -- This doesn't work because `sendAndConsumeCommitBundle` doesn't like + -- remove proposals from the backend. We should fix that in future. + -- void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + + parsedMsg <- showMessage alice1 mlsMsg + let leafIndexAlice = 0 + parsedMsg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexAlice + parsedMsg %. "message.content.sender.External" `shouldMatchInt` 0 testCreateSubConv :: (HasCallStack) => Ciphersuite -> App () testCreateSubConv suite = do @@ -492,6 +529,10 @@ testFirstCommitAllowsPartialAdds = do resp.status `shouldMatchInt` 409 resp.json %. "label" `shouldMatch` "mls-client-mismatch" +-- @SF.Separation @TSFI.RESTfulAPI @S2 +-- +-- This test verifies that the server rejects a commit containing add proposals +-- that only add a proper subset of the set of clients of a user. testAddUserPartial :: (HasCallStack) => App () testAddUserPartial = do [alice, bob, charlie] <- createAndConnectUsers (replicate 3 OwnDomain) @@ -519,6 +560,8 @@ testAddUserPartial = do err <- postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 409 err %. "label" `shouldMatch` "mls-client-mismatch" +-- @END + -- | admin removes user from a conversation but doesn't list all clients testRemoveClientsIncomplete :: (HasCallStack) => App () testRemoveClientsIncomplete = do @@ -704,6 +747,10 @@ testPropExistingConv = do res <- createAddProposals alice1 [bob] >>= traverse sendAndConsumeMessage >>= assertOne shouldBeEmpty (res %. "events") +-- @SF.Separation @TSFI.RESTfulAPI @S2 +-- +-- This test verifies that the server rejects any commit that does not +-- reference all pending proposals in an MLS group. testCommitNotReferencingAllProposals :: (HasCallStack) => App () testCommitNotReferencingAllProposals = do users@[_alice, bob, charlie] <- createAndConnectUsers (replicate 3 OwnDomain) @@ -728,6 +775,8 @@ testCommitNotReferencingAllProposals = do resp.status `shouldMatchInt` 400 resp.json %. "label" `shouldMatch` "mls-commit-missing-references" +-- @END + testUnsupportedCiphersuite :: (HasCallStack) => App () testUnsupportedCiphersuite = do setMLSCiphersuite (Ciphersuite "0x0003") diff --git a/integration/test/Test/MLS/KeyPackage.hs b/integration/test/Test/MLS/KeyPackage.hs index 507d7ff7eb3..5f95025a1da 100644 --- a/integration/test/Test/MLS/KeyPackage.hs +++ b/integration/test/Test/MLS/KeyPackage.hs @@ -238,7 +238,7 @@ testReplaceKeyPackages = do (kps, refs) <- unzip <$> replicateM 3 (generateKeyPackage alice1) -- replace old key packages with new - void $ replaceKeyPackages alice1 [suite] kps >>= getBody 201 + void $ replaceKeyPackages alice1 (Just [suite]) kps >>= getBody 201 checkCount def 4 checkCount suite 3 @@ -274,7 +274,50 @@ testReplaceKeyPackages = do setMLSCiphersuite suite kps2 <- replicateM 2 (fmap fst (generateKeyPackage alice1)) - void $ replaceKeyPackages alice1 [def, suite] (kps1 <> kps2) >>= getBody 201 + void $ replaceKeyPackages alice1 (Just [def, suite]) (kps1 <> kps2) >>= getBody 201 checkCount def 2 checkCount suite 2 + + do + setMLSCiphersuite def + defKeyPackages <- replicateM 3 (fmap fst (generateKeyPackage alice1)) + setMLSCiphersuite suite + suiteKeyPackages <- replicateM 3 (fmap fst (generateKeyPackage alice1)) + + void + $ replaceKeyPackages alice1 (Just []) [] + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + + void + $ replaceKeyPackages alice1 Nothing defKeyPackages + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + + checkCount def 3 + checkCount suite 2 + + let testErrorCases :: (HasCallStack) => Maybe [Ciphersuite] -> [ByteString] -> App () + testErrorCases ciphersuites keyPackages = do + void + $ replaceKeyPackages alice1 ciphersuites keyPackages + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "mls-protocol-error" + checkCount def 3 + checkCount suite 2 + + testErrorCases (Just []) defKeyPackages + testErrorCases (Just []) suiteKeyPackages + testErrorCases Nothing [] + testErrorCases Nothing suiteKeyPackages + testErrorCases Nothing (suiteKeyPackages <> defKeyPackages) + + testErrorCases (Just [suite]) defKeyPackages + testErrorCases (Just [suite]) (suiteKeyPackages <> defKeyPackages) + testErrorCases (Just [suite]) [] + + testErrorCases (Just [def]) suiteKeyPackages + testErrorCases (Just [def]) (suiteKeyPackages <> defKeyPackages) + testErrorCases (Just [def]) [] diff --git a/integration/test/Test/MLS/Keys.hs b/integration/test/Test/MLS/Keys.hs index d5ac4867c60..299262cc0e5 100644 --- a/integration/test/Test/MLS/Keys.hs +++ b/integration/test/Test/MLS/Keys.hs @@ -1,16 +1,42 @@ module Test.MLS.Keys where import API.Galley +import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Base64.URL as B64U import qualified Data.ByteString.Char8 as B8 import SetupHelpers import Testlib.Prelude -testPublicKeys :: (HasCallStack) => App () -testPublicKeys = do +testRawPublicKeys :: (HasCallStack) => App () +testRawPublicKeys = do u <- randomUserId OwnDomain keys <- getMLSPublicKeys u >>= getJSON 200 + do + pubkeyS <- keys %. "removal.ed25519" & asString + pubkey <- assertOne . toList . B64.decode $ B8.pack pubkeyS + B8.length pubkey `shouldMatchInt` 32 + + do + pubkeyS <- keys %. "removal.ecdsa_secp256r1_sha256" & asString + pubkey <- assertOne . toList . B64.decode $ B8.pack pubkeyS + B8.length pubkey `shouldMatchInt` 65 + + do + pubkeyS <- keys %. "removal.ecdsa_secp384r1_sha384" & asString + pubkey <- assertOne . toList . B64.decode $ B8.pack pubkeyS + B8.length pubkey `shouldMatchInt` 97 + + do + pubkeyS <- keys %. "removal.ecdsa_secp521r1_sha512" & asString + pubkey <- assertOne . toList . B64.decode $ B8.pack pubkeyS + B8.length pubkey `shouldMatchInt` 133 + +testJWKPublicKeys :: (HasCallStack) => App () +testJWKPublicKeys = do + u <- randomUserId OwnDomain + keys <- getMLSPublicKeysJWK u >>= getJSON 200 + do keys %. "removal.ed25519.crv" `shouldMatch` "Ed25519" keys %. "removal.ed25519.kty" `shouldMatch` "OKP" diff --git a/integration/test/Test/MLS/Message.hs b/integration/test/Test/MLS/Message.hs index e15635f4987..81a194d3674 100644 --- a/integration/test/Test/MLS/Message.hs +++ b/integration/test/Test/MLS/Message.hs @@ -26,9 +26,14 @@ import Notifications import SetupHelpers import Testlib.Prelude --- | Test happy case of federated MLS message sending in both directions. +-- @SF.Separation @TSFI.RESTfulAPI @S2 +-- This test verifies whether a message actually gets sent all the way to +-- cannon. + testApplicationMessage :: (HasCallStack) => App () testApplicationMessage = do + -- Test happy case of federated MLS message sending in both directions. + -- local alice and alex, remote bob [alice, alex, bob, betty] <- createUsers @@ -55,6 +60,8 @@ testApplicationMessage = do void $ createApplicationMessage bob1 "hey" >>= sendAndConsumeMessage traverse_ (awaitMatch isNewMLSMessageNotif) wss +-- @END + testAppMessageSomeReachable :: (HasCallStack) => App () testAppMessageSomeReachable = do alice1 <- startDynamicBackends [mempty] $ \[thirdDomain] -> do diff --git a/integration/test/Test/MLS/One2One.hs b/integration/test/Test/MLS/One2One.hs index 338cae3a7e4..d93e5f582c2 100644 --- a/integration/test/Test/MLS/One2One.hs +++ b/integration/test/Test/MLS/One2One.hs @@ -19,6 +19,8 @@ module Test.MLS.One2One where import API.Brig import API.Galley +import Control.Concurrent.Async +import Control.Concurrent.MVar import qualified Data.ByteString.Base64 as Base64 import qualified Data.ByteString.Char8 as B8 import qualified Data.Set as Set @@ -29,29 +31,29 @@ import Notifications import SetupHelpers import Test.Version import Testlib.Prelude +import Testlib.VersionedFed -testGetMLSOne2One :: (HasCallStack) => Version5 -> Domain -> App () -testGetMLSOne2One v otherDomain = withVersion5 v $ do - [alice, bob] <- createAndConnectUsers [OwnDomain, otherDomain] - +testGetMLSOne2OneLocalV5 :: (HasCallStack) => App () +testGetMLSOne2OneLocalV5 = withVersion5 Version5 $ do + [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] let assertConvData conv = do conv %. "epoch" `shouldMatchInt` 0 - case v of - Version5 -> conv %. "cipher_suite" `shouldMatchInt` 1 - NoVersion5 -> assertFieldMissing conv "cipher_suite" + conv %. "cipher_suite" `shouldMatchInt` 1 - conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 - conv %. "type" `shouldMatchInt` 2 - shouldBeEmpty (conv %. "members.others") + convId <- + getMLSOne2OneConversationLegacy alice bob `bindResponse` \resp -> do + conv <- getJSON 200 resp + conv %. "type" `shouldMatchInt` 2 + shouldBeEmpty (conv %. "members.others") - conv %. "members.self.conversation_role" `shouldMatch` "wire_member" - conv %. "members.self.qualified_id" `shouldMatch` (alice %. "qualified_id") - assertConvData conv + conv %. "members.self.conversation_role" `shouldMatch` "wire_member" + conv %. "members.self.qualified_id" `shouldMatch` (alice %. "qualified_id") + assertConvData conv - convId <- conv %. "qualified_id" + conv %. "qualified_id" -- check that the conversation has the same ID on the other side - conv2 <- bindResponse (getMLSOne2OneConversation bob alice) $ \resp -> do + conv2 <- bindResponse (getMLSOne2OneConversationLegacy bob alice) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json @@ -59,21 +61,69 @@ testGetMLSOne2One v otherDomain = withVersion5 v $ do conv2 %. "qualified_id" `shouldMatch` convId assertConvData conv2 +testGetMLSOne2OneRemoteV5 :: (HasCallStack) => App () +testGetMLSOne2OneRemoteV5 = withVersion5 Version5 $ do + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + getMLSOne2OneConversationLegacy alice bob `bindResponse` \resp -> do + resp.status `shouldMatchInt` 400 + resp.jsonBody %. "label" `shouldMatch` "mls-federated-one2one-not-supported" + + getMLSOne2OneConversationLegacy bob alice `bindResponse` \resp -> do + resp.status `shouldMatchInt` 400 + resp.jsonBody %. "label" `shouldMatch` "mls-federated-one2one-not-supported" + +testGetMLSOne2One :: (HasCallStack) => Domain -> App () +testGetMLSOne2One bobDomain = do + [alice, bob] <- createAndConnectUsers [OwnDomain, bobDomain] + bobDomainStr <- asString bobDomain + let assertConvData conv = do + conv %. "epoch" `shouldMatchInt` 0 + assertFieldMissing conv "cipher_suite" + + mlsOne2OneConv <- + getMLSOne2OneConversation alice bob `bindResponse` \resp -> do + one2oneConv <- getJSON 200 resp + convOwnerDomain <- asString $ one2oneConv %. "conversation.qualified_id.domain" + let user = if convOwnerDomain == bobDomainStr then bob else alice + ownerDomainPublicKeys <- getMLSPublicKeys user >>= getJSON 200 + + one2oneConv %. "public_keys" `shouldMatch` ownerDomainPublicKeys + + conv <- one2oneConv %. "conversation" + conv %. "type" `shouldMatchInt` 2 + shouldBeEmpty (conv %. "members.others") + conv %. "members.self.conversation_role" `shouldMatch` "wire_member" + conv %. "members.self.qualified_id" `shouldMatch` (alice %. "qualified_id") + assertConvData conv + + pure one2oneConv + + -- check that the conversation has the same ID on the other side + mlsOne2OneConv2 <- bindResponse (getMLSOne2OneConversation bob alice) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json + + conv2 <- mlsOne2OneConv2 %. "conversation" + conv2 %. "type" `shouldMatchInt` 2 + conv2 %. "qualified_id" `shouldMatch` (mlsOne2OneConv %. "conversation.qualified_id") + mlsOne2OneConv2 %. "public_keys" `shouldMatch` (mlsOne2OneConv %. "public_keys") + assertConvData conv2 + testMLSOne2OneOtherMember :: (HasCallStack) => One2OneScenario -> App () testMLSOne2OneOtherMember scenario = do alice <- randomUser OwnDomain def let otherDomain = one2OneScenarioUserDomain scenario convDomain = one2OneScenarioConvDomain scenario bob <- createMLSOne2OnePartner otherDomain alice convDomain - conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 do - convId <- conv %. "qualified_id" - bobConv <- getMLSOne2OneConversation bob alice >>= getJSON 200 - convId `shouldMatch` (bobConv %. "qualified_id") + convId <- one2OneConv %. "conversation.qualified_id" + bobOne2OneConv <- getMLSOne2OneConversation bob alice >>= getJSON 200 + convId `shouldMatch` (bobOne2OneConv %. "conversation.qualified_id") [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] traverse_ uploadNewKeyPackage [bob1] - resetGroup alice1 conv + resetOne2OneGroup alice1 one2OneConv withWebSocket bob1 $ \ws -> do commit <- createAddCommit alice1 [bob] void $ sendAndConsumeCommitBundle commit @@ -83,14 +133,47 @@ testMLSOne2OneOtherMember scenario = do -- Make sure the membership info is OK both for the MLS 1-to-1 endpoint and -- for the general conversation fetching endpoint. - let assertOthers other resp = do - bdy <- getJSON 200 resp - othersObj <- bdy %. "members.others" & asList + let assertOthers :: (HasCallStack, MakesValue other, MakesValue retrievedConv) => other -> retrievedConv -> App () + assertOthers other retrievedConv = do + othersObj <- retrievedConv %. "members.others" & asList otherActual <- assertOne othersObj otherActual %. "qualified_id" `shouldMatch` (other %. "qualified_id") forM_ [(alice, bob), (bob, alice)] $ \(self, other) -> do - getMLSOne2OneConversation self other `bindResponse` assertOthers other - getConversation self conv `bindResponse` assertOthers other + getMLSOne2OneConversation self other `bindResponse` \resp -> do + retrievedConv <- getJSON 200 resp >>= (%. "conversation") + assertOthers other retrievedConv + getConversation self (one2OneConv %. "conversation") `bindResponse` \resp -> do + retrievedConv <- getJSON 200 resp + assertOthers other retrievedConv + +testMLSOne2OneRemoveClientLocalV5 :: App () +testMLSOne2OneRemoveClientLocalV5 = withVersion5 Version5 $ do + [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] + conv <- getMLSOne2OneConversationLegacy alice bob >>= getJSON 200 + + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] + traverse_ uploadNewKeyPackage [bob1] + resetGroup alice1 conv + + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + + withWebSocket alice $ \wsAlice -> do + _ <- deleteClient bob bob1.client >>= getBody 200 + let predicate n = nPayload n %. "type" `isEqual` "conversation.mls-message-add" + n <- awaitMatch predicate wsAlice + shouldMatch (nPayload n %. "conversation") (objId conv) + shouldMatch (nPayload n %. "from") (objId bob) + + mlsMsg <- asByteString (nPayload n %. "data") + + -- Checks that the remove proposal is consumable by alice + void $ mlsCliConsume alice1 mlsMsg + + parsedMsg <- showMessage alice1 mlsMsg + let leafIndexBob = 1 + -- msg `shouldMatch` "foo" + parsedMsg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexBob + parsedMsg %. "message.content.sender.External" `shouldMatchInt` 0 testGetMLSOne2OneUnconnected :: (HasCallStack) => Domain -> App () testGetMLSOne2OneUnconnected otherDomain = do @@ -114,15 +197,15 @@ testMLSOne2OneBlockedAfterConnected scenario = do let otherDomain = one2OneScenarioUserDomain scenario convDomain = one2OneScenarioConvDomain scenario bob <- createMLSOne2OnePartner otherDomain alice convDomain - conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 - convId <- conv %. "qualified_id" + one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + convId <- one2OneConv %. "conversation.qualified_id" do bobConv <- getMLSOne2OneConversation bob alice >>= getJSON 200 - convId `shouldMatch` (bobConv %. "qualified_id") + convId `shouldMatch` (bobConv %. "conversation.qualified_id") [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] traverse_ uploadNewKeyPackage [bob1] - resetGroup alice1 conv + resetOne2OneGroup alice1 one2OneConv commit <- createAddCommit alice1 [bob] withWebSocket bob1 $ \ws -> do void $ sendAndConsumeCommitBundle commit @@ -153,15 +236,15 @@ testMLSOne2OneUnblocked scenario = do let otherDomain = one2OneScenarioUserDomain scenario convDomain = one2OneScenarioConvDomain scenario bob <- createMLSOne2OnePartner otherDomain alice convDomain - conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 do - convId <- conv %. "qualified_id" + convId <- one2OneConv %. "conversation.qualified_id" bobConv <- getMLSOne2OneConversation bob alice >>= getJSON 200 - convId `shouldMatch` (bobConv %. "qualified_id") + convId `shouldMatch` (bobConv %. "conversation.qualified_id") [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] traverse_ uploadNewKeyPackage [bob1] - resetGroup alice1 conv + resetOne2OneGroup alice1 one2OneConv withWebSocket bob1 $ \ws -> do commit <- createAddCommit alice1 [bob] void $ sendAndConsumeCommitBundle commit @@ -215,11 +298,12 @@ data One2OneScenario One2OneScenarioRemoteConv instance TestCases One2OneScenario where - testCases = - [ MkTestCase "[domain=own]" One2OneScenarioLocal, - MkTestCase "[domain=other;conv=own]" One2OneScenarioLocalConv, - MkTestCase "[domain=other;conv=other]" One2OneScenarioRemoteConv - ] + mkTestCases = + pure + [ MkTestCase "[domain=own]" One2OneScenarioLocal, + MkTestCase "[domain=other;conv=own]" One2OneScenarioLocalConv, + MkTestCase "[domain=other;conv=other]" One2OneScenarioRemoteConv + ] one2OneScenarioUserDomain :: One2OneScenario -> Domain one2OneScenarioUserDomain One2OneScenarioLocal = OwnDomain @@ -240,8 +324,8 @@ testMLSOne2One suite scenario = do [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] traverse_ uploadNewKeyPackage [bob1] - conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 - resetGroup alice1 conv + one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + resetOne2OneGroup alice1 one2OneConv commit <- createAddCommit alice1 [bob] withWebSocket bob1 $ \ws -> do @@ -264,6 +348,156 @@ testMLSOne2One suite scenario = do -- the cipersuite of this conversation. void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle - conv' <- getMLSOne2OneConversation alice bob >>= getJSON 200 + one2OneConv' <- getMLSOne2OneConversation alice bob >>= getJSON 200 (suiteCode, _) <- assertOne $ T.hexadecimal (T.pack suite.code) - conv' %. "cipher_suite" `shouldMatchInt` suiteCode + one2OneConv' %. "conversation.cipher_suite" `shouldMatchInt` suiteCode + +-- | This test verifies that one-to-one conversations are created inside the +-- commit lock. There used to be an issue where a conversation could be +-- partially created at the time of setting its ciphersuite, resulting in an +-- incomplete database entry that would prevent further uses of the +-- conversation. +testMLSGhostOne2OneConv :: App () +testMLSGhostOne2OneConv = do + [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] + [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] + traverse_ uploadNewKeyPackage [bob1, bob2] + one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + resetOne2OneGroup alice1 one2OneConv + + doneVar <- liftIO $ newEmptyMVar + let checkConversation = + liftIO (tryReadMVar doneVar) >>= \case + Nothing -> do + bindResponse (getConversation alice (one2OneConv %. "conversation")) $ \resp -> + resp.status `shouldMatchOneOf` [404 :: Int, 403, 200] + + checkConversation + Just _ -> pure () + checkConversationIO <- appToIO checkConversation + + createCommit <- + appToIO + $ void + $ createAddCommit alice1 [bob] + >>= sendAndConsumeCommitBundle + + liftIO $ withAsync checkConversationIO $ \a -> do + createCommit + liftIO $ putMVar doneVar () + wait a + +-- [NOTE: Federated 1:1 MLS Conversations] +-- 1:1 Conversations shouldn't work when there is no way for the creator to know +-- the MLS public keys of the backend which will host this conversation. In +-- federation API V2, this will always work and has been tested above. When one +-- of the backends doesn't support federation API v2, the 1:1 conversation can +-- still be created but only by the user whose backend hosts this conversation. + +-- | See Note: [Federated 1:1 MLS Conversations] +-- To run locally this test requires federation-v1 docker containers to be up and running. +-- See `deploy/dockerephemeral/run.sh` and comment on `StaticFedDomain` in `Testlib/VersionedFed.hs` for more details. +testMLSFederationV1ConvOnOldBackend :: App () +testMLSFederationV1ConvOnOldBackend = do + alice <- randomUser OwnDomain def + let createBob = do + bobCandidate <- randomUser (StaticFedDomain 1) def + connectUsers [alice, bobCandidate] + getMLSOne2OneConversation alice bobCandidate `bindResponse` \resp -> do + if resp.status == 533 + then pure bobCandidate + else createBob + + bob <- createBob + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] + traverse_ uploadNewKeyPackage [alice1] + + -- Alice cannot start this conversation because it would exist on Bob's + -- backend and Alice cannot get the MLS public keys of that backend. + getMLSOne2OneConversation alice bob `bindResponse` \resp -> do + fedError <- getJSON 533 resp + fedError %. "label" `shouldMatch` "federation-version-error" + + conv <- getMLSOne2OneConversationLegacy bob alice >>= getJSON 200 + keys <- getMLSPublicKeys bob >>= getJSON 200 + resetOne2OneGroupGeneric bob1 conv keys + + withWebSocket alice1 $ \wsAlice -> do + commit <- createAddCommit bob1 [alice] + void $ sendAndConsumeCommitBundle commit + + let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" + n <- awaitMatch isMessage wsAlice + nPayload n %. "data" `shouldMatch` B8.unpack (Base64.encode (fold commit.welcome)) + + withWebSocket bob1 $ \wsBob -> do + _ <- deleteClient alice alice1.client >>= getBody 200 + + let predicate n = nPayload n %. "type" `isEqual` "conversation.mls-message-add" + n <- awaitMatch predicate wsBob + shouldMatch (nPayload n %. "conversation") (objId conv) + shouldMatch (nPayload n %. "from") (objId alice) + + mlsMsg <- asByteString (nPayload n %. "data") + + -- Checks that the remove proposal is consumable by bob + void $ mlsCliConsume bob1 mlsMsg + + parsedMsg <- showMessage bob1 mlsMsg + let leafIndexAlice = 1 + parsedMsg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexAlice + parsedMsg %. "message.content.sender.External" `shouldMatchInt` 0 + +-- | See Note: Federated 1:1 MLS Conversations +-- To run locally this test requires federation-v1 docker containers to be up and running. +-- See `deploy/dockerephemeral/run.sh` and comment on `StaticFedDomain` in `Testlib/VersionedFed.hs` for more details. +testMLSFederationV1ConvOnNewBackend :: App () +testMLSFederationV1ConvOnNewBackend = do + alice <- randomUser OwnDomain def + let createBob = do + bobCandidate <- randomUser (StaticFedDomain 1) def + connectUsers [alice, bobCandidate] + getMLSOne2OneConversation alice bobCandidate `bindResponse` \resp -> do + if resp.status == 200 + then pure bobCandidate + else createBob + + bob <- createBob + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] + traverse_ uploadNewKeyPackage [bob1] + + -- Bob cannot start this conversation because it would exist on Alice's + -- backend and Bob cannot get the MLS public keys of that backend. + getMLSOne2OneConversationLegacy bob alice `bindResponse` \resp -> do + fedError <- getJSON 533 resp + fedError %. "label" `shouldMatch` "federation-remote-error" + + one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + conv <- one2OneConv %. "conversation" + resetOne2OneGroup alice1 one2OneConv + + withWebSocket bob1 $ \wsBob -> do + commit <- createAddCommit alice1 [bob] + void $ sendAndConsumeCommitBundle commit + + let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" + n <- awaitMatch isMessage wsBob + nPayload n %. "data" `shouldMatch` B8.unpack (Base64.encode (fold commit.welcome)) + + withWebSocket alice1 $ \wsAlice -> do + _ <- deleteClient bob bob1.client >>= getBody 200 + + let predicate n = nPayload n %. "type" `isEqual` "conversation.mls-message-add" + n <- awaitMatch predicate wsAlice + shouldMatch (nPayload n %. "conversation") (objId conv) + shouldMatch (nPayload n %. "from") (objId bob) + + mlsMsg <- asByteString (nPayload n %. "data") + + -- Checks that the remove proposal is consumable by bob + void $ mlsCliConsume alice1 mlsMsg + + parsedMsg <- showMessage alice1 mlsMsg + let leafIndexBob = 1 + parsedMsg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexBob + parsedMsg %. "message.content.sender.External" `shouldMatchInt` 0 diff --git a/integration/test/Test/MLS/Services.hs b/integration/test/Test/MLS/Services.hs new file mode 100644 index 00000000000..153023d1a36 --- /dev/null +++ b/integration/test/Test/MLS/Services.hs @@ -0,0 +1,96 @@ +{-# OPTIONS -Wno-ambiguous-fields #-} +module Test.MLS.Services where + +import API.Brig +import API.Common +import API.GalleyInternal (patchTeamFeatureConfig) +import SetupHelpers +import Testlib.JSON +import Testlib.Prelude + +testWhitelistUpdatePermissions :: (HasCallStack) => App () +testWhitelistUpdatePermissions = do + -- Create a team + (owner, tid, []) <- createTeam OwnDomain 1 + + -- Create a team admin + admin <- createTeamMember owner def {role = "admin"} + + -- Create a service + email <- randomEmail + provider <- make <$> setupProvider owner def {newProviderEmail = email} + providerId <- provider %. "id" & asString + service <- make <$> newService OwnDomain providerId def + + do + -- Check that a random user can't add the service to the whitelist + uid <- randomUser OwnDomain def + serviceId <- service %. "id" & asString + np <- + make + $ object + [ "id" .= serviceId, + "provider" .= providerId, + "whitelisted" .= True + ] + bindResponse (postServiceWhitelist uid tid np) $ \resp -> do + resp.status `shouldMatchInt` 403 + (resp.jsonBody %. "label") `shouldMatch` Just "insufficient-permissions" + + do + -- Check that an admin can add the service to the whitelist + serviceId <- service %. "id" & asString + np <- + make + $ object + [ "id" .= serviceId, + "provider" .= providerId, + "whitelisted" .= True + ] + postServiceWhitelist admin tid np >>= assertStatus 200 + + -- set team's defaultProtocol to MLS + mlsConfig <- + make + $ object + [ "config" + .= object + [ "allowedCipherSuites" .= [1 :: Int], + "defaultCipherSuite" .= (1 :: Int), + "defaultProtocol" .= "mls", + "protocolToggleUsers" .= ([] :: [String]), + "supportedProtocols" .= ["mls", "proteus"] + ], + "status" .= "enabled", + "ttl" .= "unlimited" + ] + patchTeamFeatureConfig OwnDomain tid "mls" mlsConfig >>= assertStatus 200 + + do + -- Check that a random user can't add the service to the whitelist + uid <- randomUser OwnDomain def + serviceId <- service %. "id" & asString + np <- + make + $ object + [ "id" .= serviceId, + "provider" .= providerId, + "whitelisted" .= True + ] + bindResponse (postServiceWhitelist uid tid np) $ \resp -> do + resp.status `shouldMatchInt` 409 + (resp.jsonBody %. "label") `shouldMatch` Just "mls-services-not-allowed" + + do + -- Check that an admin can't add the service to the whitelist + serviceId <- service %. "id" & asString + np <- + make + $ object + [ "id" .= serviceId, + "provider" .= providerId, + "whitelisted" .= True + ] + postServiceWhitelist admin tid np >>= \resp -> do + resp.status `shouldMatchInt` 409 + (resp.jsonBody %. "label") `shouldMatch` Just "mls-services-not-allowed" diff --git a/integration/test/Test/MLS/SubConversation.hs b/integration/test/Test/MLS/SubConversation.hs index 11dfdc4e7da..83c5376edf3 100644 --- a/integration/test/Test/MLS/SubConversation.hs +++ b/integration/test/Test/MLS/SubConversation.hs @@ -7,6 +7,7 @@ import qualified Data.Set as Set import MLS.Util import Notifications import SetupHelpers +import Test.MLS.One2One import Testlib.Prelude testJoinSubConv :: App () @@ -30,6 +31,60 @@ testJoinSubConv = do $ createExternalCommit alice1 Nothing >>= sendAndConsumeCommitBundle +testJoinOne2OneSubConv :: App () +testJoinOne2OneSubConv = do + [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] + [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] + traverse_ uploadNewKeyPackage [bob1, bob2] + one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + resetOne2OneGroup alice1 one2OneConv + + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + createOne2OneSubConv bob1 "conference" (one2OneConv %. "public_keys") + + -- bob adds his first client to the subconversation + sub' <- getSubConversation bob (one2OneConv %. "conversation") "conference" >>= getJSON 200 + do + tm <- sub' %. "epoch_timestamp" + assertBool "Epoch timestamp should not be null" (tm /= Null) + + -- now alice joins with her own client + void + $ createExternalCommit alice1 Nothing + >>= sendAndConsumeCommitBundle + +testLeaveOne2OneSubConv :: One2OneScenario -> Leaver -> App () +testLeaveOne2OneSubConv scenario leaver = do + -- set up 1-1 conversation + alice <- randomUser OwnDomain def + let otherDomain = one2OneScenarioUserDomain scenario + convDomain = one2OneScenarioConvDomain scenario + bob <- createMLSOne2OnePartner otherDomain alice convDomain + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] + traverse_ uploadNewKeyPackage [bob1] + one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + resetOne2OneGroup alice1 one2OneConv + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + + -- create and join subconversation + createOne2OneSubConv alice1 "conference" (one2OneConv %. "public_keys") + void $ createExternalCommit bob1 Nothing >>= sendAndConsumeCommitBundle + + -- one of the two clients leaves + let (leaverClient, leaverIndex, remainingClient) = case leaver of + Alice -> (alice1, 0, bob1) + Bob -> (bob1, 1, alice1) + + withWebSocket remainingClient $ \ws -> do + leaveCurrentConv leaverClient + + msg <- consumeMessage remainingClient Nothing ws + msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leaverIndex + msg %. "message.content.sender.External" `shouldMatchInt` 0 + + -- the other client commits the pending proposal + void $ createPendingProposalCommit remainingClient >>= sendAndConsumeCommitBundle + testDeleteParentOfSubConv :: (HasCallStack) => Domain -> App () testDeleteParentOfSubConv secondDomain = do (alice, tid, _) <- createTeam OwnDomain 1 @@ -205,7 +260,7 @@ testCreatorRemovesUserFromParent = do setMLSState childState let idxBob1 :: Int = 1 idxBob2 :: Int = 2 - for_ ((,) <$> [idxBob1, idxBob2] <*> [alice1, charlie1, charlie2] `zip` wss) \(idx, (consumer, ws)) -> do + for_ ((,) <$> [idxBob1, idxBob2] <*> wss) \(idx, ws) -> do msg <- awaitMatch do @@ -222,9 +277,8 @@ testCreatorRemovesUserFromParent = do lift do (== idx) <$> (prop %. "Remove.removed" & asInt) ws - msg %. "payload.0.data" - & asByteString - >>= mlsCliConsume consumer + for_ ws.client $ \consumer -> + msg %. "payload.0.data" & asByteString >>= mlsCliConsume consumer -- remove bob from the child state modifyMLSState $ \s -> s {members = s.members Set.\\ Set.fromList [bob1, bob2]} diff --git a/integration/test/Test/Notifications.hs b/integration/test/Test/Notifications.hs index b94060814ca..c9116d80ec9 100644 --- a/integration/test/Test/Notifications.hs +++ b/integration/test/Test/Notifications.hs @@ -93,12 +93,12 @@ testInvalidNotification = do $ getNotifications user def {since = Just notifId} >>= getJSON 404 --- | Check that client-add notifications use the V5 format: +-- | Check that client-add notifications use the V6 format: -- @ -- "capabilities": { "capabilities": [..] } -- @ -- --- Migration plan: clients must be able to parse both old and new schema starting from V6. Once V5 is deprecated, the backend can start sending notifications in the new form. +-- Migration plan: clients must be able to parse both old and new schema starting from V7. Once V6 is deprecated, the backend can start sending notifications in the new form. testAddClientNotification :: (HasCallStack) => App () testAddClientNotification = do alice <- randomUser OwnDomain def diff --git a/integration/test/Test/OAuth.hs b/integration/test/Test/OAuth.hs new file mode 100644 index 00000000000..2c018dedb61 --- /dev/null +++ b/integration/test/Test/OAuth.hs @@ -0,0 +1,137 @@ +module Test.OAuth where + +import API.Brig +import API.BrigInternal +import API.Common (defPassword) +import Data.String.Conversions +import Network.HTTP.Types +import Network.URI +import SetupHelpers +import Testlib.Prelude + +testOAuthRevokeSession :: (HasCallStack) => App () +testOAuthRevokeSession = do + user <- randomUser OwnDomain def + let uri = "https://example.com" + cid <- createOAuthClient user "foobar" uri >>= getJSON 200 >>= flip (%.) "client_id" + let scopes = ["write:conversations"] + + -- create a session that will be revoked later + (tokenToBeRevoked, sessionToBeRevoked) <- do + token <- generateAccessToken user cid scopes uri + [app] <- getOAuthApplications user >>= getJSON 200 >>= asList + [session] <- app %. "sessions" >>= asList + pure (token, session) + + -- create another session and assert that there are two sessions + validToken <- do + token <- generateAccessToken user cid scopes uri + [app] <- getOAuthApplications user >>= getJSON 200 >>= asList + sessions <- app %. "sessions" >>= asList + length sessions `shouldMatchInt` 2 + pure token + + -- attempt to revoke a session with a wrong password should fail + sessionToBeRevoked + %. "refresh_token_id" + >>= asString + >>= deleteOAuthSession user cid "foobar" + >>= assertStatus 403 + + -- revoke the first session and assert that there is only one session left + sessionToBeRevoked + %. "refresh_token_id" + >>= asString + >>= deleteOAuthSession user cid defPassword + >>= assertSuccess + [app] <- getOAuthApplications user >>= getJSON 200 >>= asList + sessions <- app %. "sessions" >>= asList + length sessions `shouldMatchInt` 1 + + -- try to use the revoked token and assert that it fails + tokenToBeRevoked + %. "refresh_token" + >>= asString + >>= createOAuthAccessTokenWithRefreshToken user cid + >>= assertStatus 403 + + -- try to use the valid token and assert that it works + validToken + %. "refresh_token" + >>= asString + >>= createOAuthAccessTokenWithRefreshToken user cid + >>= assertSuccess + +testRevokeApplicationAccountAccessV6 :: App () +testRevokeApplicationAccountAccessV6 = do + user <- randomUser OwnDomain def + bindResponse (getOAuthApplications user) $ \resp -> do + resp.status `shouldMatchInt` 200 + apps <- resp.json >>= asList + length apps `shouldMatchInt` 0 + let uri = "https://example.com" + let scopes = ["write:conversations"] + replicateM_ 3 $ do + cid <- createOAuthClient user "foobar" uri >>= getJSON 200 >>= flip (%.) "client_id" + generateAccessToken user cid scopes uri + [cid1, cid2, cid3] <- getOAuthApplications user >>= getJSON 200 >>= asList >>= mapM (%. "id") + revokeApplicationAccessV6 user cid1 >>= assertSuccess + bindResponse (getOAuthApplications user) $ \resp -> do + resp.status `shouldMatchInt` 200 + apps <- resp.json >>= asList + length apps `shouldMatchInt` 2 + ids <- for apps $ \app -> app %. "id" + ids `shouldMatchSet` [cid2, cid3] + revokeApplicationAccessV6 user cid2 >>= assertSuccess + bindResponse (getOAuthApplications user) $ \resp -> do + resp.status `shouldMatchInt` 200 + apps <- resp.json >>= asList + length apps `shouldMatchInt` 1 + ids <- for apps $ \app -> app %. "id" + ids `shouldMatchSet` [cid3] + revokeApplicationAccessV6 user cid3 >>= assertSuccess + bindResponse (getOAuthApplications user) $ \resp -> do + resp.status `shouldMatchInt` 200 + apps <- resp.json >>= asList + length apps `shouldMatchInt` 0 + +testRevokeApplicationAccountAccess :: App () +testRevokeApplicationAccountAccess = do + user <- randomUser OwnDomain def + bindResponse (getOAuthApplications user) $ \resp -> do + resp.status `shouldMatchInt` 200 + apps <- resp.json >>= asList + length apps `shouldMatchInt` 0 + let uri = "https://example.com" + let scopes = ["write:conversations"] + replicateM_ 3 $ do + cid <- createOAuthClient user "foobar" uri >>= getJSON 200 >>= flip (%.) "client_id" + generateAccessToken user cid scopes uri + [cid1, cid2, cid3] <- getOAuthApplications user >>= getJSON 200 >>= asList >>= mapM (%. "id") + revokeApplicationAccess user cid1 "foobar" >>= assertStatus 403 + revokeApplicationAccess user cid1 defPassword >>= assertSuccess + bindResponse (getOAuthApplications user) $ \resp -> do + resp.status `shouldMatchInt` 200 + apps <- resp.json >>= asList + length apps `shouldMatchInt` 2 + ids <- for apps $ \app -> app %. "id" + ids `shouldMatchSet` [cid2, cid3] + revokeApplicationAccess user cid2 defPassword >>= assertSuccess + bindResponse (getOAuthApplications user) $ \resp -> do + resp.status `shouldMatchInt` 200 + apps <- resp.json >>= asList + length apps `shouldMatchInt` 1 + ids <- for apps $ \app -> app %. "id" + ids `shouldMatchSet` [cid3] + revokeApplicationAccess user cid3 defPassword >>= assertSuccess + bindResponse (getOAuthApplications user) $ \resp -> do + resp.status `shouldMatchInt` 200 + apps <- resp.json >>= asList + length apps `shouldMatchInt` 0 + +generateAccessToken :: (MakesValue cid, MakesValue user) => user -> cid -> [String] -> String -> App Value +generateAccessToken user cid scopes uri = do + authCodeResponse <- generateOAuthAuthorizationCode user cid scopes uri + let location = fromMaybe (error "no location header") $ parseURI . cs . snd =<< locationHeader authCodeResponse + let code = maybe "no code query param" cs $ join $ lookup (cs "code") $ parseQuery $ cs location.uriQuery + createOAuthAccessToken user cid code uri >>= getJSON 200 diff --git a/integration/test/Test/PasswordReset.hs b/integration/test/Test/PasswordReset.hs new file mode 100644 index 00000000000..95a94ea3f27 --- /dev/null +++ b/integration/test/Test/PasswordReset.hs @@ -0,0 +1,105 @@ +module Test.PasswordReset where + +import API.Brig +import API.BrigInternal hiding (activate) +import API.Common +import SetupHelpers +import Testlib.Prelude + +-- @SF.Provisioning @TSFI.RESTfulAPI @S1 +-- +-- This test checks the password reset functionality of the application. +-- Besides a successful password reset the following scenarios are tested: +-- - Subsequent password reset requests should succeed without errors. +-- - Attempting to reset the password with an incorrect key or code should fail. +-- - Attempting to log in with the old password after a successful reset should fail. +-- - Attempting to log in with the new password after a successful reset should succeed. +-- - Attempting to reset the password again to the same new password should fail. +testPasswordResetShouldSucceedButFailOnWrongInputs :: (HasCallStack) => App () +testPasswordResetShouldSucceedButFailOnWrongInputs = do + u <- randomUser OwnDomain def + email <- u %. "email" & asString + passwordReset u email >>= assertSuccess + -- Even though a password reset is now in progress + -- we expect a successful response from a subsequent request to not leak any information + -- about the requested email. + passwordReset u email >>= assertSuccess + + (key, code) <- getPasswordResetData email + let newPassword = "newpassword" + + -- complete password reset with incorrect key/code should fail + completePasswordReset u "wrong-key" code newPassword >>= assertStatus 400 + login u email newPassword >>= assertStatus 403 + completePasswordReset u key "wrong-code" newPassword >>= assertStatus 400 + login u email newPassword >>= assertStatus 403 + + -- complete password reset with correct key and code should succeed + completePasswordReset u key code newPassword >>= assertSuccess + + -- try login with old password should fail + login u email defPassword >>= assertStatus 403 + -- login with new password should succeed + login u email newPassword >>= assertSuccess + -- reset password again to the same new password should fail + passwordReset u email >>= assertSuccess + (nextKey, nextCode) <- getPasswordResetData email + bindResponse (completePasswordReset u nextKey nextCode newPassword) $ \resp -> do + resp.status `shouldMatchInt` 409 + resp.json %. "label" `shouldMatch` "password-must-differ" + +-- @END + +testPasswordResetAfterEmailUpdate :: (HasCallStack) => App () +testPasswordResetAfterEmailUpdate = do + u <- randomUser OwnDomain def + email <- u %. "email" & asString + (cookie, token) <- bindResponse (login u email defPassword) $ \resp -> do + resp.status `shouldMatchInt` 200 + token <- resp.json %. "access_token" & asString + let cookie = fromJust $ getCookie "zuid" resp + pure ("zuid=" <> cookie, token) + + -- initiate email update + newEmail <- randomEmail + updateEmail u newEmail cookie token >>= assertSuccess + + -- initiate password reset + passwordReset u email >>= assertSuccess + (key, code) <- getPasswordResetData email + + -- activate new email + bindResponse (getActivationCode u newEmail) $ \resp -> do + resp.status `shouldMatchInt` 200 + activationKey <- resp.json %. "key" & asString + activationCode <- resp.json %. "code" & asString + activate u activationKey activationCode >>= assertSuccess + + bindResponse (getSelf u) $ \resp -> do + actualEmail <- resp.json %. "email" + actualEmail `shouldMatch` newEmail + + -- attempting to complete password reset should fail + bindResponse (completePasswordReset u key code "newpassword") $ \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "invalid-code" + +testPasswordResetInvalidPasswordLength :: App () +testPasswordResetInvalidPasswordLength = do + u <- randomUser OwnDomain def + email <- u %. "email" & asString + passwordReset u email >>= assertSuccess + (key, code) <- getPasswordResetData email + + -- complete password reset with a password that is too short should fail + let shortPassword = "123456" + completePasswordReset u key code shortPassword >>= assertStatus 400 + + -- try login with new password should fail + login u email shortPassword >>= assertStatus 403 + +getPasswordResetData :: String -> App (String, String) +getPasswordResetData email = do + bindResponse (getPasswordResetCode OwnDomain email) $ \resp -> do + resp.status `shouldMatchInt` 200 + (,) <$> (resp.json %. "key" & asString) <*> (resp.json %. "code" & asString) diff --git a/integration/test/Test/Property.hs b/integration/test/Test/Property.hs new file mode 100644 index 00000000000..40356c305bd --- /dev/null +++ b/integration/test/Test/Property.hs @@ -0,0 +1,141 @@ +module Test.Property where + +import API.Brig +import API.Common +import qualified Data.Map as Map +import SetupHelpers +import Testlib.Prelude + +testSetGetDeleteProperty :: App () +testSetGetDeleteProperty = do + user <- randomUser OwnDomain def + setProperty user "foo" "bar" `bindResponse` \resp -> + resp.status `shouldMatchInt` 200 + + getProperty user "foo" `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatch` toJSON "bar" + + deleteProperty user "foo" `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + + getProperty user "foo" `bindResponse` \resp -> do + resp.status `shouldMatchInt` 404 + +testGetProperties :: App () +testGetProperties = do + user <- randomUser OwnDomain def + -- Property names can only be printable ascii, using the handle function here + -- as a little shortcut. + propertyNames <- replicateM 16 $ randomHandleWithRange 8 20 + propertyVals <- replicateM 16 $ randomJSON + let properties = zip propertyNames propertyVals + forM_ properties $ \(prop, val) -> + setProperty user prop val `bindResponse` \resp -> + resp.status `shouldMatchInt` 200 + + getAllPropertyNames user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatchSet` propertyNames + + getAllPropertyValues user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatch` Map.fromList properties + +testClearProperties :: App () +testClearProperties = do + user <- randomUser OwnDomain def + + propertyNames <- replicateM 16 $ randomHandleWithRange 8 20 + propertyVals <- replicateM 16 $ randomJSON + let properties = zip propertyNames propertyVals + forM_ properties $ \(prop, val) -> + setProperty user prop val `bindResponse` \resp -> + resp.status `shouldMatchInt` 200 + + clearProperties user `bindResponse` \resp -> + resp.status `shouldMatchInt` 200 + + getAllPropertyNames user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatchSet` mempty @[String] + + getAllPropertyValues user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatch` Map.empty @String @Value + +testMaxProperties :: App () +testMaxProperties = do + user <- randomUser OwnDomain def + + -- This is hardcoded in the prod code. + let maxProperties = 16 + + propertyNames <- replicateM maxProperties $ randomHandleWithRange 8 20 + propertyVals <- replicateM maxProperties $ randomJSON + let properties = zip propertyNames propertyVals + forM_ properties $ \(prop, val) -> + setProperty user prop val `bindResponse` \resp -> + resp.status `shouldMatchInt` 200 + + seventeenthPropName <- randomHandleWithRange 8 20 + seventeenthPropVal <- randomJSON + + -- cannot set seventeenth property + setProperty user seventeenthPropName seventeenthPropVal `bindResponse` \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "too-many-properties" + + -- Old properties are maintained + getAllPropertyValues user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatch` Map.fromList properties + + -- Can still update the old properties + newPropertyVals <- replicateM 16 $ randomJSON + let newProperties = zip propertyNames newPropertyVals + forM_ newProperties $ \(prop, val) -> + setProperty user prop val `bindResponse` \resp -> + resp.status `shouldMatchInt` 200 + + getAllPropertyValues user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatch` Map.fromList newProperties + +testPropertyNameNotAscii :: App () +testPropertyNameNotAscii = do + user <- randomUser OwnDomain def + setProperty user "döner" "yes" `bindResponse` \resp -> + resp.status `shouldMatchInt` 400 + +testMaxLength :: App () +testMaxLength = do + user <- randomUser OwnDomain def + + maxKeyLength <- asInt $ readServiceConfig Brig %. "optSettings.setPropertyMaxKeyLen" + maxValLength <- asInt $ readServiceConfig Brig %. "optSettings.setPropertyMaxValueLen" + + tooLongProperty <- randomHandleWithRange (maxKeyLength + 1) (maxKeyLength + 1) + acceptableProperty <- randomHandleWithRange maxKeyLength maxKeyLength + + -- Two chars are taken by the quotes for string values. + -- + -- We use the `randomHandleWithRange` function because having non-ascii + -- characters or unprintable characters will increase the length of the JSON. + tooLongValue <- randomHandleWithRange (maxValLength - 1) (maxValLength - 1) + acceptableValue <- randomHandleWithRange (maxValLength - 2) (maxValLength - 2) + + setProperty user tooLongProperty acceptableValue `bindResponse` \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "property-key-too-large" + + setProperty user acceptableProperty tooLongValue `bindResponse` \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "property-value-too-large" + + setProperty user acceptableProperty acceptableValue `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + + getProperty user acceptableProperty `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatch` toJSON acceptableValue diff --git a/integration/test/Test/Search.hs b/integration/test/Test/Search.hs index af3f00d4e56..7ca6b48e4c6 100644 --- a/integration/test/Test/Search.hs +++ b/integration/test/Test/Search.hs @@ -1,3 +1,4 @@ +{-# OPTIONS -Wno-ambiguous-fields #-} module Test.Search where import qualified API.Brig as BrigP @@ -19,9 +20,9 @@ testSearchContactForExternalUsers = do owner <- randomUser OwnDomain def {BrigI.team = True} tid <- owner %. "team" & asString - partner <- createTeamMemberWithRole owner tid "partner" - tm1 <- createTeamMember owner tid - tm2 <- createTeamMember owner tid + partner <- createTeamMember owner def {role = "partner"} + tm1 <- createTeamMember owner def + tm2 <- createTeamMember owner def -- a team member can search for contacts bindResponse (BrigP.searchContacts tm1 (owner %. "name") OwnDomain) $ \resp -> diff --git a/integration/test/Test/Spar.hs b/integration/test/Test/Spar.hs index ab147901071..c18a517d2ea 100644 --- a/integration/test/Test/Spar.hs +++ b/integration/test/Test/Spar.hs @@ -2,9 +2,18 @@ module Test.Spar where +import qualified API.Brig as Brig +import API.BrigInternal as BrigInternal +import API.Common (randomEmail, randomExternalId, randomHandle) +import API.GalleyInternal (setTeamFeatureStatus) import API.Spar import Control.Concurrent (threadDelay) +import Data.Vector (fromList) +import qualified Data.Vector as Vector +import SAML2.WebSSO.Test.Util (SampleIdP (..), makeSampleIdPMetadata) import SetupHelpers +import Testlib.JSON +import Testlib.PTest import Testlib.Prelude testSparUserCreationInvitationTimeout :: (HasCallStack) => App () @@ -20,9 +29,316 @@ testSparUserCreationInvitationTimeout = do res.status `shouldMatchInt` 409 -- However, if we wait until the invitation timeout has passed - -- (assuming it is configured to 10s locally and in CI)... - liftIO $ threadDelay (11_000_000) + -- It's currently configured to 1s local/CI. + liftIO $ threadDelay (2_000_000) -- ...we should be able to create the user again retryT $ bindResponse (createScimUser OwnDomain tok scimUser) $ \res -> do res.status `shouldMatchInt` 201 + +testSparExternalIdDifferentFromEmailWithIdp :: (HasCallStack) => App () +testSparExternalIdDifferentFromEmailWithIdp = do + (owner, tid, _) <- createTeam OwnDomain 1 + void $ setTeamFeatureStatus owner tid "sso" "enabled" + void $ registerTestIdPWithMeta owner >>= getJSON 201 + tok <- createScimToken owner >>= getJSON 200 >>= (%. "token") >>= asString + email <- randomEmail + extId <- randomExternalId + scimUser <- randomScimUserWith extId email + userId <- createScimUser OwnDomain tok scimUser >>= getJSON 201 >>= (%. "id") >>= asString + activateEmail OwnDomain email + checkSparGetUserAndFindByExtId OwnDomain tok extId userId $ \u -> do + u %. "externalId" `shouldMatch` extId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` email + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "email" `shouldMatch` email + subject <- u %. "sso_id.subject" >>= asString + subject `shouldContainString` extId + u %. "handle" `shouldMatch` (scimUser %. "userName") + + -- Verify that updating `userName` (handle) works + scimUserWith1Update <- do + newHandle <- randomHandle + updatedScimUser <- setField "userName" newHandle scimUser + bindResponse (updateScimUser OwnDomain tok userId updatedScimUser) $ \res -> do + res.status `shouldMatchInt` 200 + res.json %. "userName" `shouldMatch` newHandle + checkSparGetUserAndFindByExtId OwnDomain tok extId userId $ \u -> do + u %. "externalId" `shouldMatch` extId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` email + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "handle" `shouldMatch` newHandle + pure updatedScimUser + + -- Verify that updating the user's external ID works + scimUserWith2Updates <- do + newExtId <- randomExternalId + updatedScimUser <- setField "externalId" newExtId scimUserWith1Update + bindResponse (updateScimUser OwnDomain tok userId updatedScimUser) $ \res -> do + res.status `shouldMatchInt` 200 + res.json %. "externalId" `shouldMatch` newExtId + checkSparGetUserAndFindByExtId OwnDomain tok newExtId userId $ \u -> do + u %. "externalId" `shouldMatch` newExtId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` email + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "email" `shouldMatch` email + subject <- u %. "sso_id.subject" >>= asString + subject `shouldContainString` newExtId + bindResponse (findUsersByExternalId OwnDomain tok extId) $ \res -> do + res.json %. "totalResults" `shouldMatchInt` 0 + res.json %. "Resources" `shouldMatch` ([] :: [Value]) + pure updatedScimUser + + -- Verify that updating the user's email works + do + let oldEmail = email + newEmail <- randomEmail + updatedScimUser <- setField "emails" (Array (Vector.fromList [object ["value" .= newEmail]])) scimUserWith2Updates + currentExtId <- updatedScimUser %. "externalId" >>= asString + bindResponse (updateScimUser OwnDomain tok userId updatedScimUser) $ \res -> do + res.status `shouldMatchInt` 200 + + -- before activation the old email should still be present + checkSparGetUserAndFindByExtId OwnDomain tok currentExtId userId $ \u -> do + u %. "externalId" `shouldMatch` currentExtId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` newEmail + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "email" `shouldMatch` oldEmail + subject <- u %. "sso_id.subject" >>= asString + subject `shouldContainString` currentExtId + + -- after activation the new email should be present + activateEmail OwnDomain newEmail + checkSparGetUserAndFindByExtId OwnDomain tok currentExtId userId $ \u -> do + u %. "externalId" `shouldMatch` currentExtId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` newEmail + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "email" `shouldMatch` newEmail + subject <- u %. "sso_id.subject" >>= asString + subject `shouldContainString` currentExtId + +registerTestIdPWithMeta :: (HasCallStack, MakesValue owner) => owner -> App Response +registerTestIdPWithMeta owner = do + SampleIdP idpmeta _ _ _ <- makeSampleIdPMetadata + createIdp owner idpmeta + +testSparExternalIdDifferentFromEmail :: (HasCallStack) => App () +testSparExternalIdDifferentFromEmail = do + (owner, tid, _) <- createTeam OwnDomain 1 + tok <- createScimToken owner >>= \resp -> resp.json %. "token" >>= asString + email <- randomEmail + extId <- randomExternalId + scimUser <- randomScimUserWith extId email + userId <- createScimUser OwnDomain tok scimUser >>= getJSON 201 >>= (%. "id") >>= asString + + checkSparGetUserAndFindByExtId OwnDomain tok extId userId $ \u -> do + u %. "externalId" `shouldMatch` extId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` email + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + res.json >>= asList >>= shouldBeEmpty + + registerUser OwnDomain tid email + + checkSparGetUserAndFindByExtId OwnDomain tok extId userId $ \u -> do + u %. "externalId" `shouldMatch` extId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` email + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "email" `shouldMatch` email + u %. "sso_id.scim_external_id" `shouldMatch` extId + u %. "handle" `shouldMatch` (scimUser %. "userName") + + -- Verify that updating the scim user works + scimUserWith1Update <- do + -- FUTUREWORK: test updating other fields besides handle as well + newHandle <- randomHandle + updatedScimUser <- setField "userName" newHandle scimUser + bindResponse (updateScimUser OwnDomain tok userId updatedScimUser) $ \res -> do + res.status `shouldMatchInt` 200 + res.json %. "userName" `shouldMatch` newHandle + checkSparGetUserAndFindByExtId OwnDomain tok extId userId $ \u -> do + u %. "externalId" `shouldMatch` extId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` email + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "handle" `shouldMatch` newHandle + pure updatedScimUser + + -- Verify that updating the user's external ID works + scimUserWith2Updates <- do + newExtId <- randomExternalId + updatedScimUser <- setField "externalId" newExtId scimUserWith1Update + bindResponse (updateScimUser OwnDomain tok userId updatedScimUser) $ \res -> do + res.status `shouldMatchInt` 200 + res.json %. "externalId" `shouldMatch` newExtId + checkSparGetUserAndFindByExtId OwnDomain tok newExtId userId $ \u -> do + u %. "externalId" `shouldMatch` newExtId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` email + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "email" `shouldMatch` email + u %. "sso_id.scim_external_id" `shouldMatch` newExtId + bindResponse (findUsersByExternalId OwnDomain tok extId) $ \res -> do + res.json %. "totalResults" `shouldMatchInt` 0 + res.json %. "Resources" `shouldMatch` ([] :: [Value]) + pure updatedScimUser + + -- Verify that updating the user's email works + do + let oldEmail = email + newEmail <- randomEmail + updatedScimUser <- setField "emails" (Array (Vector.fromList [object ["value" .= newEmail]])) scimUserWith2Updates + currentExtId <- updatedScimUser %. "externalId" >>= asString + bindResponse (updateScimUser OwnDomain tok userId updatedScimUser) $ \res -> do + res.status `shouldMatchInt` 200 + + -- before activation the new email should be returned by the SCIM API + checkSparGetUserAndFindByExtId OwnDomain tok currentExtId userId $ \u -> do + u %. "externalId" `shouldMatch` currentExtId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` newEmail + -- however brig should still return the old email + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "email" `shouldMatch` oldEmail + u %. "sso_id.scim_external_id" `shouldMatch` currentExtId + + -- after activation the new email should be present + activateEmail OwnDomain newEmail + checkSparGetUserAndFindByExtId OwnDomain tok currentExtId userId $ \u -> do + u %. "externalId" `shouldMatch` currentExtId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` newEmail + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "email" `shouldMatch` newEmail + u %. "sso_id.scim_external_id" `shouldMatch` currentExtId + +testSparExternalIdUpdateToANonEmail :: (HasCallStack) => App () +testSparExternalIdUpdateToANonEmail = do + (owner, tid, _) <- createTeam OwnDomain 1 + tok <- createScimToken owner >>= \resp -> resp.json %. "token" >>= asString + scimUser <- randomScimUser >>= removeField "emails" + email <- scimUser %. "externalId" >>= asString + userId <- bindResponse (createScimUser OwnDomain tok scimUser) $ \resp -> do + resp.status `shouldMatchInt` 201 + (resp.json %. "emails" >>= asList >>= assertOne >>= (%. "value") >>= asString) `shouldMatch` email + resp.json %. "id" >>= asString + registerUser OwnDomain tid email + + let extId = "notanemailaddress" + updatedScimUser <- setField "externalId" extId scimUser + updateScimUser OwnDomain tok userId updatedScimUser >>= assertStatus 400 + +testSparMigrateFromExternalIdOnlyToEmail :: (HasCallStack) => Tagged "mailUnchanged" Bool -> App () +testSparMigrateFromExternalIdOnlyToEmail (MkTagged emailUnchanged) = do + (owner, tid, _) <- createTeam OwnDomain 1 + tok <- createScimToken owner >>= \resp -> resp.json %. "token" >>= asString + scimUser <- randomScimUser >>= removeField "emails" + email <- scimUser %. "externalId" >>= asString + userId <- createScimUser OwnDomain tok scimUser >>= getJSON 201 >>= (%. "id") >>= asString + registerUser OwnDomain tid email + + -- Verify that updating a user with an empty emails does not change the email + bindResponse (updateScimUser OwnDomain tok userId scimUser) $ \resp -> do + resp.json %. "emails" `shouldMatch` (Array (fromList [object ["value" .= email]])) + resp.status `shouldMatchInt` 200 + + newEmail <- if emailUnchanged then pure email else randomEmail + let newEmails = (Array (fromList [object ["value" .= newEmail]])) + updatedScimUser <- setField "emails" newEmails scimUser + updateScimUser OwnDomain tok userId updatedScimUser `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "externalId" `shouldMatch` (updatedScimUser %. "externalId") + resp.json %. "emails" `shouldMatch` (updatedScimUser %. "emails") + + -- after activation the new email should be present + unless emailUnchanged $ activateEmail OwnDomain newEmail + + extId <- scimUser %. "externalId" >>= asString + checkSparGetUserAndFindByExtId OwnDomain tok extId userId $ \u -> do + u %. "externalId" `shouldMatch` extId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` newEmail + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "email" `shouldMatch` newEmail + u %. "sso_id.scim_external_id" `shouldMatch` extId + +registerUser :: (HasCallStack, MakesValue domain) => domain -> String -> String -> App () +registerUser domain tid email = do + BrigInternal.getInvitationByEmail domain email + >>= getJSON 200 + >>= BrigInternal.getInvitationCodeForTeam domain tid + >>= getJSON 200 + >>= (%. "code") + >>= asString + >>= Brig.registerUser domain email + >>= assertSuccess + +activateEmail :: (HasCallStack, MakesValue domain) => domain -> String -> App () +activateEmail domain email = do + (key, code) <- bindResponse (BrigInternal.getActivationCode domain email) $ \res -> do + (,) + <$> (res.json %. "key" >>= asString) + <*> (res.json %. "code" >>= asString) + Brig.activate domain key code >>= assertSuccess + +checkSparGetUserAndFindByExtId :: (HasCallStack, MakesValue domain) => domain -> String -> String -> String -> (Value -> App ()) -> App () +checkSparGetUserAndFindByExtId domain tok extId uid k = do + usersByExtIdResp <- findUsersByExternalId domain tok extId + usersByExtIdResp.status `shouldMatchInt` 200 + userByIdExtId <- usersByExtIdResp.json %. "Resources" >>= asList >>= assertOne + k userByIdExtId + + userByUidResp <- getScimUser domain tok uid + userByUidResp.status `shouldMatchInt` 200 + userByUid <- userByUidResp.json + k userByUid + + userByUid `shouldMatch` userByIdExtId + +testSparCreateScimTokenNoName :: (HasCallStack) => App () +testSparCreateScimTokenNoName = do + (owner, _tid, mem : _) <- createTeam OwnDomain 2 + createScimToken owner >>= assertSuccess + createScimToken owner >>= assertSuccess + tokens <- bindResponse (getScimTokens owner) $ \resp -> do + resp.status `shouldMatchInt` 200 + tokens <- resp.json %. "tokens" >>= asList + for_ tokens $ \token -> do + token %. "name" `shouldMatch` (token %. "id") + pure tokens + for_ tokens $ \token -> do + tokenId <- token %. "id" >>= asString + putScimTokenName mem tokenId "new name" >>= assertStatus 403 + putScimTokenName owner tokenId ("token:" <> tokenId) >>= assertSuccess + bindResponse (getScimTokens owner) $ \resp -> do + resp.status `shouldMatchInt` 200 + updatedTokens <- resp.json %. "tokens" >>= asList + for_ updatedTokens $ \token -> do + tokenId <- token %. "id" >>= asString + token %. "name" `shouldMatch` ("token:" <> tokenId) + +testSparCreateScimTokenWithName :: (HasCallStack) => App () +testSparCreateScimTokenWithName = do + (owner, _tid, _) <- createTeam OwnDomain 1 + let expected = "my scim token" + createScimTokenWithName owner expected >>= assertSuccess + tokens <- getScimTokens owner >>= getJSON 200 >>= (%. "tokens") >>= asList + for_ tokens $ \token -> do + token %. "name" `shouldMatch` expected diff --git a/integration/test/Test/Swagger.hs b/integration/test/Test/Swagger.hs index 5836ead12e0..571bd1ab245 100644 --- a/integration/test/Test/Swagger.hs +++ b/integration/test/Test/Swagger.hs @@ -1,14 +1,19 @@ module Test.Swagger where import qualified API.Brig as BrigP +import qualified Data.ByteString as B import qualified Data.Set as Set import Data.String.Conversions import GHC.Stack +import System.Exit +import System.FilePath +import System.Process import Testlib.Assertions import Testlib.Prelude +import UnliftIO.Temporary existingVersions :: Set Int -existingVersions = Set.fromList [0, 1, 2, 3, 4, 5, 6] +existingVersions = Set.fromList [0, 1, 2, 3, 4, 5, 6, 7] internalApis :: Set String internalApis = Set.fromList ["brig", "cannon", "cargohold", "cannon", "spar"] @@ -79,4 +84,50 @@ testSwaggerToc = do get path = rawBaseRequest OwnDomain Brig Unversioned path >>= submit "GET" html :: String - html = "

please pick an api version

/v0/api/swagger-ui/
/v1/api/swagger-ui/
/v2/api/swagger-ui/
/v3/api/swagger-ui/
/v4/api/swagger-ui/
/v5/api/swagger-ui/
/v6/api/swagger-ui/
" + html = "

OpenAPI 3.0 docs for all Wire APIs

\n

This wire-server system provides OpenAPI 3.0 documentation of our HTTP REST API.

The openapi docs are correct by construction (compiled from the server code), and more or less complete.

Some endpoints are version-controlled. Show all supported versions. find out more.\n

Public (all available versions)

\nv0: \nswagger-ui; \nswagger.json\n
\nv1: \nswagger-ui; \nswagger.json\n
\nv2: \nswagger-ui; \nswagger.json\n
\nv3: \nswagger-ui; \nswagger.json\n
\nv4: \nswagger-ui; \nswagger.json\n
\nv5: \nswagger-ui; \nswagger.json\n
\nv6: \nswagger-ui; \nswagger.json\n
\nv7: \nswagger-ui; \nswagger.json\n
\n\n

Internal (not versioned)

\n

Openapi docs for internal endpoints are served per service. I.e. there's one for `brig`, one for `cannon`, etc.. This is because Openapi doesn't play well with multiple actions having the same combination of HTTP method and URL path.

\nbrig:
\nswagger-ui; \nswagger.json\n
\ngalley:
\nswagger-ui; \nswagger.json\n
\nspar:
\nswagger-ui; \nswagger.json\n
\ncargohold:
\nswagger-ui; \nswagger.json\n
\ngundeck:
\nswagger-ui; \nswagger.json\n
\ncannon:
\nswagger-ui; \nswagger.json\n
\nproxy:
\nswagger-ui; \nswagger.json\n
\n\n

Federated API (backend-to-backend)

\nbrig (v0):
swagger-ui; swagger.json
brig (v1):
swagger-ui; swagger.json
brig (v2):
swagger-ui; swagger.json

\ngalley (v0):
swagger-ui; swagger.json
galley (v1):
swagger-ui; swagger.json
galley (v2):
swagger-ui; swagger.json

\ncargohold (v0):
swagger-ui; swagger.json
cargohold (v1):
swagger-ui; swagger.json
cargohold (v2):
swagger-ui; swagger.json

\n\n\n" + +data Swagger = SwaggerPublic | SwaggerInternal Service + +instance TestCases Swagger where + mkTestCases = + pure + [ MkTestCase "[swagger=ibrig]" (SwaggerInternal Brig), + MkTestCase "[swagger=icannon]" (SwaggerInternal Cannon), + MkTestCase "[swagger=icargohold]" (SwaggerInternal Cargohold), + MkTestCase "[swagger=igalley]" (SwaggerInternal Galley), + MkTestCase "[swagger=igundeck]" (SwaggerInternal Gundeck), + MkTestCase "[swagger=ispar]" (SwaggerInternal Spar), + MkTestCase "[swagger=public]" SwaggerPublic + ] + +-- | This runs the swagger linter [vacuum](https://quobix.com/vacuum/). +-- +-- The reason for adding the linter in the integration tests, and not in the lint job, is that +-- it calls brig for the swagger docs it validates, but no running brig during linting. +-- +-- There is also a make rule that does this, for convenience in your develop +-- flow. Make sure that brig is running before using the make rule. +testSwaggerLint :: (HasCallStack) => Swagger -> App () +testSwaggerLint sw = do + withSystemTempDirectory "swagger" $ \tmp -> do + req <- case sw of + SwaggerPublic -> + baseRequest OwnDomain Brig Versioned + $ joinHttpPath ["api", "swagger.json"] + (SwaggerInternal service) -> + baseRequest OwnDomain Brig Unversioned + $ joinHttpPath + [ "api-internal", + "swagger-ui", + serviceName service <> "-swagger.json" + ] + swagger <- submit "GET" req >>= getBody 200 + liftIO $ B.writeFile (tmp "swagger.json") swagger + let cmd = shell $ "vacuum lint -a -d -e " <> (tmp "swagger.json") + (exitCode, out, err) <- liftIO $ readCreateProcessWithExitCode cmd "" + case exitCode of + ExitSuccess -> pure () + _ -> do + liftIO $ putStrLn out + liftIO $ putStrLn err + assertFailure "swagger validation errors" diff --git a/integration/test/Test/TeamSettings.hs b/integration/test/Test/TeamSettings.hs index 03a667cf78e..74e8eaa65a6 100644 --- a/integration/test/Test/TeamSettings.hs +++ b/integration/test/Test/TeamSettings.hs @@ -26,7 +26,7 @@ import Testlib.Prelude testTeamSettingsUpdate :: (HasCallStack) => App () testTeamSettingsUpdate = do (ownerA, tidA, [mem]) <- createTeam OwnDomain 2 - partner <- createTeamMemberWithRole ownerA tidA "partner" + partner <- createTeamMember ownerA def {role = "partner"} bindResponse (putAppLockSettings tidA ownerA def) $ \resp -> do resp.status `shouldMatchInt` 200 @@ -45,7 +45,7 @@ testTeamSettingsUpdate = do testTeamPropertiesUpdate :: (HasCallStack) => App () testTeamPropertiesUpdate = do (ownerA, tidA, [mem]) <- createTeam OwnDomain 2 - partner <- createTeamMemberWithRole ownerA tidA "partner" + partner <- createTeamMember ownerA def {role = "partner"} bindResponse (putTeamProperties tidA ownerA def) $ \resp -> do resp.status `shouldMatchInt` 200 diff --git a/integration/test/Test/Teams.hs b/integration/test/Test/Teams.hs new file mode 100644 index 00000000000..5ce10031fba --- /dev/null +++ b/integration/test/Test/Teams.hs @@ -0,0 +1,350 @@ +{-# OPTIONS -Wno-ambiguous-fields #-} +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2024 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Teams where + +import API.Brig +import qualified API.BrigInternal as I +import API.Common +import API.Galley (getTeam, getTeamMembers, getTeamMembersCsv, getTeamNotifications) +import API.GalleyInternal (setTeamFeatureStatus) +import API.Gundeck +import Control.Monad.Codensity (Codensity (runCodensity)) +import Control.Monad.Extra (findM) +import Control.Monad.Reader (asks) +import qualified Data.ByteString.Char8 as B8 +import qualified Data.Map as Map +import Data.Time.Clock +import Data.Time.Format +import Notifications +import SetupHelpers +import Testlib.JSON +import Testlib.Prelude +import Testlib.ResourcePool (acquireResources) + +testInvitePersonalUserToTeam :: (HasCallStack) => App () +testInvitePersonalUserToTeam = do + resourcePool <- asks (.resourcePool) + runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do + let domain = testBackend.berDomain + (owner, tid, tm) <- runCodensity (startDynamicBackend testBackend def) $ \_ -> do + (owner, tid, tm : _) <- createTeam domain 2 + pure (owner, tid, tm) + + runCodensity + ( startDynamicBackend + testBackend + (def {galleyCfg = setField "settings.exposeInvitationURLsTeamAllowlist" [tid]}) + ) + $ \_ -> do + bindResponse (listInvitations owner tid) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "invitations" `shouldMatch` ([] :: [()]) + + ownerId <- owner %. "id" & asString + setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "enabled" >>= assertSuccess + user <- I.createUser domain def >>= getJSON 201 + uid <- user %. "id" >>= asString + email <- user %. "email" >>= asString + + inv <- postInvitation owner (PostInvitation (Just email) Nothing) >>= getJSON 201 + checkListInvitations owner tid email + code <- I.getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString + inv %. "url" & asString >>= assertUrlContainsCode code + acceptTeamInvitation user code Nothing >>= assertStatus 400 + acceptTeamInvitation user code (Just "wrong-password") >>= assertStatus 403 + + withWebSockets [owner, user, tm] $ \wss@[wsOwner, _, _] -> do + acceptTeamInvitation user code (Just defPassword) >>= assertSuccess + + -- When the team is smaller than fanout limit, all members get this + -- notification. + for_ wss $ \ws -> do + updateNotif <- awaitMatch isUserUpdatedNotif ws + updateNotif %. "payload.0.user.team" `shouldMatch` tid + + -- Admins get a team.member-join notif on the websocket for + -- team-settings + memberJobNotif <- awaitMatch isTeamMemberJoinNotif wsOwner + memberJobNotif %. "payload.0.team" `shouldMatch` tid + memberJobNotif %. "payload.0.data.user" `shouldMatch` objId user + + bindResponse (getSelf user) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "team" `shouldMatch` tid + + -- a team member can now find the former personal user in the team + bindResponse (getTeamMembers tm tid) $ \resp -> do + resp.status `shouldMatchInt` 200 + members <- resp.json %. "members" >>= asList + ids <- for members ((%. "user") >=> asString) + ids `shouldContain` [uid] + + -- the former personal user can now see other team members + bindResponse (getTeamMembers user tid) $ \resp -> do + resp.status `shouldMatchInt` 200 + members <- resp.json %. "members" >>= asList + ids <- for members ((%. "user") >=> asString) + tmId <- tm %. "id" & asString + ids `shouldContain` [ownerId] + ids `shouldContain` [tmId] + + -- the former personal user can now search for the owner + bindResponse (searchContacts user (owner %. "name") domain) $ \resp -> do + resp.status `shouldMatchInt` 200 + documents <- resp.json %. "documents" >>= asList + ids <- for documents ((%. "id") >=> asString) + ids `shouldContain` [ownerId] + + I.refreshIndex domain + -- a team member can now search for the former personal user + bindResponse (searchContacts tm (user %. "name") domain) $ \resp -> do + resp.status `shouldMatchInt` 200 + document <- resp.json %. "documents" >>= asList >>= assertOne + document %. "id" `shouldMatch` uid + document %. "team" `shouldMatch` tid + where + checkListInvitations :: Value -> String -> String -> App () + checkListInvitations owner tid email = do + newUserEmail <- randomEmail + void $ postInvitation owner (PostInvitation (Just newUserEmail) Nothing) >>= assertSuccess + bindResponse (listInvitations owner tid) $ \resp -> do + resp.status `shouldMatchInt` 200 + invitations <- resp.json %. "invitations" >>= asList + + -- personal user invitations have a different invitation URL than non-existing user invitations + newUserInv <- invitations & findM (\i -> (i %. "email" >>= asString) <&> (== newUserEmail)) + newUserInvUrl <- newUserInv %. "url" & asString + newUserInvUrl `shouldContainString` "/register" + + personalUserInv <- invitations & findM (\i -> (i %. "email" >>= asString) <&> (== email)) + personalUserInvUrl <- personalUserInv %. "url" & asString + personalUserInvUrl `shouldContainString` "/accept-invitation" + + assertUrlContainsCode :: (HasCallStack) => String -> String -> App () + assertUrlContainsCode code url = do + queryParam <- url & asString <&> getQueryParam "team-code" + queryParam `shouldMatch` Just (Just code) + +testInvitePersonalUserToLargeTeam :: (HasCallStack) => App () +testInvitePersonalUserToLargeTeam = do + teamSize <- readServiceConfig Galley %. "settings.maxFanoutSize" & asInt <&> (+ 1) + (owner, tid, (alice : otherTeamMembers)) <- createTeam OwnDomain teamSize + -- User to be invited to the team + knut <- I.createUser OwnDomain def >>= getJSON 201 + + -- Non team friends of knut + dawn <- I.createUser OwnDomain def >>= getJSON 201 + eli <- I.createUser OtherDomain def >>= getJSON 201 + + -- knut is also friends with alice, but not any other team members. + traverse_ (connectTwoUsers knut) [alice, dawn, eli] + + addFailureContext ("tid: " <> tid) $ do + uidContext <- mkContextUserIds [("owner", owner), ("alice", alice), ("knut", knut), ("dawn", dawn), ("eli", eli)] + addFailureContext uidContext $ do + lastTeamNotif <- + getTeamNotifications owner Nothing `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "notifications.-1.id" & asString + + knutEmail <- knut %. "email" >>= asString + inv <- postInvitation owner (PostInvitation (Just knutEmail) Nothing) >>= getJSON 201 + code <- I.getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString + + withWebSockets [owner, alice, dawn, eli, head otherTeamMembers] $ \[wsOwner, wsAlice, wsDawn, wsEli, wsOther] -> do + acceptTeamInvitation knut code (Just defPassword) >>= assertSuccess + + for_ [wsAlice, wsDawn] $ \ws -> do + notif <- awaitMatch isUserUpdatedNotif ws + nPayload notif %. "user.id" `shouldMatch` (objId knut) + nPayload notif %. "user.team" `shouldMatch` tid + + -- Admins get a team.member-join notif on the websocket for + -- team-settings + memberJobNotif <- awaitMatch isTeamMemberJoinNotif wsOwner + memberJobNotif %. "payload.0.team" `shouldMatch` tid + memberJobNotif %. "payload.0.data.user" `shouldMatch` objId knut + + -- Other team members don't get notified on the websocket + assertNoEvent 1 wsOther + + -- Remote users are not notified at all + assertNoEvent 1 wsEli + + -- Other team members learn about knut via team notifications + getTeamNotifications (head otherTeamMembers) (Just lastTeamNotif) `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + -- Ignore the first notif because it is always the notif matching the + -- lastTeamNotif id. + resp.json %. "notifications.1.payload.0.type" `shouldMatch` "team.member-join" + resp.json %. "notifications.1.payload.0.team" `shouldMatch` tid + resp.json %. "notifications.1.payload.0.data.user" `shouldMatch` objId knut + +mkContextUserIds :: (MakesValue user) => [(String, user)] -> App String +mkContextUserIds = + fmap (intercalate "\n") + . traverse + ( \(name, user) -> do + uid <- objQidObject user %. "id" & asString + domain <- objDomain user + pure $ name <> ": " <> uid <> "@" <> domain + ) + +testInvitePersonalUserToTeamMultipleInvitations :: (HasCallStack) => App () +testInvitePersonalUserToTeamMultipleInvitations = do + (owner, tid, _) <- createTeam OwnDomain 0 + (owner2, _, _) <- createTeam OwnDomain 0 + user <- I.createUser OwnDomain def >>= getJSON 201 + email <- user %. "email" >>= asString + inv <- postInvitation owner (PostInvitation (Just email) Nothing) >>= getJSON 201 + inv2 <- postInvitation owner2 (PostInvitation (Just email) Nothing) >>= getJSON 201 + code <- I.getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString + acceptTeamInvitation user code (Just defPassword) >>= assertSuccess + bindResponse (getSelf user) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "team" `shouldMatch` tid + code2 <- I.getInvitationCode owner2 inv2 >>= getJSON 200 >>= (%. "code") & asString + bindResponse (acceptTeamInvitation user code2 (Just defPassword)) $ \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "cannot-join-multiple-teams" + bindResponse (getSelf user) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "team" `shouldMatch` tid + acceptTeamInvitation user code (Just defPassword) >>= assertStatus 400 + +testInvitationTypesAreDistinct :: (HasCallStack) => App () +testInvitationTypesAreDistinct = do + -- We are only testing one direction because the other is not possible + -- because the non-existing user cannot have a valid session + (owner, _, _) <- createTeam OwnDomain 0 + user <- I.createUser OwnDomain def >>= getJSON 201 + email <- user %. "email" >>= asString + inv <- postInvitation owner (PostInvitation (Just email) Nothing) >>= getJSON 201 + code <- I.getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString + let body = + AddUser + { name = Just email, + email = Just email, + password = Just defPassword, + teamCode = Just code + } + addUser OwnDomain body >>= assertStatus 409 + +testTeamUserCannotBeInvited :: (HasCallStack) => App () +testTeamUserCannotBeInvited = do + (_, _, tm : _) <- createTeam OwnDomain 2 + (owner2, _, _) <- createTeam OwnDomain 0 + email <- tm %. "email" >>= asString + postInvitation owner2 (PostInvitation (Just email) Nothing) >>= assertStatus 409 + +testUpgradePersonalToTeam :: (HasCallStack) => App () +testUpgradePersonalToTeam = do + alice <- randomUser OwnDomain def + let teamName = "wonderland" + tid <- bindResponse (upgradePersonalToTeam alice teamName) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "team_name" `shouldMatch` teamName + resp.json %. "team_id" + + alice' <- getUser alice alice >>= getJSON 200 + alice' %. "team" `shouldMatch` tid + + team <- getTeam alice tid >>= getJSON 200 + team %. "name" `shouldMatch` teamName + + bindResponse (getTeamMembers alice tid) $ \resp -> do + resp.status `shouldMatchInt` 200 + owner <- asList (resp.json %. "members") >>= assertOne + owner %. "user" `shouldMatch` (alice %. "id") + shouldBeNull $ owner %. "created_at" + shouldBeNull $ owner %. "created_by" + +testUpgradePersonalToTeamAlreadyInATeam :: (HasCallStack) => App () +testUpgradePersonalToTeamAlreadyInATeam = do + (alice, _, _) <- createTeam OwnDomain 0 + + bindResponse (upgradePersonalToTeam alice "wonderland") $ \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "user-already-in-a-team" + +-- for additional tests of the CSV download particularly with SCIM users, please refer to 'Test.Spar.Scim.UserSpec' +testTeamMemberCsvExport :: (HasCallStack) => App () +testTeamMemberCsvExport = do + (owner, tid, members) <- createTeam OwnDomain 5 + + modifiedMembers <- for + ( zip + ([0, 1, 2] <> repeat 0) + (owner : members) + ) + $ \(n, m) -> do + handle <- randomHandle + putHandle m handle >>= assertSuccess + clients <- + replicateM n + $ addClient m def + >>= getJSON 201 + >>= (%. "id") + >>= asString + for_ (listToMaybe clients) $ \c -> + getNotifications m def {client = Just c} + void $ I.putSSOId m def {I.scimExternalId = Just "foo"} >>= getBody 200 + setField "handle" handle m + >>= setField "role" (if m == owner then "owner" else "member") + >>= setField "num_clients" n + + memberMap :: Map.Map String Value <- fmap Map.fromList $ for (modifiedMembers) $ \m -> do + uid <- m %. "id" & asString + pure (uid, m) + + bindResponse (getTeamMembersCsv owner tid) $ \resp -> do + resp.status `shouldMatchInt` 200 + let rows = sort $ tail $ B8.lines $ resp.body + length rows `shouldMatchInt` 5 + for_ rows $ \row -> do + let cols = B8.split ',' row + let uid = read $ B8.unpack $ cols !! 11 + let mem = memberMap Map.! uid + + ownerId <- owner %. "id" & asString + let ownerMember = memberMap Map.! ownerId + now <- formatTime defaultTimeLocale "%Y-%m-%d" <$> liftIO getCurrentTime + numClients <- mem %. "num_clients" & asInt + + let parseField = unquote . read . B8.unpack . (cols !!) + + parseField 0 `shouldMatch` (mem %. "name") + parseField 1 `shouldMatch` (mem %. "handle") + parseField 2 `shouldMatch` (mem %. "email") + role <- mem %. "role" & asString + parseField 3 `shouldMatch` role + when (role /= "owner") $ do + take 10 (parseField 4) `shouldMatch` now + parseField 5 `shouldMatch` (ownerMember %. "handle") + parseField 7 `shouldMatch` "wire" + parseField 9 `shouldMatch` "foo" + parseField 12 `shouldMatch` show numClients + (if numClients > 0 then shouldNotMatch else shouldMatch) + (parseField 13) + "" + parseField 14 `shouldMatch` "active" + where + unquote :: String -> String + unquote ('\'' : x) = x + unquote x = x diff --git a/integration/test/Test/User.hs b/integration/test/Test/User.hs index 183a391d779..548c34b1daf 100644 --- a/integration/test/Test/User.hs +++ b/integration/test/Test/User.hs @@ -4,15 +4,20 @@ module Test.User where import API.Brig import API.BrigInternal +import API.Common import API.GalleyInternal -import API.Spar +import qualified API.Spar as Spar +import Control.Monad.Codensity +import Control.Monad.Reader import qualified Data.Aeson as Aeson import qualified Data.UUID as UUID import qualified Data.UUID.V4 as UUID import SetupHelpers import Testlib.Prelude +import Testlib.ResourcePool +import Testlib.VersionedFed -testSupportedProtocols :: (HasCallStack) => Domain -> App () +testSupportedProtocols :: (HasCallStack) => OneOf Domain (FedDomain 1) -> App () testSupportedProtocols bobDomain = do alice <- randomUser OwnDomain def alice %. "supported_protocols" `shouldMatchSet` ["proteus"] @@ -114,7 +119,7 @@ testUpdateHandle = do resp.status `shouldMatchInt` 200 mb <- (assertOne =<< asList resp.json) %. "managed_by" mb `shouldMatch` "wire" - bindResponse (getScimTokens owner) $ \resp -> do + bindResponse (Spar.getScimTokens owner) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "tokens" `shouldMatch` ([] @String) @@ -172,4 +177,51 @@ testActivateAccountWithPhoneV5 = do let reqBody = Aeson.object ["phone" .= phone] activateUserV5 dom reqBody `bindResponse` \resp -> do resp.status `shouldMatchInt` 400 - resp.json %. "label" `shouldMatch` "invalid-phone" + resp.json %. "label" `shouldMatch` "bad-request" + +testMigratingPasswordHashingAlgorithm :: (HasCallStack) => App () +testMigratingPasswordHashingAlgorithm = do + let argon2idOpts = + object + [ "algorithm" .= "argon2id", + "iterations" .= (1 :: Int), + "memory" .= (128 :: Int), + "parallelism" .= (1 :: Int) + ] + cfgArgon2id = + def + { brigCfg = setField "settings.setPasswordHashingOptions" argon2idOpts, + galleyCfg = setField "settings.passwordHashingOptions" argon2idOpts + } + cfgScrypt = + def + { brigCfg = setField "settings.setPasswordHashingOptions.algorithm" "scrypt", + galleyCfg = setField "settings.passwordHashingOptions.algorithm" "scrypt" + } + resourcePool <- asks (.resourcePool) + runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do + let domain = testBackend.berDomain + email1 <- randomEmail + password1 <- randomString 20 + + email2 <- randomEmail + password2 <- randomString 20 + + runCodensity (startDynamicBackend testBackend cfgScrypt) $ \_ -> do + void $ randomUser domain (def {email = Just email1, password = Just password1}) + login domain email1 password1 >>= assertSuccess + + runCodensity (startDynamicBackend testBackend cfgArgon2id) $ \_ -> do + login domain email1 password1 >>= assertSuccess + + -- Create second user to ensure that we're testing migrating back. This is + -- not really needed because the login above rehashes the password, but it + -- makes the test clearer. + void $ randomUser domain (def {email = Just email2, password = Just password2}) + login domain email2 password2 >>= assertSuccess + + -- Check that both users can still login with Scrypt in case the operator + -- wants to rollback the config. + runCodensity (startDynamicBackend testBackend cfgScrypt) $ \_ -> do + login domain email1 password1 >>= assertSuccess + login domain email2 password2 >>= assertSuccess diff --git a/integration/test/Test/Version.hs b/integration/test/Test/Version.hs index abd59a49958..df0a7ab731c 100644 --- a/integration/test/Test/Version.hs +++ b/integration/test/Test/Version.hs @@ -9,22 +9,24 @@ newtype Versioned' = Versioned' Versioned -- | This instance is used to generate tests for some of the versions. (Not checking all of them for time efficiency reasons) instance TestCases Versioned' where - testCases = - [ MkTestCase "[version=unversioned]" (Versioned' Unversioned), - MkTestCase "[version=versioned]" (Versioned' Versioned), - MkTestCase "[version=v1]" (Versioned' (ExplicitVersion 1)), - MkTestCase "[version=v3]" (Versioned' (ExplicitVersion 3)), - MkTestCase "[version=v6]" (Versioned' (ExplicitVersion 6)) - ] + mkTestCases = + pure + [ MkTestCase "[version=unversioned]" (Versioned' Unversioned), + MkTestCase "[version=versioned]" (Versioned' Versioned), + MkTestCase "[version=v1]" (Versioned' (ExplicitVersion 1)), + MkTestCase "[version=v3]" (Versioned' (ExplicitVersion 3)), + MkTestCase "[version=v6]" (Versioned' (ExplicitVersion 6)) + ] -- | Used to test endpoints that have changed after version 5 data Version5 = Version5 | NoVersion5 instance TestCases Version5 where - testCases = - [ MkTestCase "[version=versioned]" NoVersion5, - MkTestCase "[version=v5]" Version5 - ] + mkTestCases = + pure + [ MkTestCase "[version=versioned]" NoVersion5, + MkTestCase "[version=v5]" Version5 + ] withVersion5 :: Version5 -> App a -> App a withVersion5 Version5 = withAPIVersion 5 diff --git a/integration/test/Testlib/App.hs b/integration/test/Testlib/App.hs index 38188f9a67e..2eecee9d686 100644 --- a/integration/test/Testlib/App.hs +++ b/integration/test/Testlib/App.hs @@ -63,11 +63,6 @@ instance MakesValue Domain where make OwnDomain = asks (String . T.pack . (.domain1)) make OtherDomain = asks (String . T.pack . (.domain2)) -data FedDomain = FedV0Domain - -instance MakesValue FedDomain where - make FedV0Domain = asks (String . T.pack . (.federationV0Domain)) - -- | Run an action, `recoverAll`ing with exponential backoff (min step 8ms, total timeout -- ~15s). Search this package for examples how to use it. -- diff --git a/integration/test/Testlib/Assertions.hs b/integration/test/Testlib/Assertions.hs index f426336b6c7..28ddf0c0af1 100644 --- a/integration/test/Testlib/Assertions.hs +++ b/integration/test/Testlib/Assertions.hs @@ -84,21 +84,6 @@ shouldMatchWithMsg msg a b = do else pure "" assertFailure $ (maybe "" (<> "\n") msg) <> "Actual:\n" <> pa <> "\nExpected:\n" <> pb <> diff --- | apply some canonicalization transformations that *usually* do not change semantics before --- comparing. -shouldMatchLeniently :: (MakesValue a, MakesValue b, HasCallStack) => a -> b -> App () -shouldMatchLeniently = shouldMatchWithRules [EmptyArrayIsNull, RemoveNullFieldsFromObjects] (const $ pure Nothing) - --- | apply *all* canonicalization transformations before comparing. some of these may not be --- valid on your input, see 'LenientMatchRule' for details. -shouldMatchSloppily :: (MakesValue a, MakesValue b, HasCallStack) => a -> b -> App () -shouldMatchSloppily = shouldMatchWithRules [minBound ..] (const $ pure Nothing) - --- | apply *all* canonicalization transformations before comparing. some of these may not be --- valid on your input, see 'LenientMatchRule' for details. -shouldMatchALittle :: (MakesValue a, MakesValue b, HasCallStack) => (Aeson.Value -> App (Maybe Aeson.Value)) -> a -> b -> App () -shouldMatchALittle = shouldMatchWithRules [minBound ..] - data LenientMatchRule = EmptyArrayIsNull | ArraysAreSets @@ -195,6 +180,15 @@ shouldMatchInt :: App () shouldMatchInt = shouldMatch +shouldNotMatchInt :: + (MakesValue a, HasCallStack) => + -- | The actual value + a -> + -- | The expected value + Int -> + App () +shouldNotMatchInt = shouldNotMatch + shouldMatchRange :: (MakesValue a, HasCallStack) => -- | The actual value @@ -221,6 +215,9 @@ shouldMatchSet a b = do shouldBeEmpty :: (MakesValue a, HasCallStack) => a -> App () shouldBeEmpty a = a `shouldMatch` (mempty :: [Value]) +shouldBeNull :: (MakesValue a, HasCallStack) => a -> App () +shouldBeNull a = a `shouldMatch` Aeson.Null + shouldMatchOneOf :: (MakesValue a, MakesValue b, HasCallStack) => a -> @@ -255,13 +252,21 @@ super `shouldContain` sub = do assertFailure $ "String or List:\n" <> show super <> "\nDoes not contain:\n" <> show sub printFailureDetails :: AssertionFailure -> IO String -printFailureDetails (AssertionFailure stack mbResponse msg) = do +printFailureDetails (AssertionFailure stack mbResponse ctx msg) = do s <- prettierCallStack stack pure . unlines $ colored yellow "assertion failure:" : colored red msg : "\n" <> s : toList (fmap prettyResponse mbResponse) + <> toList (fmap prettyContext ctx) + +prettyContext :: String -> String +prettyContext ctx = do + unlines + [ colored yellow "context:", + colored blue ctx + ] printExceptionDetails :: SomeException -> IO String printExceptionDetails e = do diff --git a/integration/test/Testlib/Cannon.hs b/integration/test/Testlib/Cannon.hs index 7b69cf60cad..45093c3e252 100644 --- a/integration/test/Testlib/Cannon.hs +++ b/integration/test/Testlib/Cannon.hs @@ -53,7 +53,7 @@ import qualified Control.Monad.Catch as Catch import Control.Monad.IO.Class import Control.Monad.Reader (asks) import Control.Monad.STM -import Data.Aeson (Value (..), decodeStrict') +import Data.Aeson hiding ((.=)) import Data.ByteString (ByteString) import Data.ByteString.Conversion (fromByteString) import Data.ByteString.Conversion.To @@ -95,6 +95,13 @@ instance HasField "client" WebSocket (Maybe ClientIdentity) where client = c } +instance HasField "user" WebSocket Value where + getField ws = + object + [ "domain" .= ws.wsConnect.domain, + "id" .= ws.wsConnect.user + ] + -- Specifies how a Websocket at cannon should be opened data WSConnect = WSConnect { user :: String, @@ -219,7 +226,7 @@ run wsConnect app = do headers = mempty, request = request } - throwIO (AssertionFailure callStack (Just r) (displayException ex)) + throwIO (AssertionFailure callStack (Just r) Nothing (displayException ex)) liftIO $ race_ waitForPresence waitForException pure wsapp @@ -421,7 +428,11 @@ awaitNMatches :: App [Value] awaitNMatches nExpected checkMatch ws = do res <- awaitNMatchesResult nExpected checkMatch ws - assertAwaitResult res + withWebSocketFailureContext ws $ + assertAwaitResult res + +withWebSocketFailureContext :: WebSocket -> App a -> App a +withWebSocketFailureContext ws = addFailureContext ("on websocket for user: " <> ws.wsConnect.user <> "@" <> ws.wsConnect.domain) assertAwaitResult :: (HasCallStack) => AwaitResult -> App [Value] assertAwaitResult res = do @@ -481,7 +492,7 @@ assertNoEvent :: Int -> WebSocket -> App () -assertNoEvent to ws = do +assertNoEvent to ws = withWebSocketFailureContext ws $ do mEvent <- awaitAnyEvent to ws case mEvent of Just event -> assertFailure $ "Expected no event, but got: " <> show event diff --git a/integration/test/Testlib/Certs.hs b/integration/test/Testlib/Certs.hs index b6fda9b5204..64df6e4c152 100644 --- a/integration/test/Testlib/Certs.hs +++ b/integration/test/Testlib/Certs.hs @@ -32,10 +32,6 @@ privateKeyToString = toPem . keyToPEM PKCS8Format . PrivKeyRSA publicKeyToString :: RSA.PublicKey -> String publicKeyToString = toPem . pubKeyToPEM . PubKeyRSA --- | order: publickey, private key -keyPairToString :: RSAKeyPair -> (String, String) -keyPairToString = bimap publicKeyToString privateKeyToString - -- | the minimum key size is hard coded to be 256 bytes (= 2048 bits) mkKeyPair :: (HasCallStack) => (Integer, Integer) -> App RSAKeyPair mkKeyPair primes = @@ -57,21 +53,6 @@ primesB = 1030843359898456423663521323846594342599509001361505950190458094255790543792826808869649005832755187592625111972154015489882697017782849415061917844274039201990123282710414810809677284498651901967728601289390435426055251344683598043635553930587608961202440578033000424009931449958127951542294372025522185552538021557179009278446615246891375299863655746951224012338422185000952023195927317706092311999889180603374149659663869483313116251085191329801800565556652256960650364631610748235925879940728370511827034946814052737660926604082837303885143652256413187183052924192977324527952882600246973965189570970469037044568259408811931440525775822585332497163319841870179534838043708793539688804501356153704884928847627798172061867373042270416202913078776299057112318300845218218100606684092792088779583532324019862407866255929320869554565576301069075336647916168479092314004711778618335406757602974282533765740790546167166172626995630463716394043281720388344899550856555259477489548509996409954619324524195894460510128676025203769176155038527250084664954695197534485529595784255553806751541708069739004260117122700058054443774458724994738753921481706985581116480802534320353367271370286704034867136678539759260831996400891886615914808935283451835347282009482924185619896114631919985205238905153951336432886954324618000593140640843908517786951586431386674557882396487935889471856924185568502767114186884930347618747984770073080480895996031031971187681573023398782756925726725786964170460286504569090697402674905089317540771910375616350312239688178277204391962835159620450731320465816254229575392846112372636483958055913716148919092913102176828552932292829256960875180097808893909460952573027221089128208000054670526724565994184754244760290009957352237133054978847493874379201323517903544742831961755055100216728931496213920467911320372016970509300894067675803619448926461034580033818298648457643287641768005986812455071220244863874301028965665847375769473444088940776224643189987541019987285740411119351744972645543429351630677554481991322726604779330104110295967482897278840078926508970545806499140537364387530291523697762079684955475417383069988065253583073257131193644210418873929829417895241230927769637328283865111435730810586338426336027745629520975220163350734423915441885289661065494424704587153904031874537230782548938379423349488654701140981815973723582107593419642780372301171156324514852331126462907486017679770773972513376077318418003532168673261819818236071249 ) --- | create a root certificate authority CertificateBundle -createRootCA :: - (HasCallStack) => - -- | the root CA's name - String -> - -- | the root CA's keymaterial - RSAKeyPair -> - SignedCert -createRootCA caName (pubKey, privKey) = - mkSignedCert - pubKey - privKey - caName - caName - -- | sign an intermediate/ leaf certificate by signing with an intermediate/ root CA's key intermediateCert :: (HasCallStack) => diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index 336383deb72..b5611178b6f 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -11,8 +11,6 @@ import Data.Functor import Data.IORef import qualified Data.Map as Map import Data.Maybe (fromMaybe) -import Data.Set (Set) -import qualified Data.Set as Set import Data.Traversable (for) import qualified Data.Yaml as Yaml import qualified Database.CQL.IO as Cassandra @@ -88,7 +86,8 @@ mkGlobalEnv cfgFile = do Map.fromList $ [ (intConfig.backendOne.originDomain, intConfig.backendOne.beServiceMap), (intConfig.backendTwo.originDomain, intConfig.backendTwo.beServiceMap), - (intConfig.federationV0.originDomain, intConfig.federationV0.beServiceMap) + (intConfig.federationV0.originDomain, intConfig.federationV0.beServiceMap), + (intConfig.federationV1.originDomain, intConfig.federationV1.beServiceMap) ] <> [(berDomain resource, resourceServiceMap resource) | resource <- resources] tempDir <- Codensity $ withSystemTempDirectory "test" @@ -102,11 +101,11 @@ mkGlobalEnv cfgFile = do gDomain2 = intConfig.backendTwo.originDomain, gIntegrationTestHostName = intConfig.integrationTestHostName, gFederationV0Domain = intConfig.federationV0.originDomain, + gFederationV1Domain = intConfig.federationV1.originDomain, gDynamicDomains = (.domain) <$> Map.elems intConfig.dynamicBackends, - gDefaultAPIVersion = 6, + gDefaultAPIVersion = 7, gManager = manager, gServicesCwdBase = devEnvProjectRoot <&> ( "services"), - gRemovalKeyPaths = mempty, gBackendResourcePool = resourcePool, gRabbitMQConfig = intConfig.rabbitmq, gTempDir = tempDir, @@ -141,11 +140,19 @@ mkEnv ge = do domain2 = gDomain2 ge, integrationTestHostName = gIntegrationTestHostName ge, federationV0Domain = gFederationV0Domain ge, + federationV1Domain = gFederationV1Domain ge, dynamicDomains = gDynamicDomains ge, defaultAPIVersion = gDefaultAPIVersion ge, + -- hardcode API versions for federated domains because they don't have + -- latest things. Ensure we do not use development API versions in + -- those domains. + apiVersionByDomain = + Map.fromList + [ (gFederationV0Domain ge, 4), + (gFederationV1Domain ge, 5) + ], manager = gManager ge, servicesCwdBase = gServicesCwdBase ge, - removalKeyPaths = gRemovalKeyPaths ge, prekeys = pks, lastPrekeys = lpks, mls = mls, @@ -154,18 +161,6 @@ mkEnv ge = do timeOutSeconds = ge.gTimeOutSeconds } -destroy :: IORef (Set BackendResource) -> BackendResource -> IO () -destroy ioRef = modifyIORef' ioRef . Set.insert - -create :: IORef (Set.Set BackendResource) -> IO BackendResource -create ioRef = - atomicModifyIORef - ioRef - $ \s -> - case Set.minView s of - Nothing -> error "No resources available" - Just (r, s') -> (s', r) - allCiphersuites :: [Ciphersuite] -- FUTUREWORK: add 0x0005 to this list once openmls supports it allCiphersuites = map Ciphersuite ["0x0001", "0xf031", "0x0002", "0x0007"] diff --git a/integration/test/Testlib/HTTP.hs b/integration/test/Testlib/HTTP.hs index d155a45c46f..0b919db3c7c 100644 --- a/integration/test/Testlib/HTTP.hs +++ b/integration/test/Testlib/HTTP.hs @@ -4,6 +4,7 @@ import qualified Control.Exception as E import Control.Monad.Reader import qualified Data.Aeson as Aeson import qualified Data.Aeson.Types as Aeson +import Data.Bifunctor (Bifunctor (bimap)) import Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as C8 import qualified Data.ByteString.Lazy as L @@ -11,21 +12,25 @@ import qualified Data.CaseInsensitive as CI import Data.Function import Data.List import Data.List.Split (splitOn) +import qualified Data.Map as Map import Data.Maybe import Data.String import Data.String.Conversions (cs) import qualified Data.Text as T import qualified Data.Text.Encoding as T +import Data.Tuple.Extra import GHC.Generics import GHC.Stack import qualified Network.HTTP.Client as HTTP import Network.HTTP.Types (hLocation) import qualified Network.HTTP.Types as HTTP +import Network.HTTP.Types.URI (parseQuery) import Network.URI (URI (..), URIAuth (..), parseURI) import Testlib.Assertions import Testlib.Env import Testlib.JSON import Testlib.Types +import Web.Cookie import Prelude splitHttpPath :: String -> [String] @@ -40,6 +45,18 @@ addJSONObject = addJSON . Aeson.object addJSON :: (Aeson.ToJSON a) => a -> HTTP.Request -> HTTP.Request addJSON obj = addBody (HTTP.RequestBodyLBS (Aeson.encode obj)) "application/json" +addXML :: ByteString -> HTTP.Request -> HTTP.Request +addXML xml = addBody (HTTP.RequestBodyBS xml) "application/xml" + +addUrlEncodedForm :: [(String, String)] -> HTTP.Request -> HTTP.Request +addUrlEncodedForm form req = + req + { HTTP.requestBody = HTTP.RequestBodyLBS (L.fromStrict (HTTP.renderSimpleQuery False (both C8.pack <$> form))), + HTTP.requestHeaders = + (fromString "Content-Type", fromString "application/x-www-form-urlencoded") + : HTTP.requestHeaders req + } + addBody :: HTTP.RequestBody -> String -> HTTP.Request -> HTTP.Request addBody body contentType req = req @@ -73,13 +90,15 @@ setCookie :: String -> HTTP.Request -> HTTP.Request setCookie c r = addHeader "Cookie" (cs c) r +getCookie :: String -> Response -> Maybe String +getCookie name resp = do + cookieHeader <- lookup (CI.mk $ cs "set-cookie") resp.headers + cs <$> lookup (cs name) (parseCookies cookieHeader) + addQueryParams :: [(String, String)] -> HTTP.Request -> HTTP.Request addQueryParams params req = HTTP.setQueryString (map (\(k, v) -> (cs k, Just (cs v))) params) req -contentTypeJSON :: HTTP.Request -> HTTP.Request -contentTypeJSON = addHeader "Content-Type" "application/json" - contentTypeMixed :: HTTP.Request -> HTTP.Request contentTypeMixed = addHeader "Content-Type" "multipart/mixed" @@ -120,8 +139,8 @@ assertLabel status label resp = do onFailureAddResponse :: (HasCallStack) => Response -> App a -> App a onFailureAddResponse r m = App $ do e <- ask - liftIO $ E.catch (runAppWithEnv e m) $ \(AssertionFailure stack _ msg) -> do - E.throw (AssertionFailure stack (Just r) msg) + liftIO $ E.catch (runAppWithEnv e m) $ \(AssertionFailure stack _ ctx msg) -> do + E.throw (AssertionFailure stack (Just r) ctx msg) data Versioned = Versioned | Unversioned | ExplicitVersion Int deriving stock (Generic) @@ -130,21 +149,30 @@ data Versioned = Versioned | Unversioned | ExplicitVersion Int -- OwnDomain ...`. rawBaseRequest :: (HasCallStack, MakesValue domain) => domain -> Service -> Versioned -> String -> App HTTP.Request rawBaseRequest domain service versioned path = do + domainV <- objDomain domain + pathSegsPrefix <- case versioned of Versioned -> do - v <- asks (.defaultAPIVersion) + v <- getAPIVersionFor domainV pure ["v" <> show v] Unversioned -> pure [] ExplicitVersion v -> do pure ["v" <> show v] - domainV <- objDomain domain serviceMap <- getServiceMap domainV liftIO . HTTP.parseRequest $ let HostPort h p = serviceHostPort serviceMap service in "http://" <> h <> ":" <> show p <> ("/" <> joinHttpPath (pathSegsPrefix <> splitHttpPath path)) +getAPIVersionFor :: (MakesValue domain) => domain -> App Int +getAPIVersionFor domain = do + d <- asString domain + versionMap <- asks (.apiVersionByDomain) + case Map.lookup d versionMap of + Nothing -> asks (.defaultAPIVersion) + Just v -> pure v + baseRequest :: (HasCallStack, MakesValue user) => user -> Service -> Versioned -> String -> App HTTP.Request baseRequest user service versioned path = do req <- rawBaseRequest user service versioned path @@ -201,3 +229,12 @@ locationHeader = findHeader hLocation findHeader :: HTTP.HeaderName -> Response -> Maybe (HTTP.HeaderName, ByteString) findHeader name resp = find (\(name', _) -> name == name') resp.headers + +getQueryParam :: String -> String -> Maybe (Maybe String) +getQueryParam name url = + parseURI url + >>= lookup name + . fmap (bimap cs ((<$>) cs)) + . parseQuery + . cs + . uriQuery diff --git a/integration/test/Testlib/JSON.hs b/integration/test/Testlib/JSON.hs index a62065ed5f4..96ee6da2492 100644 --- a/integration/test/Testlib/JSON.hs +++ b/integration/test/Testlib/JSON.hs @@ -25,6 +25,7 @@ import Data.String import qualified Data.Text as T import qualified Data.Text.Encoding as T import Data.Vector ((!?)) +import qualified Data.Vector as V import GHC.Stack import Testlib.Types import Prelude @@ -237,7 +238,10 @@ lookupField val selector = do Object ob -> pure (KM.lookup (KM.fromString k) ob) -- index array Array arr -> case reads k of - [(i, "")] -> pure (arr !? i) + [(i, "")] -> + if i >= 0 + then pure (arr !? i) + else pure (arr !? (V.length arr + i)) _ -> assertFailureWithJSON arr $ "Invalid array index \"" <> k <> "\"" x -> assertFailureWithJSON x ("Object or Array" `typeWasExpectedButGot` x) go k [] v = get v k @@ -258,6 +262,23 @@ setField :: setField selector v x = do modifyField @a @Value selector (\_ -> pure (toJSON v)) x +-- | Merges fields if the old and new are both Objects or Arrays. Otherwise new +-- field overwrites the old completely +mergeField :: forall a b. (HasCallStack, MakesValue a, ToJSON b) => String -> b -> a -> App Value +mergeField selector v x = do + modifyField @a @Value + selector + ( \case + Just (Object old) -> case toJSON v of + (Object new) -> pure $ Object (new <> old) + nonObjectNew -> pure nonObjectNew + Just (Array old) -> case toJSON v of + (Array new) -> pure $ Array (old <> new) + nonArrayNew -> pure nonArrayNew + _ -> pure (toJSON v) + ) + x + member :: (HasCallStack, MakesValue a) => String -> a -> App Bool member k x = KM.member (KM.fromString k) <$> (make x >>= asObject) diff --git a/integration/test/Testlib/MockIntegrationService.hs b/integration/test/Testlib/MockIntegrationService.hs index 95dccb2fff7..3af81e87783 100644 --- a/integration/test/Testlib/MockIntegrationService.hs +++ b/integration/test/Testlib/MockIntegrationService.hs @@ -2,10 +2,13 @@ module Testlib.MockIntegrationService ( withMockServer, lhMockAppWithPrekeys, lhMockApp, + lhMockAppV, + lhMockNoCommonVersion, mkLegalHoldSettings, CreateMock (..), LiftedApplication, MockServerSettings (..), + LhApiVersion (..), ) where @@ -65,7 +68,10 @@ withMockServer settings mkApp go = withFreePortAnyAddr \(sPort, sock) -> do Nothing -> error . show =<< poll srv lhMockApp :: Chan (Wai.Request, LBS.ByteString) -> LiftedApplication -lhMockApp = lhMockAppWithPrekeys def +lhMockApp = lhMockAppWithPrekeys V0 def + +lhMockAppV :: LhApiVersion -> Chan (Wai.Request, LBS.ByteString) -> LiftedApplication +lhMockAppV v = lhMockAppWithPrekeys v def data MockServerSettings = MkMockServerSettings { -- | the certificate the mock service uses @@ -98,25 +104,47 @@ instance (App ~ f) => Default (CreateMock f) where somePrekeys = replicateM 3 getPrekey } +data LhApiVersion = V0 | V1 + deriving (Show, Generic) + -- | LegalHold service. Just fake the API, do not maintain any internal state. lhMockAppWithPrekeys :: - CreateMock App -> Chan (Wai.Request, LBS.ByteString) -> LiftedApplication -lhMockAppWithPrekeys mks ch req cont = withRunInIO \inIO -> do + LhApiVersion -> CreateMock App -> Chan (Wai.Request, LBS.ByteString) -> LiftedApplication +lhMockAppWithPrekeys version mks ch req cont = withRunInIO \inIO -> do reqBody <- Wai.strictRequestBody req writeChan ch (req, reqBody) inIO do - (nextLastPrekey, threePrekeys) <- - (,) - <$> mks.nextLastPrey - <*> mks.somePrekeys - case (cs <$> pathInfo req, cs $ requestMethod req, cs @_ @String <$> getRequestHeader "Authorization" req) of - (["legalhold", "status"], "GET", _) -> cont respondOk - (_, _, Nothing) -> cont missingAuth - (["legalhold", "initiate"], "POST", Just _) -> cont (initiateResp nextLastPrekey threePrekeys) - (["legalhold", "confirm"], "POST", Just _) -> cont respondOk - (["legalhold", "remove"], "POST", Just _) -> cont respondOk - _ -> cont respondBad + case version of + V0 -> + case (cs <$> pathInfo req, cs $ requestMethod req, cs @_ @String <$> getRequestHeader "Authorization" req) of + (["legalhold", "status"], "GET", _) -> cont respondOk + (_, _, Nothing) -> cont missingAuth + (["legalhold", "initiate"], "POST", Just _) -> do + (nextLastPrekey, threePrekeys) <- getPreyKeys + cont (initiateResp nextLastPrekey threePrekeys) + (["legalhold", "confirm"], "POST", Just _) -> cont respondOk + (["legalhold", "remove"], "POST", Just _) -> cont respondOk + _ -> cont respondBad + V1 -> + case (cs <$> pathInfo req, cs $ requestMethod req, cs @_ @String <$> getRequestHeader "Authorization" req) of + (["legalhold", "status"], "GET", _) -> cont respondOk + (["legalhold", "api-version"], "GET", _) -> cont $ apiVersionResp [0, 1] + (_, _, Nothing) -> cont missingAuth + (["legalhold", "initiate"], "POST", Just _) -> do + (nextLastPrekey, threePrekeys) <- getPreyKeys + cont (initiateResp nextLastPrekey threePrekeys) + (["legalhold", "confirm"], "POST", Just _) -> cont respondOk + (["legalhold", "remove"], "POST", Just _) -> cont respondOk + (["legalhold", "v1", "initiate"], "POST", Just _) -> do + (nextLastPrekey, threePrekeys) <- getPreyKeys + cont (initiateResp nextLastPrekey threePrekeys) + (["legalhold", "v1", "confirm"], "POST", Just _) -> cont respondOk + (["legalhold", "v1", "remove"], "POST", Just _) -> cont respondOk + _ -> cont respondBad where + getPreyKeys :: App (Value, [Value]) + getPreyKeys = (,) <$> mks.nextLastPrey <*> mks.somePrekeys + initiateResp :: Value -> [Value] -> Wai.Response initiateResp npk pks = responseLBS status200 [(hContentType, cs "application/json")] @@ -126,17 +154,34 @@ lhMockAppWithPrekeys mks ch req cont = withRunInIO \inIO -> do "last_prekey" .= npk ] - respondOk :: Wai.Response - respondOk = responseLBS status200 mempty mempty +apiVersionResp :: [Int] -> Wai.Response +apiVersionResp versions = + responseLBS status200 [(hContentType, cs "application/json")] + . encode + . Data.Aeson.object + $ [ "supported" .= versions + ] - respondBad :: Wai.Response - respondBad = responseLBS status404 mempty mempty +respondOk :: Wai.Response +respondOk = responseLBS status200 mempty mempty - missingAuth :: Wai.Response - missingAuth = responseLBS status400 mempty (cs "no authorization header") +respondBad :: Wai.Response +respondBad = responseLBS status404 mempty mempty - getRequestHeader :: String -> Wai.Request -> Maybe ByteString - getRequestHeader name = lookup (fromString name) . requestHeaders +missingAuth :: Wai.Response +missingAuth = responseLBS status400 mempty (cs "no authorization header") + +getRequestHeader :: String -> Wai.Request -> Maybe ByteString +getRequestHeader name = lookup (fromString name) . requestHeaders + +lhMockNoCommonVersion :: + Chan () -> LiftedApplication +lhMockNoCommonVersion _ req cont = withRunInIO \inIO -> do + inIO do + case (cs <$> pathInfo req, cs $ requestMethod req) of + (["legalhold", "status"], "GET") -> cont respondOk + (["legalhold", "api-version"], "GET") -> cont $ apiVersionResp [9999999] + _ -> cont respondBad mkLegalHoldSettings :: (String, Warp.Port) -> Value mkLegalHoldSettings (botHost, lhPort) = diff --git a/integration/test/Testlib/ModService.hs b/integration/test/Testlib/ModService.hs index 061acca529e..379547c4d2b 100644 --- a/integration/test/Testlib/ModService.hs +++ b/integration/test/Testlib/ModService.hs @@ -124,11 +124,14 @@ startDynamicBackends beOverrides k = when (Prelude.length beOverrides > 3) $ lift $ failApp "Too many backends. Currently only 3 are supported." pool <- asks (.resourcePool) resources <- acquireResources (Prelude.length beOverrides) pool - void $ traverseConcurrentlyCodensity (uncurry startDynamicBackend) (zip resources beOverrides) + void $ + traverseConcurrentlyCodensity + (void . uncurry startDynamicBackend) + (zip resources beOverrides) pure $ map (.berDomain) resources k -startDynamicBackend :: BackendResource -> ServiceOverrides -> Codensity App () +startDynamicBackend :: BackendResource -> ServiceOverrides -> Codensity App String startDynamicBackend resource beOverrides = do let overrides = mconcat @@ -136,10 +139,12 @@ startDynamicBackend resource beOverrides = do setEsIndex, setFederationSettings, setAwsConfigs, + setMlsPrivateKeyPaths, setLogLevel, beOverrides ] startBackend resource overrides + pure resource.berDomain where setAwsConfigs :: ServiceOverrides setAwsConfigs = @@ -200,6 +205,12 @@ startDynamicBackend resource beOverrides = do { brigCfg = setField "elasticsearch.index" resource.berElasticsearchIndex } + setMlsPrivateKeyPaths :: ServiceOverrides + setMlsPrivateKeyPaths = + def + { galleyCfg = setField "settings.mlsPrivateKeyPaths" resource.berMlsPrivateKeyPaths + } + setLogLevel :: ServiceOverrides setLogLevel = def @@ -449,9 +460,10 @@ startNginzLocal resource = do -- override port configuration let portConfigTemplate = [r|listen {localPort}; -listen {http2_port} http2; -listen {ssl_port} ssl http2; -listen [::]:{ssl_port} ssl http2; +listen {http2_port}; +listen {ssl_port} ssl; +listen [::]:{ssl_port} ssl; +http2 on; |] let portConfig = portConfigTemplate diff --git a/integration/test/Testlib/PTest.hs b/integration/test/Testlib/PTest.hs index 037cb276260..850960b2d2c 100644 --- a/integration/test/Testlib/PTest.hs +++ b/integration/test/Testlib/PTest.hs @@ -1,44 +1,45 @@ module Testlib.PTest where +import Control.Monad.Trans.Class +import Control.Monad.Trans.Writer import Data.Bifunctor (bimap) import Data.Char (toLower) import Data.Functor ((<&>)) import Data.Kind import Data.Proxy +import Data.Traversable import GHC.Generics import GHC.TypeLits import Testlib.Env +import Testlib.JSON import Testlib.Types import Prelude type Test = (String, String, String, String, App ()) +yieldTests :: (HasTests x) => String -> String -> String -> String -> x -> WriterT [Test] IO () +yieldTests m n s f x = do + t <- lift (mkTests m n s f x) + tell t + class HasTests x where - mkTests :: String -> String -> String -> String -> x -> [Test] + mkTests :: String -> String -> String -> String -> x -> IO [Test] instance HasTests (App ()) where - mkTests m n s f x = [(m, n, s, f, x)] + mkTests m n s f x = pure [(m, n, s, f, x)] instance (HasTests x, TestCases a) => HasTests (a -> x) where - mkTests m n s f x = - flip foldMap (testCases @a) \tc -> + mkTests m n s f x = do + tcs <- mkTestCases @a + fmap concat $ for tcs $ \tc -> mkTests m (n <> tc.testCaseName) s f (x tc.testCase) data TestCase a = MkTestCase {testCaseName :: String, testCase :: a} - deriving stock (Eq, Ord, Show, Generic) + deriving stock (Eq, Ord, Show, Generic, Functor, Foldable, Traversable) -- | enumerate all members of a bounded enum type --- --- >>> testCases @Bool --- [MkTestCase {testCaseName = "[bool=false]", testCase = False},MkTestCase {testCaseName = "[bool=true]", testCase = True}] --- >>> testCases @Domain --- [MkTestCase {testCaseName = "[domain=owndomain]", testCase = OwnDomain},MkTestCase {testCaseName = "[domain=otherdomain]", testCase = OtherDomain}] --- >>> testCases @Ciphersuite --- [MkTestCase {testCaseName = "[suite=0x0001]", testCase = Ciphersuite {code = "0x0001"}},MkTestCase {testCaseName = "[suite=0xf031]", testCase = Ciphersuite {code = "0xf031"}}] --- >>> testCases @(Tagged "foo" Bool) --- [MkTestCase {testCaseName = "[foo=false]", testCase = MkTagged {unTagged = False}},MkTestCase {testCaseName = "[foo=true]", testCase = MkTagged {unTagged = True}}] class TestCases a where - testCases :: [TestCase a] + mkTestCases :: IO [TestCase a] type Tagged :: Symbol -> Type -> Type newtype Tagged s a = MkTagged {unTagged :: a} @@ -52,21 +53,20 @@ pattern TaggedBool a = MkTagged a {-# COMPLETE TaggedBool #-} -- | only works for outer-most use of `Tagged` (not: `Maybe (Tagged "bla" Bool)`) --- --- >>> testCases @(Tagged "bla" Bool) instance (GEnum (Rep a), KnownSymbol s, Generic a) => TestCases (Tagged s a) where - testCases = - uni @(Rep a) <&> \case - -- replace the toplevel - (Left _ : ls, tc) -> - MkTestCase - { testCaseName = foldr mkName "" (Left (symbolVal @s Proxy) : ls), - testCase = MkTagged $ to tc - } - _ -> error "tagged test cases: impossible" + mkTestCases = + pure $ + uni @(Rep a) <&> \case + -- replace the toplevel + (Left _ : ls, tc) -> + MkTestCase + { testCaseName = foldr mkName "" (Left (symbolVal @s Proxy) : ls), + testCase = MkTagged $ to tc + } + _ -> error "tagged test cases: impossible" instance TestCases Ciphersuite where - testCases = do + mkTestCases = pure $ do suite <- allCiphersuites pure $ MkTestCase @@ -75,20 +75,22 @@ instance TestCases Ciphersuite where } instance TestCases CredentialType where - testCases = - [ MkTestCase "[ctype=basic]" BasicCredentialType, - MkTestCase "[ctype=x509]" X509CredentialType - ] + mkTestCases = + pure + [ MkTestCase "[ctype=basic]" BasicCredentialType, + MkTestCase "[ctype=x509]" X509CredentialType + ] -- | a default instance, normally we don't do such things but this is more convenient in -- the test suite as you don't have to derive anything instance {-# OVERLAPPABLE #-} (Generic a, GEnum (Rep a)) => TestCases a where - testCases = - uni @(Rep a) <&> \(tcn, tc) -> - MkTestCase - { testCaseName = foldr mkName "" tcn, - testCase = to tc - } + mkTestCases = + pure $ + uni @(Rep a) <&> \(tcn, tc) -> + MkTestCase + { testCaseName = foldr mkName "" tcn, + testCase = to tc + } {-# INLINE [1] mkName #-} mkName :: Either String String -> String -> String @@ -118,3 +120,15 @@ instance GEnum U1 where instance (GEnum (Rep k), Generic k) => GEnum (K1 r k) where uni = fmap (K1 . to) <$> uni @(Rep k) + +data OneOf a b = OneOfA a | OneOfB b + +instance (MakesValue a, MakesValue b) => MakesValue (OneOf a b) where + make (OneOfA a) = make a + make (OneOfB b) = make b + +instance (TestCases a, TestCases b) => TestCases (OneOf a b) where + mkTestCases = do + as <- fmap (map (fmap OneOfA)) mkTestCases + bs <- fmap (map (fmap OneOfB)) mkTestCases + pure $ as <> bs diff --git a/integration/test/Testlib/Prelude.hs b/integration/test/Testlib/Prelude.hs index 69c3797f54d..3bacfc4dd82 100644 --- a/integration/test/Testlib/Prelude.hs +++ b/integration/test/Testlib/Prelude.hs @@ -52,15 +52,6 @@ module Testlib.Prelude putStr, putStrLn, print, - getChar, - getLine, - getContents, - interact, - readFile, - writeFile, - appendFile, - readIO, - readLn, liftIO, -- * Functor @@ -186,33 +177,6 @@ putStrLn = liftIO . P.putStrLn print :: (Show a, MonadIO m) => a -> m () print = liftIO . P.print -getChar :: (MonadIO m) => m Char -getChar = liftIO P.getChar - -getLine :: (MonadIO m) => m String -getLine = liftIO P.getLine - -getContents :: (MonadIO m) => m String -getContents = liftIO P.getContents - -interact :: (MonadIO m) => (String -> String) -> m () -interact = liftIO . P.interact - -readFile :: (MonadIO m) => FilePath -> m String -readFile = liftIO . P.readFile - -writeFile :: (MonadIO m) => FilePath -> String -> m () -writeFile = fmap liftIO . P.writeFile - -appendFile :: (MonadIO m) => FilePath -> String -> m () -appendFile = fmap liftIO . P.appendFile - -readIO :: (Read a, MonadIO m) => String -> m a -readIO = liftIO . P.readIO - -readLn :: (Read a, MonadIO m) => m a -readLn = liftIO P.readLn - ---------------------------------------------------------------------- -- Functor diff --git a/integration/test/Testlib/ResourcePool.hs b/integration/test/Testlib/ResourcePool.hs index c67b7031e43..aa518939fef 100644 --- a/integration/test/Testlib/ResourcePool.hs +++ b/integration/test/Testlib/ResourcePool.hs @@ -15,6 +15,7 @@ import Control.Concurrent import Control.Monad.Catch import Control.Monad.Codensity import Control.Monad.IO.Class +import Data.Aeson import Data.Foldable (for_) import Data.Functor import Data.IORef @@ -124,7 +125,8 @@ backendResources dynConfs = berVHost = dynConf.domain, berNginzSslPort = Ports.portForDyn Ports.NginzSSL i, berNginzHttp2Port = Ports.portForDyn Ports.NginzHttp2 i, - berInternalServicePorts = Ports.internalServicePorts name + berInternalServicePorts = Ports.internalServicePorts name, + berMlsPrivateKeyPaths = dynConf.mlsPrivateKeyPaths } ) where @@ -154,7 +156,17 @@ backendA = berVHost = "backendA", berNginzSslPort = Ports.port Ports.NginzSSL BackendA, berInternalServicePorts = Ports.internalServicePorts BackendA, - berNginzHttp2Port = Ports.port Ports.NginzHttp2 BackendA + berNginzHttp2Port = Ports.port Ports.NginzHttp2 BackendA, + berMlsPrivateKeyPaths = + object + [ fromString "removal" + .= object + [ fromString "ed25519" .= "test/resources/backendA/ed25519.pem", + fromString "ecdsa_secp256r1_sha256" .= "test/resources/backendA/ecdsa_secp256r1_sha256.pem", + fromString "ecdsa_secp384r1_sha384" .= "test/resources/backendA/ecdsa_secp384r1_sha384.pem", + fromString "ecdsa_secp521r1_sha512" .= "test/resources/backendA/ecdsa_secp521r1_sha512.pem" + ] + ] } backendB :: BackendResource @@ -183,5 +195,15 @@ backendB = berVHost = "backendB", berNginzSslPort = Ports.port Ports.NginzSSL BackendB, berInternalServicePorts = Ports.internalServicePorts BackendB, - berNginzHttp2Port = Ports.port Ports.NginzHttp2 BackendB + berNginzHttp2Port = Ports.port Ports.NginzHttp2 BackendB, + berMlsPrivateKeyPaths = + object + [ fromString "removal" + .= object + [ fromString "ed25519" .= "test/resources/backendB/ed25519.pem", + fromString "ecdsa_secp256r1_sha256" .= "test/resources/backendB/ecdsa_secp256r1_sha256.pem", + fromString "ecdsa_secp384r1_sha384" .= "test/resources/backendB/ecdsa_secp384r1_sha384.pem", + fromString "ecdsa_secp521r1_sha512" .= "test/resources/backendB/ecdsa_secp521r1_sha512.pem" + ] + ] } diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index 431530c91e0..d5385a16376 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -1,34 +1,22 @@ -module Testlib.Run (main, mainI, createGlobalEnv) where +module Testlib.Run (main, mainI) where import Control.Concurrent import Control.Exception as E import Control.Monad import Control.Monad.Codensity import Control.Monad.IO.Class -import Control.Monad.Reader -import Crypto.Error -import qualified Crypto.PubKey.Ed25519 as Ed25519 -import Data.Aeson (Value) -import Data.ByteArray (convert) -import Data.ByteString (ByteString) -import qualified Data.ByteString as B import Data.Foldable import Data.Function import Data.Functor import Data.List -import qualified Data.Map as Map -import Data.PEM import Data.Time.Clock -import Data.Traversable (for) import RunAllTests import System.Directory import System.Environment import System.Exit import System.FilePath -import Testlib.App import Testlib.Assertions import Testlib.Env -import Testlib.JSON import Testlib.Options import Testlib.Printing import Testlib.Types @@ -99,6 +87,7 @@ main = do let f = testFilter opts cfg = opts.configFile + allTests <- mkAllTests let tests = filter (\(qname, _, _, _) -> f qname) . sortOn (\(qname, _, _, _) -> qname) @@ -111,67 +100,6 @@ main = do if opts.listTests then doListTests tests else runTests tests opts.xmlReport cfg -createGlobalEnv :: FilePath -> Codensity IO GlobalEnv -createGlobalEnv cfg = do - genv0 <- mkGlobalEnv cfg - -- Run codensity locally here, because we only need the environment to get at - -- Galley's configuration. Accessing the environment has the side effect of - -- creating a temporary mls directory, which we don't need here. - - let removalKeysDir = gTempDir genv0 "removal-keys" - keys <- liftIO . lowerCodensity $ do - env <- mkEnv genv0 - liftIO $ createDirectoryIfMissing True removalKeysDir - liftIO . runAppWithEnv env $ do - config <- readServiceConfig Galley - for - [ ("ed25519", loadEd25519Key), - ("ecdsa_secp256r1_sha256", loadEcKey "ecdsa_secp256r1_sha256" 73), - ("ecdsa_secp384r1_sha384", loadEcKey "ecdsa_secp384r1_sha384" 88), - ("ecdsa_secp521r1_sha512", loadEcKey "ecdsa_secp521r1_sha512" 108) - ] - $ \(sigScheme, load) -> do - key <- load config - let path = removalKeysDir (sigScheme <> ".key") - liftIO $ B.writeFile path key - pure (sigScheme, path) - - -- save removal key to a temporary file - pure genv0 {gRemovalKeyPaths = Map.fromList keys} - -getPrivateKeyPath :: Value -> String -> App FilePath -getPrivateKeyPath config signatureScheme = do - relPath <- config %. "settings.mlsPrivateKeyPaths.removal" %. signatureScheme & asString - asks \env' -> case env'.servicesCwdBase of - Nothing -> relPath - Just dir -> dir "galley" relPath - -loadEcKey :: String -> Int -> Value -> App ByteString -loadEcKey sigScheme offset config = do - path <- getPrivateKeyPath config sigScheme - bs <- liftIO $ B.readFile path - pems <- case pemParseBS bs of - Left err -> assertFailure $ "Could not parse removal key PEM: " <> err - Right x -> pure x - asn1 <- pemContent <$> assertOne pems - -- quick and dirty ASN.1 decoding: assume the key is of the correct - -- format, and simply skip the header - pure $ B.drop offset asn1 - -loadEd25519Key :: Value -> App ByteString -loadEd25519Key config = do - path <- getPrivateKeyPath config "ed25519" - bs <- liftIO $ B.readFile path - pems <- case pemParseBS bs of - Left err -> assertFailure $ "Could not parse removal key PEM: " <> err - Right x -> pure x - asn1 <- pemContent <$> assertOne pems - -- quick and dirty ASN.1 decoding: assume the key is of the correct - -- format, and simply skip the 16 byte header - let bytes = B.drop 16 asn1 - priv <- liftIO . throwCryptoErrorIO $ Ed25519.secretKey bytes - pure (convert (Ed25519.toPublic priv)) - runTests :: [(String, x, y, App ())] -> Maybe FilePath -> FilePath -> IO () runTests tests mXMLOutput cfg = do output <- newChan @@ -181,7 +109,7 @@ runTests tests mXMLOutput cfg = do Nothing -> pure () let writeOutput = writeChan output . Just - runCodensity (createGlobalEnv cfg) $ \genv -> + runCodensity (mkGlobalEnv cfg) $ \genv -> withAsync displayOutput $ \displayThread -> do -- Currently 4 seems to be stable, more seems to create more timeouts. report <- fmap mconcat $ pooledForConcurrentlyN 4 tests $ \(qname, _, _, action) -> do diff --git a/integration/test/Testlib/RunServices.hs b/integration/test/Testlib/RunServices.hs index e5c5c7611ce..a88686b2979 100644 --- a/integration/test/Testlib/RunServices.hs +++ b/integration/test/Testlib/RunServices.hs @@ -1,16 +1,15 @@ -module Testlib.RunServices where +module Testlib.RunServices (main) where import Control.Concurrent import Control.Monad.Codensity +import Options.Applicative import System.Directory -import System.Environment (getArgs) -import System.Exit (exitWith) +import System.Exit import System.FilePath import System.Posix (getWorkingDirectory) import System.Process import Testlib.Prelude import Testlib.ResourcePool -import Testlib.Run (createGlobalEnv) parentDir :: FilePath -> Maybe FilePath parentDir path = @@ -32,32 +31,83 @@ findProjectRoot path = do Nothing -> pure Nothing Just p -> findProjectRoot p +data Opts = Opts + { withManualTestingOverrides :: Bool, + runSubprocess :: [String] + } + deriving (Show) + +optsParser :: Parser Opts +optsParser = + Opts + <$> switch + ( long "with-manual-testing-overrides" + <> short 'm' + <> help "Run services with settings tuned for manual app usage (not recommended for running integration tests)" + ) + <*> many + ( strArgument + ( metavar "COMMAND_WITH_ARGS" + <> help "When specified, the command will be run after services have started and service will be killed after the command exits" + ) + ) + main :: IO () main = do cwd <- getWorkingDirectory mbProjectRoot <- findProjectRoot cwd + opts <- execParser (info (optsParser <**> helper) fullDesc) cfg <- case mbProjectRoot of Nothing -> error "Could not find project root. Please make sure you call run-services from somewhere in wire-server." Just projectRoot -> pure $ joinPath [projectRoot, "services/integration.yaml"] - args <- getArgs - - let run = case args of + let run = case opts.runSubprocess of [] -> do putStrLn "services started" - forever (threadDelay 1000000000) + forever (threadDelay maxBound) _ -> do - let cp = proc "sh" (["-c", "exec \"$@\"", "--"] <> args) + let cp = proc "sh" (["-c", "exec \"$@\"", "--"] <> opts.runSubprocess) (_, _, _, ph) <- createProcess cp exitWith =<< waitForProcess ph - runCodensity (createGlobalEnv cfg >>= mkEnv) $ \env -> + runCodensity (mkGlobalEnv cfg >>= mkEnv) $ \env -> runAppWithEnv env $ lowerCodensity $ do _modifyEnv <- traverseConcurrentlyCodensity - (\r -> startDynamicBackend r mempty) + ( \r -> + void + $ if opts.withManualTestingOverrides + then startDynamicBackend r manualTestingOverrides + else startDynamicBackend r mempty + ) [backendA, backendB] liftIO run + +manualTestingOverrides :: ServiceOverrides +manualTestingOverrides = + let smtpEndpoint = object ["host" .= "localhost", "port" .= (2500 :: Int)] + authSettings = + object + [ "userTokenTimeout" .= (4838400 :: Int), + "sessionTokenTimeout" .= (86400 :: Int), + "accessTokenTimeout" .= (900 :: Int), + "providerTokenTimeout" .= (900 :: Int), + "legalHoldUserTokenTimeout" .= (4838400 :: Int), + "legalHoldAccessTokenTimeout" .= (900 :: Int) + ] + in def + { brigCfg = + mergeField "emailSMS.email.smtpEndpoint" smtpEndpoint + >=> setField "emailSMS.email.smtpConnType" "plain" + >=> removeField "emailSMS.email.sesQueue" + >=> removeField "emailSMS.email.sesEndpoint" + >=> mergeField "zauth.authSettings" authSettings + >=> setField @_ @Int "optSettings.setActivationTimeout" 3600 + >=> setField @_ @Int "optSettings.setVerificationTimeout" 3600 + >=> setField @_ @Int "optSettings.setTeamInvitationTimeout" 3600 + >=> setField @_ @Int "optSettings.setUserCookieRenewAge" 1209600 + >=> removeField "optSettings.setSuspendInactiveUsers" + } diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index e77e8b0a457..e25b33d06f8 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -11,6 +11,7 @@ import Control.Monad.Base import Control.Monad.Catch import Control.Monad.Reader import Control.Monad.Trans.Control +import Crypto.Random (MonadRandom (..)) import Data.Aeson import qualified Data.Aeson as Aeson import Data.ByteString (ByteString) @@ -68,7 +69,8 @@ data BackendResource = BackendResource berVHost :: String, berNginzSslPort :: Word16, berNginzHttp2Port :: Word16, - berInternalServicePorts :: forall a. (Num a) => Service -> a + berInternalServicePorts :: forall a. (Num a) => Service -> a, + berMlsPrivateKeyPaths :: Value } instance Eq BackendResource where @@ -79,7 +81,8 @@ instance Ord BackendResource where data DynamicBackendConfig = DynamicBackendConfig { domain :: String, - federatorExternalPort :: Word16 + federatorExternalPort :: Word16, + mlsPrivateKeyPaths :: Value } deriving (Show, Generic) @@ -105,11 +108,11 @@ data GlobalEnv = GlobalEnv gDomain2 :: String, gIntegrationTestHostName :: String, gFederationV0Domain :: String, + gFederationV1Domain :: String, gDynamicDomains :: [String], gDefaultAPIVersion :: Int, gManager :: HTTP.Manager, gServicesCwdBase :: Maybe FilePath, - gRemovalKeyPaths :: Map String FilePath, gBackendResourcePool :: ResourcePool BackendResource, gRabbitMQConfig :: RabbitMQConfig, gTempDir :: FilePath, @@ -120,6 +123,7 @@ data IntegrationConfig = IntegrationConfig { backendOne :: BackendConfig, backendTwo :: BackendConfig, federationV0 :: BackendConfig, + federationV1 :: BackendConfig, integrationTestHostName :: String, dynamicBackends :: Map String DynamicBackendConfig, rabbitmq :: RabbitMQConfig, @@ -134,6 +138,7 @@ instance FromJSON IntegrationConfig where <$> parseJSON (Object o) <*> o .: fromString "backendTwo" <*> o .: fromString "federation-v0" + <*> o .: fromString "federation-v1" <*> o .: fromString "integrationTestHostName" <*> o .: fromString "dynamicBackends" <*> o .: fromString "rabbitmq" @@ -201,12 +206,12 @@ data Env = Env domain2 :: String, integrationTestHostName :: String, federationV0Domain :: String, + federationV1Domain :: String, dynamicDomains :: [String], defaultAPIVersion :: Int, + apiVersionByDomain :: Map String Int, manager :: HTTP.Manager, servicesCwdBase :: Maybe FilePath, - -- | paths to removal keys by signature scheme - removalKeyPaths :: Map String FilePath, prekeys :: IORef [(Int, String)], lastPrekeys :: IORef [String], mls :: IORef MLSState, @@ -237,6 +242,9 @@ data ClientIdentity = ClientIdentity } deriving stock (Show, Eq, Ord, Generic) +instance HasField "qualifiedUserId" ClientIdentity Aeson.Value where + getField cid = object [fromString "id" .= cid.user, fromString "domain" .= cid.domain] + newtype Ciphersuite = Ciphersuite {code :: String} deriving (Eq, Ord, Show, Generic) @@ -304,14 +312,15 @@ getRequestBody req = case HTTP.requestBody req of data AssertionFailure = AssertionFailure { callstack :: CallStack, response :: Maybe Response, + context :: Maybe String, msg :: String } instance Show AssertionFailure where - show (AssertionFailure _ _ msg) = "AssertionFailure _ _ " <> show msg + show (AssertionFailure _ _ _ msg) = "AssertionFailure _ _ _ " <> show msg instance Exception AssertionFailure where - displayException (AssertionFailure _ _ msg) = msg + displayException (AssertionFailure _ _ _ msg) = msg newtype App a = App {unApp :: ReaderT Env IO a} deriving newtype @@ -328,6 +337,9 @@ newtype App a = App {unApp :: ReaderT Env IO a} MonadBaseControl IO ) +instance MonadRandom App where + getRandomBytes n = liftIO (getRandomBytes n) + runAppWithEnv :: Env -> App a -> IO a runAppWithEnv e m = runReaderT (unApp m) e @@ -380,7 +392,7 @@ assertFailure :: (HasCallStack) => String -> App a assertFailure msg = forceList msg $ liftIO $ - E.throw (AssertionFailure callStack Nothing msg) + E.throw (AssertionFailure callStack Nothing Nothing msg) where forceList [] y = y forceList (x : xs) y = seq x (forceList xs y) @@ -393,10 +405,12 @@ assertNothing :: (HasCallStack) => Maybe a -> App () assertNothing = maybe (pure ()) $ const $ assertFailure "Maybe value was Just, not Nothing" addFailureContext :: String -> App a -> App a -addFailureContext msg = modifyFailureMsg (\m -> m <> "\nThis failure happened in this context:\n" <> msg) +addFailureContext ctx = modifyFailureContext (\mCtx0 -> Just $ maybe ctx (\x -> ctx <> "\n" <> x) mCtx0) -modifyFailureMsg :: (String -> String) -> App a -> App a -modifyFailureMsg modMessage = modifyFailure (\e -> e {msg = modMessage e.msg}) +modifyFailureContext :: (Maybe String -> Maybe String) -> App a -> App a +modifyFailureContext modContext = + modifyFailure + (\e -> e {context = modContext e.context}) modifyFailure :: (AssertionFailure -> AssertionFailure) -> App a -> App a modifyFailure modifyAssertion action = do diff --git a/integration/test/Testlib/VersionedFed.hs b/integration/test/Testlib/VersionedFed.hs new file mode 100644 index 00000000000..7f18da0a401 --- /dev/null +++ b/integration/test/Testlib/VersionedFed.hs @@ -0,0 +1,66 @@ +module Testlib.VersionedFed where + +import Control.Monad.Reader +import Data.Proxy +import qualified Data.Text as T +import GHC.TypeLits +import System.Environment +import Testlib.PTest +import Testlib.Prelude + +data FedDomain n = FedDomain + +instance MakesValue (FedDomain 0) where + make FedDomain = asks (String . T.pack . (.federationV0Domain)) + +instance MakesValue (FedDomain 1) where + make FedDomain = asks (String . T.pack . (.federationV1Domain)) + +instance (KnownNat n) => TestCases (FedDomain n) where + mkTestCases = + let v = natVal (Proxy @n) + in map (fmap (const FedDomain)) + <$> mkFedTestCase ("[domain=fed-v" <> show v <> "]") v + +mkFedTestCase :: String -> Integer -> IO [TestCase Integer] +mkFedTestCase name n = do + v <- lookupEnv $ "ENABLE_FEDERATION_V" <> show n + if v == Just "1" + then pure [MkTestCase name n] + else pure [] + +data AnyFedDomain = AnyFedDomain Integer + +instance MakesValue AnyFedDomain where + make (AnyFedDomain 0) = asks (String . T.pack . (.federationV0Domain)) + make (AnyFedDomain 1) = asks (String . T.pack . (.federationV1Domain)) + make (AnyFedDomain _) = error "invalid federation version" + +instance TestCases AnyFedDomain where + mkTestCases = + map (fmap AnyFedDomain) + . concat + <$> traverse + (uncurry mkFedTestCase) + [("[domain=fed-v" <> show v <> "]", v) | v <- [0, 1]] + +-- | This can be used as an argument for parametrised tests. It will be bound +-- to at least 'OtherDomain', and optionally to legacy federated domains, +-- according to the values of the corresponding environment variables +-- (@ENABLE_FEDERATION_V0@ and similar). +data StaticDomain = StaticDomain | StaticFedDomain Integer + deriving (Eq) + +instance MakesValue StaticDomain where + make StaticDomain = make OtherDomain + make (StaticFedDomain n) = make (AnyFedDomain n) + +instance TestCases StaticDomain where + mkTestCases = do + feds <- + map (fmap StaticFedDomain) + . concat + <$> traverse + (uncurry mkFedTestCase) + [("[domain=fed-v" <> show v <> "]", v) | v <- [0, 1]] + pure $ [MkTestCase "[domain=other]" StaticDomain] <> feds diff --git a/libs/bilge/bilge.cabal b/libs/bilge/bilge.cabal index b3b4154bbc7..8e64bbe92e5 100644 --- a/libs/bilge/bilge.cabal +++ b/libs/bilge/bilge.cabal @@ -31,6 +31,7 @@ library default-extensions: AllowAmbiguousTypes BangPatterns + BlockArguments ConstraintKinds DataKinds DefaultSignatures @@ -97,5 +98,6 @@ library , uri-bytestring , wai , wai-extra + , wire-otel default-language: GHC2021 diff --git a/libs/bilge/default.nix b/libs/bilge/default.nix index 8c35f0746ad..1844d50b1d2 100644 --- a/libs/bilge/default.nix +++ b/libs/bilge/default.nix @@ -26,6 +26,7 @@ , uri-bytestring , wai , wai-extra +, wire-otel }: mkDerivation { pname = "bilge"; @@ -53,6 +54,7 @@ mkDerivation { uri-bytestring wai wai-extra + wire-otel ]; description = "Library for composing HTTP requests"; license = lib.licenses.agpl3Only; diff --git a/libs/bilge/src/Bilge/RPC.hs b/libs/bilge/src/Bilge/RPC.hs index 77edab5326f..182bd303488 100644 --- a/libs/bilge/src/Bilge/RPC.hs +++ b/libs/bilge/src/Bilge/RPC.hs @@ -23,7 +23,6 @@ module Bilge.RPC RPCException (..), rpc, rpc', - statusCheck, parseResponse, rpcExceptionMsg, ) @@ -34,13 +33,14 @@ import Bilge.Request import Bilge.Response import Control.Error hiding (err) import Control.Monad.Catch (MonadCatch, MonadThrow (..), try) -import Control.Monad.Except import Data.Aeson (FromJSON, eitherDecode') import Data.CaseInsensitive (original) import Data.Text.Lazy (pack) +import Data.Text.Lazy qualified as T import Imports hiding (log) import Network.HTTP.Client qualified as HTTP import System.Logger.Class +import Wire.OpenTelemetry (withClientInstrumentation) class HasRequestId m where getRequestId :: m RequestId @@ -71,7 +71,7 @@ instance Show RPCException where . showString "}" rpc :: - (MonadIO m, MonadCatch m, MonadHttp m, HasRequestId m) => + (MonadUnliftIO m, MonadCatch m, MonadHttp m, HasRequestId m) => LText -> (Request -> Request) -> m (Response (Maybe LByteString)) @@ -83,7 +83,7 @@ rpc sys = rpc' sys empty -- Note: 'syncIO' is wrapped around the IO action performing the request -- and any exceptions caught are re-thrown in an 'RPCException'. rpc' :: - (MonadIO m, MonadCatch m, MonadHttp m, HasRequestId m) => + (MonadUnliftIO m, MonadCatch m, MonadHttp m, HasRequestId m) => -- | A label for the remote system in case of 'RPCException's. LText -> Request -> @@ -91,8 +91,9 @@ rpc' :: m (Response (Maybe LByteString)) rpc' sys r f = do rId <- getRequestId - let rq = f . requestId rId $ r - res <- try $ httpLbs rq id + let rq = f $ requestId rId r + res <- try $ withClientInstrumentation ("intra-call-to-" <> T.toStrict sys) \k -> do + k rq \r' -> httpLbs r' id case res of Left x -> throwM $ RPCException sys rq x Right x -> pure x @@ -104,17 +105,6 @@ rpcExceptionMsg (RPCException sys req ex) = headers = foldr hdr id (HTTP.requestHeaders req) hdr (k, v) x = x ~~ original k .= v -statusCheck :: - (MonadError e m) => - Int -> - (LText -> e) -> - Response (Maybe LByteString) -> - m () -statusCheck c f r = - unless (statusCode r == c) $ - throwError $ - f ("unexpected status code: " <> pack (show $ statusCode r)) - parseResponse :: (Exception e, MonadThrow m, FromJSON a) => (LText -> e) -> diff --git a/libs/bilge/src/Bilge/TestSession.hs b/libs/bilge/src/Bilge/TestSession.hs index 246b7a17bcb..4f49c2d23e6 100644 --- a/libs/bilge/src/Bilge/TestSession.hs +++ b/libs/bilge/src/Bilge/TestSession.hs @@ -40,6 +40,3 @@ liftSession session = SessionT $ do let resultInState = runReaderT session app let resultInIO = ST.evalStateT resultInState clientState liftIO resultInIO - -runSessionT :: (Monad m) => SessionT m a -> Wai.Application -> m a -runSessionT session app = ST.evalStateT (runReaderT (unSessionT session) app) WaiTest.initState diff --git a/libs/brig-types/brig-types.cabal b/libs/brig-types/brig-types.cabal index 7f294c52fac..817720e0b65 100644 --- a/libs/brig-types/brig-types.cabal +++ b/libs/brig-types/brig-types.cabal @@ -17,7 +17,6 @@ library Brig.Types.Instances Brig.Types.Intra Brig.Types.Provider.Tag - Brig.Types.Search Brig.Types.Team Brig.Types.Team.LegalHold Brig.Types.Test.Arbitrary @@ -73,16 +72,12 @@ library -funbox-strict-fields -Wredundant-constraints -Wunused-packages build-depends: - aeson >=2.0.1.0 - , attoparsec >=0.10 - , base >=4 && <5 - , bytestring + base >=4 && <5 , bytestring-conversion >=0.2 , cassandra-util , containers >=0.5 , imports , QuickCheck >=2.9 - , text >=0.11 , types-common >=0.16 , wire-api @@ -148,15 +143,13 @@ test-suite brig-types-tests -Wunused-packages build-depends: - aeson >=2.0.1.0 - , base >=4 && <5 + aeson >=2.0.1.0 + , base >=4 && <5 , brig-types - , bytestring-conversion >=0.3.1 , imports , openapi3 - , QuickCheck >=2.9 + , QuickCheck >=2.9 , tasty - , tasty-hunit , tasty-quickcheck , wire-api diff --git a/libs/brig-types/default.nix b/libs/brig-types/default.nix index 78932b5d379..d427109a406 100644 --- a/libs/brig-types/default.nix +++ b/libs/brig-types/default.nix @@ -4,9 +4,7 @@ # dependencies are added or removed. { mkDerivation , aeson -, attoparsec , base -, bytestring , bytestring-conversion , cassandra-util , containers @@ -16,9 +14,7 @@ , openapi3 , QuickCheck , tasty -, tasty-hunit , tasty-quickcheck -, text , types-common , wire-api }: @@ -27,28 +23,22 @@ mkDerivation { version = "1.35.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ - aeson - attoparsec base - bytestring bytestring-conversion cassandra-util containers imports QuickCheck - text types-common wire-api ]; testHaskellDepends = [ aeson base - bytestring-conversion imports openapi3 QuickCheck tasty - tasty-hunit tasty-quickcheck wire-api ]; diff --git a/libs/brig-types/src/Brig/Types/Intra.hs b/libs/brig-types/src/Brig/Types/Intra.hs index e224a62419e..6b0a81ca597 100644 --- a/libs/brig-types/src/Brig/Types/Intra.hs +++ b/libs/brig-types/src/Brig/Types/Intra.hs @@ -16,7 +16,7 @@ -- with this program. If not, see . module Brig.Types.Intra - ( UserAccount (..), + ( User (..), NewUserScimInvitation (..), UserSet (..), ) diff --git a/libs/brig-types/src/Brig/Types/Search.hs b/libs/brig-types/src/Brig/Types/Search.hs deleted file mode 100644 index 2a5006968f6..00000000000 --- a/libs/brig-types/src/Brig/Types/Search.hs +++ /dev/null @@ -1,107 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# 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 . - -module Brig.Types.Search - ( TeamSearchInfo (..), - SearchVisibilityInbound (..), - defaultSearchVisibilityInbound, - searchVisibilityInboundFromFeatureStatus, - ) -where - -import Cassandra qualified as C -import Data.Aeson -import Data.Attoparsec.ByteString -import Data.ByteString.Builder -import Data.ByteString.Conversion -import Data.ByteString.Lazy -import Data.Id (TeamId) -import Data.Text.Encoding -import Imports -import Test.QuickCheck -import Wire.API.Team.Feature - --- | Outbound search restrictions configured by team admin of the searcher. This --- value restricts the set of user that are searched. --- --- See 'optionallySearchWithinTeam' for the effect on full-text search. --- --- See 'mkTeamSearchInfo' for the business logic that defines the TeamSearchInfo --- value. --- --- Search results might be affected by the inbound search restriction settings of --- the searched user. ('SearchVisibilityInbound') -data TeamSearchInfo - = -- | Only users that are not part of any team are searched - NoTeam - | -- | Only users from the same team as the searcher are searched - TeamOnly TeamId - | -- | No search restrictions, all users are searched - AllUsers - --- | Inbound search restrictions configured by team to-be-searched. Affects only --- full-text search (i.e. search on the display name and the handle), not exact --- handle search. -data SearchVisibilityInbound - = -- | The user can only be found by users from the same team - SearchableByOwnTeam - | -- | The user can by found by any user of any team - SearchableByAllTeams - deriving (Eq, Show) - -instance Arbitrary SearchVisibilityInbound where - arbitrary = elements [SearchableByOwnTeam, SearchableByAllTeams] - -instance ToByteString SearchVisibilityInbound where - builder SearchableByOwnTeam = "searchable-by-own-team" - builder SearchableByAllTeams = "searchable-by-all-teams" - -instance FromByteString SearchVisibilityInbound where - parser = - SearchableByOwnTeam - <$ string "searchable-by-own-team" - <|> SearchableByAllTeams - <$ string "searchable-by-all-teams" - -instance C.Cql SearchVisibilityInbound where - ctype = C.Tagged C.IntColumn - - toCql SearchableByOwnTeam = C.CqlInt 0 - toCql SearchableByAllTeams = C.CqlInt 1 - - fromCql (C.CqlInt 0) = pure SearchableByOwnTeam - fromCql (C.CqlInt 1) = pure SearchableByAllTeams - fromCql n = Left $ "Unexpected SearchVisibilityInbound: " ++ show n - -defaultSearchVisibilityInbound :: SearchVisibilityInbound -defaultSearchVisibilityInbound = SearchableByOwnTeam - -searchVisibilityInboundFromFeatureStatus :: FeatureStatus -> SearchVisibilityInbound -searchVisibilityInboundFromFeatureStatus FeatureStatusDisabled = SearchableByOwnTeam -searchVisibilityInboundFromFeatureStatus FeatureStatusEnabled = SearchableByAllTeams - -instance ToJSON SearchVisibilityInbound where - toJSON = String . decodeUtf8 . toStrict . toLazyByteString . builder - -instance FromJSON SearchVisibilityInbound where - parseJSON = withText "SearchVisibilityInbound" $ \str -> - case runParser (parser @SearchVisibilityInbound) (encodeUtf8 str) of - Left err -> fail err - Right result -> pure result diff --git a/libs/brig-types/src/Brig/Types/User.hs b/libs/brig-types/src/Brig/Types/User.hs index f3cc87ba048..75dfe18f59a 100644 --- a/libs/brig-types/src/Brig/Types/User.hs +++ b/libs/brig-types/src/Brig/Types/User.hs @@ -19,7 +19,6 @@ module Brig.Types.User ( ManagedByUpdate (..), RichInfoUpdate (..), PasswordResetPair, - HavePendingInvitations (..), ) where diff --git a/libs/brig-types/test/unit/Test/Brig/Roundtrip.hs b/libs/brig-types/test/unit/Test/Brig/Roundtrip.hs index 13cfc3570e6..d7f91ce70c7 100644 --- a/libs/brig-types/test/unit/Test/Brig/Roundtrip.hs +++ b/libs/brig-types/test/unit/Test/Brig/Roundtrip.hs @@ -19,7 +19,6 @@ module Test.Brig.Roundtrip where import Data.Aeson (FromJSON, ToJSON, parseJSON, toJSON) import Data.Aeson.Types (parseEither) -import Data.ByteString.Conversion import Data.OpenApi (ToSchema, validatePrettyToJSON) import Imports import Test.Tasty (TestTree) @@ -56,14 +55,3 @@ testRoundTripWithSwagger = testProperty msg (trip .&&. scm) validatePrettyToJSON v ) $ isNothing (validatePrettyToJSON v) - -testRoundTripByteString :: - forall a. - (Arbitrary a, Typeable a, ToByteString a, FromByteString a, Eq a, Show a) => - TestTree -testRoundTripByteString = testProperty msg trip - where - msg = show (typeRep @a) - trip (v :: a) = - counterexample (show $ toByteString' v) $ - Just v === (fromByteString . toByteString') v diff --git a/libs/brig-types/test/unit/Test/Brig/Types/User.hs b/libs/brig-types/test/unit/Test/Brig/Types/User.hs index ee966465ad2..79a848fcef8 100644 --- a/libs/brig-types/test/unit/Test/Brig/Types/User.hs +++ b/libs/brig-types/test/unit/Test/Brig/Types/User.hs @@ -26,15 +26,12 @@ module Test.Brig.Types.User where import Brig.Types.Connection (UpdateConnectionsInternal (..)) -import Brig.Types.Intra (NewUserScimInvitation (..), UserAccount (..)) -import Brig.Types.Search (SearchVisibilityInbound (..)) +import Brig.Types.Intra import Brig.Types.User (ManagedByUpdate (..), RichInfoUpdate (..)) -import Data.Aeson import Imports import Test.Brig.Roundtrip (testRoundTrip, testRoundTripWithSwagger) import Test.QuickCheck (Arbitrary (arbitrary)) import Test.Tasty -import Test.Tasty.HUnit import Wire.API.Routes.Internal.Brig.EJPD (EJPDRequestBody (..), EJPDResponseBody (..)) import Wire.API.User.Auth.ReAuth @@ -49,23 +46,8 @@ roundtripTests = testRoundTrip @NewUserScimInvitation, testRoundTripWithSwagger @EJPDRequestBody, testRoundTripWithSwagger @EJPDResponseBody, - testRoundTrip @UpdateConnectionsInternal, - testRoundTrip @SearchVisibilityInbound, - testRoundTripWithSwagger @UserAccount, - testGroup "golden tests" $ - [testCaseUserAccount] + testRoundTrip @UpdateConnectionsInternal ] instance Arbitrary ReAuthUser where arbitrary = ReAuthUser <$> arbitrary <*> arbitrary <*> arbitrary - -testCaseUserAccount :: TestTree -testCaseUserAccount = testCase "UserAcccount" $ do - assertEqual "1" (Just json1) (encode <$> decode @UserAccount json1) - assertEqual "2" (Just json2) (encode <$> decode @UserAccount json2) - where - json1 :: LByteString - json1 = "{\"accent_id\":1,\"assets\":[],\"deleted\":true,\"email\":\"foo@example.com\",\"expires_at\":\"1864-05-09T17:20:22.192Z\",\"handle\":\"-ve\",\"id\":\"00000000-0000-0001-0000-000100000000\",\"locale\":\"lu\",\"managed_by\":\"wire\",\"name\":\"bla\",\"picture\":[],\"qualified_id\":{\"domain\":\"4-o60.j7-i\",\"id\":\"00000000-0000-0001-0000-000100000000\"},\"service\":{\"id\":\"00000000-0000-0001-0000-000000000001\",\"provider\":\"00000001-0000-0001-0000-000000000001\"},\"status\":\"suspended\",\"supported_protocols\":[\"proteus\"],\"team\":\"00000000-0000-0001-0000-000100000001\"}" - - json2 :: LByteString - json2 = "{\"accent_id\":0,\"assets\":[{\"key\":\"3-4-00000000-0000-0001-0000-000000000000\",\"size\":\"preview\",\"type\":\"image\"}],\"email\":\"@\",\"expires_at\":\"1864-05-10T22:45:44.823Z\",\"handle\":\"b8m\",\"id\":\"00000000-0000-0000-0000-000000000001\",\"locale\":\"tk-KZ\",\"managed_by\":\"wire\",\"name\":\"name2\",\"picture\":[],\"qualified_id\":{\"domain\":\"1-8wq0.b22k1.w5\",\"id\":\"00000000-0000-0000-0000-000000000001\"},\"service\":{\"id\":\"00000000-0000-0001-0000-000000000001\",\"provider\":\"00000001-0000-0001-0000-000100000000\"},\"status\":\"pending-invitation\",\"supported_protocols\":[\"proteus\"],\"team\":\"00000000-0000-0001-0000-000000000001\"}" diff --git a/libs/cassandra-util/cassandra-util.cabal b/libs/cassandra-util/cassandra-util.cabal index af2e0094209..927498c24f5 100644 --- a/libs/cassandra-util/cassandra-util.cabal +++ b/libs/cassandra-util/cassandra-util.cabal @@ -18,6 +18,7 @@ library Cassandra.Helpers Cassandra.MigrateSchema Cassandra.Options + Cassandra.QQ Cassandra.Schema Cassandra.Settings Cassandra.Util @@ -87,6 +88,7 @@ library , optparse-applicative >=0.10 , retry , split >=0.2 + , template-haskell , text >=0.11 , time >=1.4 , tinylog >=0.7 diff --git a/libs/cassandra-util/default.nix b/libs/cassandra-util/default.nix index c7b1451a36e..e02d098a9b7 100644 --- a/libs/cassandra-util/default.nix +++ b/libs/cassandra-util/default.nix @@ -19,6 +19,7 @@ , optparse-applicative , retry , split +, template-haskell , text , time , tinylog @@ -44,6 +45,7 @@ mkDerivation { optparse-applicative retry split + template-haskell text time tinylog diff --git a/libs/cassandra-util/src/Cassandra.hs b/libs/cassandra-util/src/Cassandra.hs index 6774abbeb56..74dcdfc45f4 100644 --- a/libs/cassandra-util/src/Cassandra.hs +++ b/libs/cassandra-util/src/Cassandra.hs @@ -91,3 +91,4 @@ import Cassandra.Exec as C x1, x5, ) +import Cassandra.QQ as C (sql) diff --git a/libs/cassandra-util/src/Cassandra/Exec.hs b/libs/cassandra-util/src/Cassandra/Exec.hs index 795083fe39d..8ef7d64337c 100644 --- a/libs/cassandra-util/src/Cassandra/Exec.hs +++ b/libs/cassandra-util/src/Cassandra/Exec.hs @@ -24,10 +24,10 @@ module Cassandra.Exec paramsP, x5, x1, - syncCassandra, paginateC, PageWithState (..), paginateWithState, + paginateWithStateC, paramsPagingState, pwsHasMore, module C, @@ -80,15 +80,6 @@ data CassandraError | Other !SomeException deriving (Show) -syncCassandra :: (MonadIO m, MonadCatch m) => m a -> m (Either CassandraError a) -syncCassandra m = - catches - (Right <$> m) - [ Handler $ \(e :: Error) -> pure . Left . Cassandra $ e, - Handler $ \(e :: IOException) -> pure . Left . Comm $ e, - Handler $ \(e :: SomeException) -> pure . Left . Other $ e - ] - -- | Stream results of a query. -- -- You can execute this conduit by doing @transPipe (runClient ...)@. @@ -125,6 +116,29 @@ paginateWithState q p = do pure $ PageWithState b (pagingState m) _ -> throwM $ UnexpectedResponse (hrHost r) (hrResponse r) +-- | Like 'paginateWithState' but returns a conduit instead of one page. +-- +-- This can be used with 'paginateWithState' like this: +-- @ +-- main :: IO () +-- main = do +-- runConduit $ +-- paginateWithStateC getUsers +-- .| mapC doSomethingWithAPageOfUsers +-- where +-- getUsers state = paginateWithState getUsersQuery (paramsPagingState Quorum () 10000 state) +-- @ +paginateWithStateC :: forall m a. (Monad m) => (Maybe Protocol.PagingState -> m (PageWithState a)) -> ConduitT () [a] m () +paginateWithStateC getPage = do + go =<< lift (getPage Nothing) + where + go :: PageWithState a -> ConduitT () [a] m () + go page = do + unless (null page.pwsResults) $ + yield (page.pwsResults) + when (pwsHasMore page) $ + go =<< lift (getPage page.pwsState) + paramsPagingState :: Consistency -> a -> Int32 -> Maybe Protocol.PagingState -> QueryParams a paramsPagingState c p n state = QueryParams c False p (Just n) state Nothing Nothing {-# INLINE paramsPagingState #-} diff --git a/libs/cassandra-util/src/Cassandra/Helpers.hs b/libs/cassandra-util/src/Cassandra/Helpers.hs index 8a260d530b5..4c2834f7ffc 100644 --- a/libs/cassandra-util/src/Cassandra/Helpers.hs +++ b/libs/cassandra-util/src/Cassandra/Helpers.hs @@ -1,4 +1,4 @@ -module Cassandra.Helpers where +module Cassandra.Helpers (toOptionFieldName) where import Data.Aeson.TH import Imports diff --git a/libs/cassandra-util/src/Cassandra/Options.hs b/libs/cassandra-util/src/Cassandra/Options.hs index f1f62056eee..e8aca6a7de5 100644 --- a/libs/cassandra-util/src/Cassandra/Options.hs +++ b/libs/cassandra-util/src/Cassandra/Options.hs @@ -5,34 +5,28 @@ module Cassandra.Options where -import Cassandra.Helpers -import Control.Lens import Data.Aeson.TH import Imports data Endpoint = Endpoint - { _host :: !Text, - _port :: !Word16 + { host :: !Text, + port :: !Word16 } deriving (Show, Generic) -deriveFromJSON toOptionFieldName ''Endpoint - -makeLenses ''Endpoint +deriveFromJSON defaultOptions ''Endpoint data CassandraOpts = CassandraOpts - { _endpoint :: !Endpoint, - _keyspace :: !Text, + { endpoint :: !Endpoint, + keyspace :: !Text, -- | If this option is unset, use all available nodes. -- If this option is set, use only cassandra nodes in the given datacentre -- -- This option is most likely only necessary during a cassandra DC migration -- FUTUREWORK: remove this option again, or support a datacentre migration feature - _filterNodesByDatacentre :: !(Maybe Text), - _tlsCa :: Maybe FilePath + filterNodesByDatacentre :: !(Maybe Text), + tlsCa :: Maybe FilePath } deriving (Show, Generic) -deriveFromJSON toOptionFieldName ''CassandraOpts - -makeLenses ''CassandraOpts +deriveFromJSON defaultOptions ''CassandraOpts diff --git a/libs/cassandra-util/src/Cassandra/QQ.hs b/libs/cassandra-util/src/Cassandra/QQ.hs new file mode 100644 index 00000000000..c15df3f3dca --- /dev/null +++ b/libs/cassandra-util/src/Cassandra/QQ.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE TemplateHaskellQuotes #-} + +module Cassandra.QQ (sql) where + +import Imports +import Language.Haskell.TH +import Language.Haskell.TH.Quote (QuasiQuoter (..)) + +-- | a simple quasi quoter to allow for tree-sitter syntax highlight injection. +-- This uses the name sql because that is known to tree-sitter, unlike cql +sql :: QuasiQuoter +sql = + QuasiQuoter + { quotePat = error "Cassandra.QQ: sql quasiquoter cannot be used as pattern", + quoteType = error "Cassandra.QQ: sql quasiquoter cannot be used as type", + quoteDec = error "Cassandra.QQ: sql quasiquoter cannot be used as declaration", + quoteExp = appE [|fromString|] . stringE + } diff --git a/libs/cassandra-util/src/Cassandra/Util.hs b/libs/cassandra-util/src/Cassandra/Util.hs index f8b793f77db..d6968ead939 100644 --- a/libs/cassandra-util/src/Cassandra/Util.hs +++ b/libs/cassandra-util/src/Cassandra/Util.hs @@ -28,7 +28,6 @@ import Cassandra.CQL import Cassandra.Options import Cassandra.Schema import Cassandra.Settings (dcFilterPolicyIfConfigured, initialContactsDisco, initialContactsPlain, mkLogger) -import Control.Lens import Data.Aeson import Data.Fixed import Data.List.NonEmpty qualified as NE @@ -46,12 +45,12 @@ defInitCassandra :: CassandraOpts -> Log.Logger -> IO ClientState defInitCassandra opts logger = do let basicCasSettings = setLogger (CT.mkLogger logger) - . setPortNumber (fromIntegral (opts ^. endpoint . port)) - . setContacts (unpack (opts ^. endpoint . host)) [] - . setKeyspace (Keyspace (opts ^. keyspace)) + . setPortNumber (fromIntegral opts.endpoint.port) + . setContacts (unpack opts.endpoint.host) [] + . setKeyspace (Keyspace opts.keyspace) . setProtocolVersion V4 $ defSettings - initCassandra basicCasSettings (opts ^. tlsCa) logger + initCassandra basicCasSettings opts.tlsCa logger -- | Create Cassandra `ClientState` ("connection") for a service initCassandraForService :: @@ -64,22 +63,22 @@ initCassandraForService :: initCassandraForService opts serviceName discoUrl mbSchemaVersion logger = do c <- maybe - (initialContactsPlain (opts ^. endpoint . host)) + (initialContactsPlain opts.endpoint.host) (initialContactsDisco ("cassandra_" ++ serviceName) . unpack) discoUrl let basicCasSettings = setLogger (mkLogger (Log.clone (Just (pack ("cassandra." ++ serviceName))) logger)) . setContacts (NE.head c) (NE.tail c) - . setPortNumber (fromIntegral (opts ^. endpoint . port)) - . setKeyspace (Keyspace (opts ^. keyspace)) + . setPortNumber (fromIntegral opts.endpoint.port) + . setKeyspace (Keyspace opts.keyspace) . setMaxConnections 4 . setPoolStripes 4 . setSendTimeout 3 . setResponseTimeout 10 . setProtocolVersion V4 - . setPolicy (dcFilterPolicyIfConfigured logger (opts ^. filterNodesByDatacentre)) + . setPolicy (dcFilterPolicyIfConfigured logger opts.filterNodesByDatacentre) $ defSettings - p <- initCassandra basicCasSettings (opts ^. tlsCa) logger + p <- initCassandra basicCasSettings opts.tlsCa logger maybe (pure ()) (\v -> runClient p $ (versionCheck v)) mbSchemaVersion pure p @@ -109,6 +108,7 @@ initCassandra settings Nothing logger = do -- | Read cassandra's writetimes https://docs.datastax.com/en/dse/5.1/cql/cql/cql_using/useWritetime.html -- as UTCTime values without any loss of precision newtype Writetime a = Writetime {writetimeToUTC :: UTCTime} + deriving (Eq, Show, Functor) instance Cql (Writetime a) where ctype = Tagged BigIntColumn diff --git a/libs/extended/default.nix b/libs/extended/default.nix index b47de8057a2..61f4643c17e 100644 --- a/libs/extended/default.nix +++ b/libs/extended/default.nix @@ -14,7 +14,6 @@ , data-default , errors , exceptions -, extra , gitignoreSource , hspec , hspec-discover @@ -25,7 +24,6 @@ , lib , metrics-wai , monad-control -, optparse-applicative , resourcet , retry , servant @@ -59,14 +57,12 @@ mkDerivation { data-default errors exceptions - extra http-client http-client-tls http-types imports metrics-wai monad-control - optparse-applicative resourcet retry servant diff --git a/libs/extended/extended.cabal b/libs/extended/extended.cabal index 03d180a004a..65ad7864014 100644 --- a/libs/extended/extended.cabal +++ b/libs/extended/extended.cabal @@ -22,7 +22,6 @@ library Data.Time.Clock.DiffTime Network.AMQP.Extended Network.RabbitMqAdmin - Options.Applicative.Extended Servant.API.Extended Servant.API.Extended.Endpath Servant.API.Extended.RawM @@ -90,14 +89,12 @@ library , data-default , errors , exceptions - , extra , http-client , http-client-tls , http-types , imports , metrics-wai , monad-control - , optparse-applicative , resourcet , retry , servant diff --git a/libs/extended/src/Data/Time/Clock/DiffTime.hs b/libs/extended/src/Data/Time/Clock/DiffTime.hs index 5541fd43d38..2480cf4e59d 100644 --- a/libs/extended/src/Data/Time/Clock/DiffTime.hs +++ b/libs/extended/src/Data/Time/Clock/DiffTime.hs @@ -1,13 +1,7 @@ module Data.Time.Clock.DiffTime ( DiffTime, - weeksToDiffTime, - daysToDiffTime, - hoursToDiffTime, - minutesToDiffTime, secondsToDiffTime, millisecondsToDiffTime, - microsecondsToDiffTime, - nanosecondsToDiffTime, picosecondsToDiffTime, diffTimeToFullMicroseconds, diffTimeToPicoseconds, @@ -17,27 +11,14 @@ where import Data.Time import Imports -weeksToDiffTime, - daysToDiffTime, - hoursToDiffTime, - minutesToDiffTime, - millisecondsToDiffTime, - microsecondsToDiffTime, - nanosecondsToDiffTime :: - Integer -> DiffTime -weeksToDiffTime = daysToDiffTime . (7 *) -daysToDiffTime = hoursToDiffTime . (24 *) -hoursToDiffTime = minutesToDiffTime . (60 *) -minutesToDiffTime = secondsToDiffTime . (60 *) +-- FUTUREWORK: we really should be doing all this with https://hackage.haskell.org/package/units... +millisecondsToDiffTime :: Integer -> DiffTime millisecondsToDiffTime = picosecondsToDiffTime . (e9 *) -microsecondsToDiffTime = picosecondsToDiffTime . (e6 *) -nanosecondsToDiffTime = picosecondsToDiffTime . (e3 *) -- | Rounds down. Useful for 'threadDelay', 'timeout', etc. diffTimeToFullMicroseconds :: DiffTime -> Int diffTimeToFullMicroseconds = fromInteger . (`div` e6) . diffTimeToPicoseconds -e3, e6, e9 :: Integer -e3 = 1_000 +e6, e9 :: Integer e6 = 1_000_000 e9 = 1_000_000_000 diff --git a/libs/extended/src/Network/AMQP/Extended.hs b/libs/extended/src/Network/AMQP/Extended.hs index b3131fce2af..955e54c0a33 100644 --- a/libs/extended/src/Network/AMQP/Extended.hs +++ b/libs/extended/src/Network/AMQP/Extended.hs @@ -3,7 +3,7 @@ module Network.AMQP.Extended ( RabbitMqHooks (..), RabbitMqAdminOpts (..), - RabbitMqOpts (..), + AmqpEndpoint (..), openConnectionWithRetries, mkRabbitMqAdminClientEnv, mkRabbitMqChannelMVar, @@ -103,9 +103,9 @@ mkRabbitMqAdminClientEnv opts = do (either throwM pure <=< flip runClientM clientEnv) (toServant $ adminClient basicAuthData) --- | When admin opts are needed use `RabbitMqOpts Identity`, otherwise use --- `RabbitMqOpts NoAdmin`. -data RabbitMqOpts = RabbitMqOpts +-- | When admin opts are needed use `AmqpEndpoint Identity`, otherwise use +-- `AmqpEndpoint NoAdmin`. +data AmqpEndpoint = AmqpEndpoint { host :: !String, port :: !Int, vHost :: !Text, @@ -113,19 +113,19 @@ data RabbitMqOpts = RabbitMqOpts } deriving (Show) -instance FromJSON RabbitMqOpts where - parseJSON = withObject "RabbitMqAdminOpts" $ \v -> - RabbitMqOpts +instance FromJSON AmqpEndpoint where + parseJSON = withObject "AmqpEndpoint" $ \v -> + AmqpEndpoint <$> v .: "host" <*> v .: "port" <*> v .: "vHost" <*> parseTlsJson v -demoteOpts :: RabbitMqAdminOpts -> RabbitMqOpts -demoteOpts RabbitMqAdminOpts {..} = RabbitMqOpts {..} +demoteOpts :: RabbitMqAdminOpts -> AmqpEndpoint +demoteOpts RabbitMqAdminOpts {..} = AmqpEndpoint {..} -- | Useful if the application only pushes into some queues. -mkRabbitMqChannelMVar :: Logger -> RabbitMqOpts -> IO (MVar Q.Channel) +mkRabbitMqChannelMVar :: Logger -> AmqpEndpoint -> IO (MVar Q.Channel) mkRabbitMqChannelMVar l opts = do chanMVar <- newEmptyMVar connThread <- @@ -152,10 +152,10 @@ openConnectionWithRetries :: forall m. (MonadIO m, MonadMask m, MonadBaseControl IO m) => Logger -> - RabbitMqOpts -> + AmqpEndpoint -> RabbitMqHooks m -> m () -openConnectionWithRetries l RabbitMqOpts {..} hooks = do +openConnectionWithRetries l AmqpEndpoint {..} hooks = do (username, password) <- liftIO $ readCredsFromEnv connectWithRetries username password where diff --git a/libs/galley-types/default.nix b/libs/galley-types/default.nix index 5a6070c01a4..05f50010a65 100644 --- a/libs/galley-types/default.nix +++ b/libs/galley-types/default.nix @@ -9,16 +9,15 @@ , bytestring-conversion , containers , crypton +, data-default , errors , gitignoreSource , imports , lens , lib , memory -, QuickCheck , schema-profunctor -, tasty -, tasty-quickcheck +, sop-core , text , types-common , utf8-string @@ -36,26 +35,18 @@ mkDerivation { bytestring-conversion containers crypton + data-default errors imports lens memory - QuickCheck schema-profunctor + sop-core text types-common utf8-string uuid wire-api ]; - testHaskellDepends = [ - aeson - base - imports - QuickCheck - tasty - tasty-quickcheck - wire-api - ]; license = lib.licenses.agpl3Only; } diff --git a/libs/galley-types/galley-types.cabal b/libs/galley-types/galley-types.cabal index 04201486276..a3c6cea0cfe 100644 --- a/libs/galley-types/galley-types.cabal +++ b/libs/galley-types/galley-types.cabal @@ -75,12 +75,13 @@ library , bytestring-conversion , containers >=0.5 , crypton + , data-default , errors , imports , lens >=4.12 , memory - , QuickCheck , schema-profunctor + , sop-core , text >=0.11 , types-common >=0.16 , utf8-string @@ -88,74 +89,3 @@ library , wire-api default-language: GHC2021 - -test-suite galley-types-tests - type: exitcode-stdio-1.0 - main-is: Main.hs - - -- cabal-fmt: expand test - other-modules: - Paths_galley_types - Test.Galley.Roundtrip - Test.Galley.Types - - hs-source-dirs: test/unit - default-extensions: - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - NoImplicitPrelude - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - - ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -with-rtsopts=-N -Wredundant-constraints - -Wunused-packages - - build-depends: - aeson - , base - , galley-types - , imports - , QuickCheck - , tasty - , tasty-quickcheck - , wire-api - - default-language: GHC2021 diff --git a/libs/galley-types/src/Galley/Types.hs b/libs/galley-types/src/Galley/Types.hs index b08103a22cd..1674d3bae0f 100644 --- a/libs/galley-types/src/Galley/Types.hs +++ b/libs/galley-types/src/Galley/Types.hs @@ -18,16 +18,13 @@ -- with this program. If not, see . module Galley.Types - ( foldrOtrRecipients, - Accept (..), + ( Accept (..), ) where import Data.Aeson -import Data.Id (ClientId, UserId) -import Data.Map.Strict qualified as Map +import Data.Id (UserId) import Imports -import Wire.API.Message -------------------------------------------------------------------------------- -- Accept @@ -47,14 +44,3 @@ instance ToJSON Accept where instance FromJSON Accept where parseJSON = withObject "accept" $ \o -> Accept <$> o .: "user" - --------------------------------------------------------------------------------- --- utility functions - -foldrOtrRecipients :: (UserId -> ClientId -> Text -> a -> a) -> a -> OtrRecipients -> a -foldrOtrRecipients f a = - Map.foldrWithKey go a - . userClientMap - . otrRecipientsMap - where - go u cs acc = Map.foldrWithKey (f u) acc cs diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index 75d70c0fb14..28be18a4b0a 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -4,6 +4,7 @@ {-# LANGUAGE StandaloneKindSignatures #-} {-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} -- This file is part of the Wire Server implementation. -- @@ -25,32 +26,12 @@ module Galley.Types.Teams ( TeamCreationTime (..), tcTime, - FeatureFlags (..), - flagSSO, - flagLegalHold, - flagTeamSearchVisibility, - flagFileSharing, - flagAppLockDefaults, - flagClassifiedDomains, - flagConferenceCalling, - flagSelfDeletingMessages, - flagConversationGuestLinks, - flagsTeamFeatureValidateSAMLEmailsStatus, - flagTeamFeatureSndFactorPasswordChallengeStatus, - flagTeamFeatureSearchVisibilityInbound, - flagOutlookCalIntegration, - flagMLS, - flagMlsE2EId, - flagMlsMigration, - flagEnforceFileDownloadLocation, - flagLimitedEventFanout, - Defaults (..), - ImplicitLockStatus (..), - unImplicitLockStatus, - unDefaults, - FeatureSSO (..), - FeatureLegalHold (..), - FeatureTeamSearchVisibilityAvailability (..), + GetFeatureDefaults (..), + FeatureDefaults (..), + FeatureFlags, + DefaultsInitial (..), + initialFeature, + featureDefaults, notTeamMember, findTeamMember, isTeamMember, @@ -61,14 +42,16 @@ where import Control.Lens (makeLenses, view) import Data.Aeson +import Data.Aeson.Key qualified as Key import Data.Aeson.Types qualified as A import Data.ByteString (toStrict) import Data.ByteString.UTF8 qualified as UTF8 +import Data.Default import Data.Id (UserId) -import Data.Schema qualified as Schema +import Data.SOP +import Data.Schema qualified as S import Data.Set qualified as Set import Imports -import Test.QuickCheck (Arbitrary) import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.API.Team.Permission @@ -78,158 +61,302 @@ newtype TeamCreationTime = TeamCreationTime { _tcTime :: Int64 } -data FeatureFlags = FeatureFlags - { _flagSSO :: !FeatureSSO, - _flagLegalHold :: !FeatureLegalHold, - _flagTeamSearchVisibility :: !FeatureTeamSearchVisibilityAvailability, - _flagAppLockDefaults :: !(Defaults (ImplicitLockStatus AppLockConfig)), - _flagClassifiedDomains :: !(ImplicitLockStatus ClassifiedDomainsConfig), - _flagFileSharing :: !(Defaults (WithStatus FileSharingConfig)), - _flagConferenceCalling :: !(Defaults (ImplicitLockStatus ConferenceCallingConfig)), - _flagSelfDeletingMessages :: !(Defaults (WithStatus SelfDeletingMessagesConfig)), - _flagConversationGuestLinks :: !(Defaults (WithStatus GuestLinksConfig)), - _flagsTeamFeatureValidateSAMLEmailsStatus :: !(Defaults (ImplicitLockStatus ValidateSAMLEmailsConfig)), - _flagTeamFeatureSndFactorPasswordChallengeStatus :: !(Defaults (WithStatus SndFactorPasswordChallengeConfig)), - _flagTeamFeatureSearchVisibilityInbound :: !(Defaults (ImplicitLockStatus SearchVisibilityInboundConfig)), - _flagMLS :: !(Defaults (WithStatus MLSConfig)), - _flagOutlookCalIntegration :: !(Defaults (WithStatus OutlookCalIntegrationConfig)), - _flagMlsE2EId :: !(Defaults (WithStatus MlsE2EIdConfig)), - _flagMlsMigration :: !(Defaults (WithStatus MlsMigrationConfig)), - _flagEnforceFileDownloadLocation :: !(Defaults (WithStatus EnforceFileDownloadLocationConfig)), - _flagLimitedEventFanout :: !(Defaults (ImplicitLockStatus LimitedEventFanoutConfig)) - } - deriving (Eq, Show, Generic) +-- | Used to extract the feature config type out of 'FeatureDefaults' or +-- related types. +type family ConfigOf a -newtype Defaults a = Defaults {_unDefaults :: a} - deriving (Eq, Ord, Show, Enum, Bounded, Generic, Functor) - deriving newtype (Arbitrary) +type instance ConfigOf (FeatureDefaults cfg) = cfg -instance (FromJSON a) => FromJSON (Defaults a) where - parseJSON = withObject "default object" $ \ob -> - Defaults <$> (ob .: "defaults") +-- | Convert a feature default value to an actual 'LockableFeature'. +class GetFeatureDefaults a where + featureDefaults1 :: a -> LockableFeature (ConfigOf a) -instance (ToJSON a) => ToJSON (Defaults a) where - toJSON (Defaults x) = - object ["defaults" .= toJSON x] +type instance ConfigOf (Feature cfg) = cfg -data FeatureSSO - = FeatureSSOEnabledByDefault - | FeatureSSODisabledByDefault - deriving (Eq, Ord, Show, Enum, Bounded, Generic) +instance (IsFeatureConfig cfg) => GetFeatureDefaults (Feature cfg) where + featureDefaults1 = withLockStatus (def @(LockableFeature cfg)).lockStatus + +-- | Some features do not have a configured default value, so this takes it +-- wholly from the 'Default' instance. +newtype FixedDefaults cfg = FixedDefaults (FeatureDefaults cfg) + +type instance ConfigOf (FixedDefaults cfg) = cfg + +instance (IsFeatureConfig cfg) => GetFeatureDefaults (FixedDefaults cfg) where + featureDefaults1 _ = def -data FeatureLegalHold +type instance ConfigOf (LockableFeature cfg) = cfg + +instance GetFeatureDefaults (LockableFeature cfg) where + featureDefaults1 = id + +data family FeatureDefaults cfg + +data instance FeatureDefaults LegalholdConfig = FeatureLegalHoldDisabledPermanently | FeatureLegalHoldDisabledByDefault | FeatureLegalHoldWhitelistTeamsAndImplicitConsent - deriving (Eq, Ord, Show, Enum, Bounded, Generic) + deriving stock (Eq, Ord, Show) + deriving (ParseFeatureDefaults) via RequiredField LegalholdConfig + deriving (GetFeatureDefaults) via FixedDefaults LegalholdConfig --- | Default value for all teams that have not enabled or disabled this feature explicitly. -data FeatureTeamSearchVisibilityAvailability - = FeatureTeamSearchVisibilityAvailableByDefault - | FeatureTeamSearchVisibilityUnavailableByDefault - deriving (Eq, Ord, Show, Enum, Bounded, Generic) - --- NOTE: This is used only in the config and thus YAML... camelcase -instance FromJSON FeatureFlags where - parseJSON = withObject "FeatureFlags" $ \obj -> - FeatureFlags - <$> obj .: "sso" - <*> obj .: "legalhold" - <*> obj .: "teamSearchVisibility" - <*> withImplicitLockStatusOrDefault obj "appLock" - <*> (fromMaybe (ImplicitLockStatus (defFeatureStatus @ClassifiedDomainsConfig)) <$> (obj .:? "classifiedDomains")) - <*> (fromMaybe (Defaults (defFeatureStatus @FileSharingConfig)) <$> (obj .:? "fileSharing")) - <*> withImplicitLockStatusOrDefault obj "conferenceCalling" - <*> (fromMaybe (Defaults (defFeatureStatus @SelfDeletingMessagesConfig)) <$> (obj .:? "selfDeletingMessages")) - <*> (fromMaybe (Defaults (defFeatureStatus @GuestLinksConfig)) <$> (obj .:? "conversationGuestLinks")) - <*> withImplicitLockStatusOrDefault obj "validateSAMLEmails" - <*> (fromMaybe (Defaults (defFeatureStatus @SndFactorPasswordChallengeConfig)) <$> (obj .:? "sndFactorPasswordChallenge")) - <*> withImplicitLockStatusOrDefault obj "searchVisibilityInbound" - <*> (fromMaybe (Defaults (defFeatureStatus @MLSConfig)) <$> (obj .:? "mls")) - <*> (fromMaybe (Defaults (defFeatureStatus @OutlookCalIntegrationConfig)) <$> (obj .:? "outlookCalIntegration")) - <*> (fromMaybe (Defaults (defFeatureStatus @MlsE2EIdConfig)) <$> (obj .:? "mlsE2EId")) - <*> (fromMaybe (Defaults (defFeatureStatus @MlsMigrationConfig)) <$> (obj .:? "mlsMigration")) - <*> (fromMaybe (Defaults (defFeatureStatus @EnforceFileDownloadLocationConfig)) <$> (obj .:? "enforceFileDownloadLocation")) - <*> withImplicitLockStatusOrDefault obj "limitedEventFanout" - where - withImplicitLockStatusOrDefault :: forall cfg. (IsFeatureConfig cfg, Schema.ToSchema cfg) => Object -> Key -> A.Parser (Defaults (ImplicitLockStatus cfg)) - withImplicitLockStatusOrDefault obj fieldName = fromMaybe (Defaults (ImplicitLockStatus (defFeatureStatus @cfg))) <$> obj .:? fieldName - -instance ToJSON FeatureFlags where - toJSON - ( FeatureFlags - sso - legalhold - searchVisibility - appLock - classifiedDomains - fileSharing - conferenceCalling - selfDeletingMessages - guestLinks - validateSAMLEmails - sndFactorPasswordChallenge - searchVisibilityInbound - mls - outlookCalIntegration - mlsE2EId - mlsMigration - enforceFileDownloadLocation - teamMemberDeletedLimitedEventFanout - ) = - object - [ "sso" .= sso, - "legalhold" .= legalhold, - "teamSearchVisibility" .= searchVisibility, - "appLock" .= appLock, - "classifiedDomains" .= classifiedDomains, - "fileSharing" .= fileSharing, - "conferenceCalling" .= conferenceCalling, - "selfDeletingMessages" .= selfDeletingMessages, - "conversationGuestLinks" .= guestLinks, - "validateSAMLEmails" .= validateSAMLEmails, - "sndFactorPasswordChallenge" .= sndFactorPasswordChallenge, - "searchVisibilityInbound" .= searchVisibilityInbound, - "mls" .= mls, - "outlookCalIntegration" .= outlookCalIntegration, - "mlsE2EId" .= mlsE2EId, - "mlsMigration" .= mlsMigration, - "enforceFileDownloadLocation" .= enforceFileDownloadLocation, - "limitedEventFanout" .= teamMemberDeletedLimitedEventFanout - ] - -instance FromJSON FeatureSSO where +instance FromJSON (FeatureDefaults LegalholdConfig) where + parseJSON (String "disabled-permanently") = pure $ FeatureLegalHoldDisabledPermanently + parseJSON (String "disabled-by-default") = pure $ FeatureLegalHoldDisabledByDefault + parseJSON (String "whitelist-teams-and-implicit-consent") = pure FeatureLegalHoldWhitelistTeamsAndImplicitConsent + parseJSON bad = fail $ "FeatureLegalHold: " <> (UTF8.toString . toStrict . encode $ bad) + +data instance FeatureDefaults SSOConfig + = FeatureSSOEnabledByDefault + | FeatureSSODisabledByDefault + deriving stock (Eq, Ord, Show) + deriving (ParseFeatureDefaults) via RequiredField SSOConfig + +instance FromJSON (FeatureDefaults SSOConfig) where parseJSON (String "enabled-by-default") = pure FeatureSSOEnabledByDefault parseJSON (String "disabled-by-default") = pure FeatureSSODisabledByDefault parseJSON bad = fail $ "FeatureSSO: " <> (UTF8.toString . toStrict . encode $ bad) -instance ToJSON FeatureSSO where - toJSON FeatureSSOEnabledByDefault = String "enabled-by-default" - toJSON FeatureSSODisabledByDefault = String "disabled-by-default" +instance GetFeatureDefaults (FeatureDefaults SSOConfig) where + featureDefaults1 flag = + def + { status = case flag of + FeatureSSOEnabledByDefault -> FeatureStatusEnabled + FeatureSSODisabledByDefault -> FeatureStatusDisabled + } -instance FromJSON FeatureLegalHold where - parseJSON (String "disabled-permanently") = pure $ FeatureLegalHoldDisabledPermanently - parseJSON (String "disabled-by-default") = pure $ FeatureLegalHoldDisabledByDefault - parseJSON (String "whitelist-teams-and-implicit-consent") = pure FeatureLegalHoldWhitelistTeamsAndImplicitConsent - parseJSON bad = fail $ "FeatureLegalHold: " <> (UTF8.toString . toStrict . encode $ bad) +-- | Default value for all teams that have not enabled or disabled this feature explicitly. +data instance FeatureDefaults SearchVisibilityAvailableConfig + = FeatureTeamSearchVisibilityAvailableByDefault + | FeatureTeamSearchVisibilityUnavailableByDefault + deriving stock (Eq, Ord, Show) -instance ToJSON FeatureLegalHold where - toJSON FeatureLegalHoldDisabledPermanently = String "disabled-permanently" - toJSON FeatureLegalHoldDisabledByDefault = String "disabled-by-default" - toJSON FeatureLegalHoldWhitelistTeamsAndImplicitConsent = String "whitelist-teams-and-implicit-consent" +instance ParseFeatureDefaults (FeatureDefaults SearchVisibilityAvailableConfig) where + parseFeatureDefaults obj = obj .: "teamSearchVisibility" -instance FromJSON FeatureTeamSearchVisibilityAvailability where +instance FromJSON (FeatureDefaults SearchVisibilityAvailableConfig) where parseJSON (String "enabled-by-default") = pure FeatureTeamSearchVisibilityAvailableByDefault parseJSON (String "disabled-by-default") = pure FeatureTeamSearchVisibilityUnavailableByDefault parseJSON bad = fail $ "FeatureSearchVisibility: " <> (UTF8.toString . toStrict . encode $ bad) -instance ToJSON FeatureTeamSearchVisibilityAvailability where - toJSON FeatureTeamSearchVisibilityAvailableByDefault = String "enabled-by-default" - toJSON FeatureTeamSearchVisibilityUnavailableByDefault = String "disabled-by-default" +instance GetFeatureDefaults (FeatureDefaults SearchVisibilityAvailableConfig) where + featureDefaults1 flag = + def + { status = case flag of + FeatureTeamSearchVisibilityAvailableByDefault -> FeatureStatusEnabled + FeatureTeamSearchVisibilityUnavailableByDefault -> FeatureStatusDisabled + } + +newtype instance FeatureDefaults SearchVisibilityInboundConfig + = SearchVisibilityInboundDefaults (Feature SearchVisibilityInboundConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (Feature SearchVisibilityInboundConfig) + deriving (ParseFeatureDefaults) via OptionalField SearchVisibilityInboundConfig + +newtype instance FeatureDefaults ValidateSAMLEmailsConfig + = ValidateSAMLEmailsDefaults (Feature ValidateSAMLEmailsConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (Feature ValidateSAMLEmailsConfig) + deriving (ParseFeatureDefaults) via OptionalField ValidateSAMLEmailsConfig + +data instance FeatureDefaults DigitalSignaturesConfig = DigitalSignaturesDefaults + deriving stock (Eq, Show) + deriving (GetFeatureDefaults) via FixedDefaults DigitalSignaturesConfig + +instance ParseFeatureDefaults (FeatureDefaults DigitalSignaturesConfig) where + parseFeatureDefaults _ = pure DigitalSignaturesDefaults + +newtype instance FeatureDefaults AppLockConfig + = AppLockDefaults (Feature AppLockConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (Feature AppLockConfig) + deriving (ParseFeatureDefaults) via OptionalField AppLockConfig + +newtype instance FeatureDefaults FileSharingConfig + = FileSharingDefaults (LockableFeature FileSharingConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature FileSharingConfig) + deriving (ParseFeatureDefaults) via OptionalField FileSharingConfig + +newtype instance FeatureDefaults ClassifiedDomainsConfig + = ClassifiedDomainsDefaults (Feature ClassifiedDomainsConfig) + deriving stock (Eq, Show) + deriving newtype (Default, FromJSON) + deriving (ParseFeatureDefaults) via OptionalField ClassifiedDomainsConfig + deriving (GetFeatureDefaults) via Feature ClassifiedDomainsConfig + +newtype instance FeatureDefaults ConferenceCallingConfig + = ConferenceCallingDefaults (LockableFeature ConferenceCallingConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature ConferenceCallingConfig) + deriving (ParseFeatureDefaults) via OptionalField ConferenceCallingConfig + +newtype instance FeatureDefaults SelfDeletingMessagesConfig + = SelfDeletingMessagesDefaults (LockableFeature SelfDeletingMessagesConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature SelfDeletingMessagesConfig) + deriving (ParseFeatureDefaults) via OptionalField SelfDeletingMessagesConfig + +newtype instance FeatureDefaults GuestLinksConfig + = GuestLinksDefaults (LockableFeature GuestLinksConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature GuestLinksConfig) + deriving (ParseFeatureDefaults) via OptionalField GuestLinksConfig + +newtype instance FeatureDefaults SndFactorPasswordChallengeConfig + = SndFactorPasswordChallengeDefaults (LockableFeature SndFactorPasswordChallengeConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature SndFactorPasswordChallengeConfig) + deriving (ParseFeatureDefaults) via OptionalField SndFactorPasswordChallengeConfig + +newtype instance FeatureDefaults MLSConfig = MLSDefaults (DefaultsInitial MLSConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via DefaultsInitial MLSConfig + deriving (ParseFeatureDefaults) via OptionalField MLSConfig + +data instance FeatureDefaults ExposeInvitationURLsToTeamAdminConfig + = ExposeInvitationURLsToTeamAdminDefaults + deriving stock (Eq, Show) + deriving (GetFeatureDefaults) via FixedDefaults ExposeInvitationURLsToTeamAdminConfig + +instance ParseFeatureDefaults (FeatureDefaults ExposeInvitationURLsToTeamAdminConfig) where + parseFeatureDefaults _ = pure ExposeInvitationURLsToTeamAdminDefaults + +newtype instance FeatureDefaults OutlookCalIntegrationConfig + = OutlookCalIntegrationDefaults (LockableFeature OutlookCalIntegrationConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature OutlookCalIntegrationConfig) + deriving (ParseFeatureDefaults) via OptionalField OutlookCalIntegrationConfig + +newtype instance FeatureDefaults MlsE2EIdConfig + = MlsE2EIdDefaults (LockableFeature MlsE2EIdConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature MlsE2EIdConfig) + deriving (ParseFeatureDefaults) via OptionalField MlsE2EIdConfig + +newtype instance FeatureDefaults MlsMigrationConfig + = MlsMigrationDefaults (LockableFeature MlsMigrationConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature MlsMigrationConfig) + deriving (ParseFeatureDefaults) via OptionalField MlsMigrationConfig + +newtype instance FeatureDefaults EnforceFileDownloadLocationConfig + = EnforceFileDownloadLocationDefaults (LockableFeature EnforceFileDownloadLocationConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature EnforceFileDownloadLocationConfig) + deriving (ParseFeatureDefaults) via OptionalField EnforceFileDownloadLocationConfig + +newtype instance FeatureDefaults LimitedEventFanoutConfig + = LimitedEventFanoutDefaults (Feature LimitedEventFanoutConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (Feature LimitedEventFanoutConfig) + deriving (ParseFeatureDefaults) via OptionalField LimitedEventFanoutConfig + +featureKey :: forall cfg. (IsFeatureConfig cfg) => Key.Key +featureKey = Key.fromText $ featureName @cfg + +class ParseFeatureDefaults a where + parseFeatureDefaults :: A.Object -> A.Parser a + +newtype RequiredField cfg = RequiredField (FeatureDefaults cfg) + +instance + (IsFeatureConfig cfg, FromJSON (FeatureDefaults cfg)) => + ParseFeatureDefaults (RequiredField cfg) + where + parseFeatureDefaults obj = RequiredField <$> obj .: featureKey @cfg + +newtype OptionalField cfg = OptionalField (FeatureDefaults cfg) + +instance + ( IsFeatureConfig cfg, + Default (FeatureDefaults cfg), + FromJSON (FeatureDefaults cfg) + ) => + ParseFeatureDefaults (OptionalField cfg) + where + parseFeatureDefaults obj = OptionalField <$> obj .:? featureKey @cfg .!= def + +type FeatureFlags = AllFeatures FeatureDefaults + +featureDefaults :: + forall cfg. + ( GetFeatureDefaults (FeatureDefaults cfg), + NpProject cfg Features + ) => + FeatureFlags -> + LockableFeature cfg +featureDefaults = featureDefaults1 . npProject + +class FeatureFlagsFromObject f cfgs where + featureFlagsFromObject :: A.Object -> A.Parser (NP f cfgs) + +instance FeatureFlagsFromObject f '[] where + featureFlagsFromObject _ = pure Nil + +instance + ( ParseFeatureDefaults (f cfg), + FeatureFlagsFromObject f cfgs + ) => + FeatureFlagsFromObject f (cfg : cfgs) + where + featureFlagsFromObject obj = + (:*) + <$> parseFeatureDefaults obj + <*> featureFlagsFromObject obj + +instance + (FeatureFlagsFromObject FeatureDefaults Features) => + FromJSON FeatureFlags + where + parseJSON = withObject "FeatureFlags" featureFlagsFromObject + +newtype Defaults a = Defaults {_unDefaults :: a} + +instance (FromJSON a) => FromJSON (Defaults a) where + parseJSON = withObject "default object" $ \ob -> + Defaults <$> (ob .: "defaults") + +data DefaultsInitial cfg = DefaultsInitial + { defFeature :: LockableFeature cfg, + initial :: cfg + } + deriving (Eq, Show) + +instance (IsFeatureConfig cfg) => Default (DefaultsInitial cfg) where + def = DefaultsInitial def def + +type instance ConfigOf (DefaultsInitial cfg) = cfg + +instance GetFeatureDefaults (DefaultsInitial cfg) where + featureDefaults1 = defFeature + +instance (IsFeatureConfig cfg) => FromJSON (DefaultsInitial cfg) where + parseJSON = withObject "default with initial" $ \ob -> do + feat <- ob .:? "defaults" .!= def + mc <- + fromMaybe feat.config + <$> A.explicitParseFieldMaybe S.schemaParseJSON ob "initialConfig" + pure $ DefaultsInitial feat mc + +initialFeature :: DefaultsInitial cfg -> LockableFeature cfg +initialFeature d = d.defFeature {config = d.initial} makeLenses ''TeamCreationTime -makeLenses ''FeatureFlags -makeLenses ''Defaults notTeamMember :: [UserId] -> [TeamMember] -> [UserId] notTeamMember uids tmms = @@ -245,7 +372,6 @@ findTeamMember u = find ((u ==) . view userId) isTeamOwner :: TeamMemberOptPerms -> Bool isTeamOwner tm = optionalPermissions tm == Just fullPermissions --- | Use this to construct the condition expected by 'teamMemberJson', 'teamMemberListJson' canSeePermsOf :: TeamMember -> TeamMember -> Bool canSeePermsOf seeer seeee = seeer `hasPermission` GetMemberPermissions || seeer == seeee diff --git a/libs/galley-types/test/unit/Test/Galley/Roundtrip.hs b/libs/galley-types/test/unit/Test/Galley/Roundtrip.hs deleted file mode 100644 index b9d1fcc8568..00000000000 --- a/libs/galley-types/test/unit/Test/Galley/Roundtrip.hs +++ /dev/null @@ -1,36 +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 Test.Galley.Roundtrip where - -import Data.Aeson (FromJSON, ToJSON, parseJSON, toJSON) -import Data.Aeson.Types (parseEither) -import Imports -import Test.Tasty (TestTree) -import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty, (===)) -import Type.Reflection (typeRep) - -testRoundTrip :: - forall a. - (Arbitrary a, Typeable a, ToJSON a, FromJSON a, Eq a, Show a) => - TestTree -testRoundTrip = testProperty msg trip - where - msg = show (typeRep @a) - trip (v :: a) = - counterexample (show $ toJSON v) $ - Right v === (parseEither parseJSON . toJSON) v diff --git a/libs/galley-types/test/unit/Test/Galley/Types.hs b/libs/galley-types/test/unit/Test/Galley/Types.hs deleted file mode 100644 index aa2c03a1411..00000000000 --- a/libs/galley-types/test/unit/Test/Galley/Types.hs +++ /dev/null @@ -1,59 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# OPTIONS_GHC -Wno-orphans -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 Test.Galley.Types where - -import Galley.Types.Teams -import Imports -import Test.Galley.Roundtrip (testRoundTrip) -import Test.QuickCheck qualified as QC -import Test.Tasty -import Test.Tasty.QuickCheck -import Wire.API.Team.Feature as Public - -tests :: TestTree -tests = testGroup "Tests" [testRoundTrip @FeatureFlags] - -instance Arbitrary FeatureFlags where - arbitrary = - FeatureFlags - <$> QC.elements [minBound ..] - <*> QC.elements [minBound ..] - <*> QC.elements [minBound ..] - -- the default lock status is implicitly added on deserialization and ignored on serialization, therefore we need to fix it to the default here - -- we will be able to remove this once the lock status is explicitly included in the config - <*> fmap (fmap unlocked) arbitrary - <*> fmap unlocked arbitrary - <*> arbitrary - <*> fmap (fmap unlocked) arbitrary - <*> arbitrary - <*> arbitrary - <*> fmap (fmap unlocked) arbitrary - <*> arbitrary - <*> fmap (fmap unlocked) arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> fmap (fmap unlocked) arbitrary - where - unlocked :: ImplicitLockStatus a -> ImplicitLockStatus a - unlocked = ImplicitLockStatus . Public.setLockStatus Public.LockStatusUnlocked . _unImplicitLockStatus diff --git a/libs/gundeck-types/.ormolu b/libs/gundeck-types/.ormolu deleted file mode 120000 index 157b212d7cd..00000000000 --- a/libs/gundeck-types/.ormolu +++ /dev/null @@ -1 +0,0 @@ -../../.ormolu \ No newline at end of file diff --git a/libs/gundeck-types/gundeck-types.cabal b/libs/gundeck-types/gundeck-types.cabal deleted file mode 100644 index 26e75b33b7f..00000000000 --- a/libs/gundeck-types/gundeck-types.cabal +++ /dev/null @@ -1,85 +0,0 @@ -cabal-version: 1.12 -name: gundeck-types -version: 1.45.0 -description: API types of Gundeck. -category: Network -author: Wire Swiss GmbH -maintainer: Wire Swiss GmbH -copyright: (c) 2017 Wire Swiss GmbH -license: AGPL-3 -license-file: LICENSE -build-type: Simple - -library - exposed-modules: - Gundeck.Types - Gundeck.Types.Common - Gundeck.Types.Event - Gundeck.Types.Presence - Gundeck.Types.Push - Gundeck.Types.Push.V2 - - other-modules: Paths_gundeck_types - hs-source-dirs: src - default-extensions: - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - NoImplicitPrelude - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - - ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -Wredundant-constraints -Wunused-packages - - build-depends: - aeson >=2.0.1.0 - , attoparsec >=0.10 - , base >=4 && <5 - , bytestring >=0.10 - , bytestring-conversion >=0.2 - , containers >=0.5 - , imports - , lens >=4.11 - , network-uri >=2.6 - , text >=0.11 - , types-common >=0.16 - , wire-api - - default-language: GHC2021 diff --git a/libs/gundeck-types/src/Gundeck/Types/Common.hs b/libs/gundeck-types/src/Gundeck/Types/Common.hs deleted file mode 100644 index 1158830d1c8..00000000000 --- a/libs/gundeck-types/src/Gundeck/Types/Common.hs +++ /dev/null @@ -1,61 +0,0 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} - --- 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 Gundeck.Types.Common where - -import Data.Aeson -import Data.Attoparsec.ByteString (takeByteString) -import Data.ByteString.Char8 qualified as Bytes -import Data.ByteString.Conversion -import Data.Text qualified as Text -import Imports -import Network.URI qualified as Net - -newtype CannonId = CannonId - { cannonId :: Text - } - deriving - ( Eq, - Ord, - Show, - FromJSON, - ToJSON, - FromByteString, - ToByteString - ) - -newtype URI = URI - { fromURI :: Net.URI - } - deriving (Eq, Ord, Show) - -instance FromJSON URI where - parseJSON = withText "URI" (parse . Text.unpack) - -instance ToJSON URI where - toJSON uri = String $ Text.pack (show (fromURI uri)) - -instance ToByteString URI where - builder = builder . show . fromURI - -instance FromByteString URI where - parser = takeByteString >>= parse . Bytes.unpack - -parse :: (MonadFail m) => String -> m URI -parse = maybe (fail "Invalid URI") (pure . URI) . Net.parseURI diff --git a/libs/gundeck-types/src/Gundeck/Types/Presence.hs b/libs/gundeck-types/src/Gundeck/Types/Presence.hs deleted file mode 100644 index 04aa78b28ca..00000000000 --- a/libs/gundeck-types/src/Gundeck/Types/Presence.hs +++ /dev/null @@ -1,69 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Gundeck.Types.Presence - ( module Gundeck.Types.Presence, - module Common, - ) -where - -import Data.Aeson -import Data.ByteString.Lazy qualified as Lazy -import Data.Id -import Data.Misc (Milliseconds) -import Gundeck.Types.Common as Common -import Imports - --- | This is created in gundeck by cannon every time the client opens a new websocket connection. --- (That's why we always have a 'ConnId' from the most recent connection by that client.) -data Presence = Presence - { userId :: !UserId, - connId :: !ConnId, - -- | cannon instance hosting the presence - resource :: !URI, - -- | This is 'Nothing' if either (a) the presence is older - -- than mandatory end-to-end encryption, or (b) the client is - -- operating the team settings pages without the need for - -- end-to-end crypto. - clientId :: !(Maybe ClientId), - createdAt :: !Milliseconds, - -- | REFACTOR: temp. addition to ease migration - __field :: !Lazy.ByteString - } - deriving (Eq, Ord, Show) - -instance ToJSON Presence where - toJSON p = - object - [ "user_id" .= userId p, - "device_id" .= connId p, - "resource" .= resource p, - "client_id" .= clientId p, - "created_at" .= createdAt p - ] - -instance FromJSON Presence where - parseJSON = withObject "Presence" $ \o -> - Presence - <$> o .: "user_id" - <*> o .: "device_id" - <*> o .: "resource" - <*> o .:? "client_id" - <*> o .:? "created_at" .!= 0 - <*> pure "" diff --git a/libs/hscim/server/Main.hs b/libs/hscim/server/Main.hs index cfb31664ce0..3d21696c0e1 100644 --- a/libs/hscim/server/Main.hs +++ b/libs/hscim/server/Main.hs @@ -68,7 +68,7 @@ mkUserDB = do E.value = maybe (error "couldn't parse email") - EmailAddress2 + EmailAddress (emailAddress "elton@wire.com"), E.primary = Nothing } diff --git a/libs/hscim/src/Web/Scim/Client.hs b/libs/hscim/src/Web/Scim/Client.hs index c80070fb038..e736534e56e 100644 --- a/libs/hscim/src/Web/Scim/Client.hs +++ b/libs/hscim/src/Web/Scim/Client.hs @@ -31,7 +31,6 @@ module Web.Scim.Client getUsers, getUser, postUser, - putUser, patchUser, deleteUser, @@ -134,15 +133,6 @@ postUser :: IO (StoredUser tag) postUser env tok = case users (scimClients env) tok of ((_ :<|> (_ :<|> r)) :<|> (_ :<|> (_ :<|> _))) -> r -putUser :: - (HasScimClient tag) => - ClientEnv -> - Maybe (AuthData tag) -> - UserId tag -> - User tag -> - IO (StoredUser tag) -putUser env tok = case users (scimClients env) tok of ((_ :<|> (_ :<|> _)) :<|> (r :<|> (_ :<|> _))) -> r - patchUser :: (HasScimClient tag) => ClientEnv -> diff --git a/libs/hscim/src/Web/Scim/Schema/Meta.hs b/libs/hscim/src/Web/Scim/Schema/Meta.hs index 5f5fc851047..11439c94c6a 100644 --- a/libs/hscim/src/Web/Scim/Schema/Meta.hs +++ b/libs/hscim/src/Web/Scim/Schema/Meta.hs @@ -33,6 +33,8 @@ data ETag = Weak Text | Strong Text instance ToJSON ETag where toJSON (Weak tag) = String $ "W/" <> pack (show tag) + -- (if a strong tag contains a "W/" prefix by accident, it will be parsed as weak tag. this + -- is mildly confusing, but should do no harm.) toJSON (Strong tag) = String $ pack (show tag) instance FromJSON ETag where diff --git a/libs/hscim/src/Web/Scim/Schema/User/Email.hs b/libs/hscim/src/Web/Scim/Schema/User/Email.hs index 2b0364bcb7d..cd52a80a7e8 100644 --- a/libs/hscim/src/Web/Scim/Schema/User/Email.hs +++ b/libs/hscim/src/Web/Scim/Schema/User/Email.hs @@ -17,28 +17,29 @@ module Web.Scim.Schema.User.Email where +import Control.Applicative ((<|>)) import Data.Aeson import Data.Text hiding (dropWhile) import Data.Text.Encoding (decodeUtf8, encodeUtf8) import GHC.Generics (Generic) -import Text.Email.Validate -import Web.Scim.Schema.Common +import qualified Text.Email.Validate as Email +import Web.Scim.Schema.Common hiding (value) -newtype EmailAddress2 = EmailAddress2 - {unEmailAddress :: EmailAddress} +newtype EmailAddress = EmailAddress + {unEmailAddress :: Email.EmailAddress} deriving (Show, Eq) -instance FromJSON EmailAddress2 where - parseJSON = withText "Email" $ \e -> case emailAddress (encodeUtf8 e) of +instance FromJSON EmailAddress where + parseJSON = withText "Email" $ \e -> case Email.emailAddress (encodeUtf8 e) of Nothing -> fail "Invalid email" - Just some -> pure $ EmailAddress2 some + Just some -> pure $ EmailAddress some -instance ToJSON EmailAddress2 where - toJSON (EmailAddress2 e) = String $ decodeUtf8 . toByteString $ e +instance ToJSON EmailAddress where + toJSON (EmailAddress e) = String $ decodeUtf8 . Email.toByteString $ e data Email = Email - { typ :: Maybe Text, - value :: EmailAddress2, + { typ :: Maybe Text, -- Work, private, and so on + value :: EmailAddress, primary :: Maybe ScimBool } deriving (Show, Eq, Generic) @@ -48,3 +49,16 @@ instance FromJSON Email where instance ToJSON Email where toJSON = genericToJSON serializeOptions + +emailToEmailAddress :: Email -> Email.EmailAddress +emailToEmailAddress = unEmailAddress . value + +scimEmailsToEmailAddress :: [Email] -> Maybe Email.EmailAddress +scimEmailsToEmailAddress es = pickPrimary es <|> pickFirst es + where + pickFirst [] = Nothing + pickFirst (e : _) = Just (unEmailAddress (value e)) + + pickPrimary = pickFirst . Prelude.filter isPrimary + + isPrimary e = primary e == Just (ScimBool True) diff --git a/libs/hscim/test/Test/Schema/UserSpec.hs b/libs/hscim/test/Test/Schema/UserSpec.hs index 14b7b2ed8fb..1885060facc 100644 --- a/libs/hscim/test/Test/Schema/UserSpec.hs +++ b/libs/hscim/test/Test/Schema/UserSpec.hs @@ -38,7 +38,7 @@ import Lens.Micro import Network.URI.Static (uri) import Test.Hspec import Test.Schema.Util (genUri, mk_prop_caseInsensitive) -import Text.Email.Validate (emailAddress) +import Text.Email.Validate (emailAddress, validate) import qualified Web.Scim.Class.User as UserClass import Web.Scim.Filter (AttrPath (..)) import Web.Scim.Schema.Common (ScimBool (ScimBool), URI (..), WithId (..), lowerKey) @@ -69,6 +69,38 @@ type UserExtraPatch = KeyMap.KeyMap Text spec :: Spec spec = do + describe "scimEmailsToEmailAddress" $ do + let Right adr1 = validate "one@example.com" + Right adr2 = validate "two@example.com" + Right adr3 = validate "three@example.com" + + false1 = Nothing + false2 = Just (ScimBool False) + true = Just (ScimBool True) + + it "returns Nothing if empty" $ do + scimEmailsToEmailAddress [] `shouldBe` Nothing + + it "returns first primary if it exists" $ do + scimEmailsToEmailAddress + [ Email Nothing (EmailAddress adr1) false1, + Email Nothing (EmailAddress adr2) false2, + Email (Just "this is ignored") (EmailAddress adr3) true + ] + `shouldBe` Just adr3 + + it "returns first entry if no primary exists" $ do + scimEmailsToEmailAddress + [ Email Nothing (EmailAddress adr1) false1, + Email Nothing (EmailAddress adr2) false2 + ] + `shouldBe` Just adr1 + scimEmailsToEmailAddress + [ Email Nothing (EmailAddress adr1) false2, + Email Nothing (EmailAddress adr2) false1 + ] + `shouldBe` Just adr1 + describe "applyPatch" $ do it "only applies patch for supported fields" $ do let schemas' = [] @@ -127,13 +159,15 @@ spec = do toJSON minimalUser `shouldBe` minimalUserJson eitherDecode (encode minimalUserJson) `shouldBe` Right minimalUser it "treats 'null' and '[]' as absence of fields" $ - eitherDecode (encode minimalUserJsonRedundant) `shouldBe` Right minimalUser + eitherDecode (encode minimalUserJsonRedundant) + `shouldBe` Right minimalUser it "allows casing variations in field names" $ do require $ mk_prop_caseInsensitive genUser require $ mk_prop_caseInsensitive (ListResponse.fromList . (: []) <$> genStoredUser) eitherDecode (encode minimalUserJsonNonCanonical) `shouldBe` Right minimalUser it "doesn't require the 'schemas' field" $ - eitherDecode (encode minimalUserJsonNoSchemas) `shouldBe` Right minimalUser + eitherDecode (encode minimalUserJsonNoSchemas) + `shouldBe` Right minimalUser it "doesn't add 'extra' if it's an empty object" $ do toJSON (extendedUser UserExtraEmpty) `shouldBe` extendedUserEmptyJson eitherDecode (encode extendedUserEmptyJson) @@ -251,7 +285,7 @@ completeUser = Email.value = maybe (error "couldn't parse email") - EmailAddress2 + EmailAddress (emailAddress "user@example.com"), Email.primary = Nothing } diff --git a/libs/imports/default.nix b/libs/imports/default.nix index 728fca8f3b5..86157dd0c2a 100644 --- a/libs/imports/default.nix +++ b/libs/imports/default.nix @@ -7,6 +7,7 @@ , bytestring , containers , deepseq +, either , extra , gitignoreSource , lib @@ -26,6 +27,7 @@ mkDerivation { bytestring containers deepseq + either extra mtl text diff --git a/libs/imports/imports.cabal b/libs/imports/imports.cabal index a1ddc13d9bb..d2c9bb24cc3 100644 --- a/libs/imports/imports.cabal +++ b/libs/imports/imports.cabal @@ -73,6 +73,7 @@ library , bytestring , containers , deepseq + , either , extra , mtl , text diff --git a/libs/imports/src/Imports.hs b/libs/imports/src/Imports.hs index ef162e09846..aee8b7c0e9c 100644 --- a/libs/imports/src/Imports.hs +++ b/libs/imports/src/Imports.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -35,6 +37,7 @@ module Imports module Data.Monoid, module Data.Maybe, module Data.Either, + module Data.Either.Combinators, module Data.Foldable, module Data.Traversable, module Data.Tuple, @@ -60,19 +63,13 @@ module Imports module UnliftIO.Directory, -- ** Prelude - putChar, putStr, putStrLn, print, - getChar, getLine, - getContents, - interact, readFile, writeFile, appendFile, - readIO, - readLn, -- ** Environment getArgs, @@ -118,12 +115,18 @@ module Imports -- * Functor (<$$>), (<$$$>), + + -- * development + todo, + pattern TODO, + TodoException (..), ) where -- common in some libs import Control.Applicative hiding (empty, many, optional, some) import Control.DeepSeq (NFData (..), deepseq) +import Control.Exception import Control.Monad hiding (forM, forM_, mapM, mapM_, msum, sequence, sequence_) import Control.Monad.Extra (unlessM, whenM) import Control.Monad.IO.Unlift @@ -145,6 +148,7 @@ import Data.ByteString (ByteString) import Data.ByteString.Lazy qualified import Data.Char import Data.Either +import Data.Either.Combinators hiding (fromLeft, fromRight, isLeft, isRight) import Data.Foldable import Data.Function import Data.Functor @@ -152,15 +156,12 @@ import Data.Functor.Identity import Data.HashMap.Strict (HashMap) import Data.HashSet (HashSet) import Data.Int --- 'insert' and 'delete' are common in database modules +import Data.Kind (Type) import Data.List hiding (delete, insert, singleton) --- Lazy and strict versions are the same import Data.Map (Map) import Data.Maybe --- First and Last are going to be deprecated. Use Semigroup instead import Data.Monoid hiding (First (..), Last (..)) import Data.Ord --- conflicts with Options.Applicative.Option (should we care?) import Data.Semigroup hiding (diff) import Data.Set (Set) import Data.String @@ -170,21 +171,18 @@ import Data.Traversable import Data.Tuple import Data.Void import Data.Word +import GHC.Exts import GHC.Generics (Generic) import GHC.Stack (HasCallStack) import Text.Read (readEither, readMaybe) import UnliftIO.Concurrent --- Permissions is common in Galley import UnliftIO.Directory hiding (Permissions) import UnliftIO.Environment import UnliftIO.Exception --- Handle is hidden because it's common in Brig import UnliftIO.IO hiding (Handle, getMonotonicTime) import UnliftIO.IORef import UnliftIO.MVar import UnliftIO.STM --- Explicitly saying what to import because some things from Prelude clash --- with e.g. UnliftIO modules import Prelude ( Bounded (..), Double, @@ -241,9 +239,6 @@ type LByteString = Data.ByteString.Lazy.ByteString ---------------------------------------------------------------------------- -- Lifted functions from Prelude -putChar :: (MonadIO m) => Char -> m () -putChar = liftIO . P.putChar - putStr :: (MonadIO m) => String -> m () putStr = liftIO . P.putStr @@ -253,18 +248,9 @@ putStrLn = liftIO . P.putStrLn print :: (Show a, MonadIO m) => a -> m () print = liftIO . P.print -getChar :: (MonadIO m) => m Char -getChar = liftIO P.getChar - getLine :: (MonadIO m) => m String getLine = liftIO P.getLine -getContents :: (MonadIO m) => m String -getContents = liftIO P.getContents - -interact :: (MonadIO m) => (String -> String) -> m () -interact = liftIO . P.interact - readFile :: (MonadIO m) => FilePath -> m String readFile = liftIO . P.readFile @@ -274,11 +260,117 @@ writeFile = fmap liftIO . P.writeFile appendFile :: (MonadIO m) => FilePath -> String -> m () appendFile = fmap liftIO . P.appendFile -readIO :: (Read a, MonadIO m) => String -> m a -readIO = liftIO . P.readIO +---------------------------------------------------------------------- +-- placeholders + +-- | 'todo' indicates unfinished code. +-- +-- It is to be used whenever you want to indicate that you are missing a part of +-- the implementation and want to fill that in later. +-- +-- This takes a middle ground between other alternatives - unlike typed holes it doesn't cause +-- a /compile time error/, but in contrast to 'GHC.Err.error' and 'GHC.Err.undefined', it does emit +-- a /warning at compilation time/. +-- +-- Similarly to all of 'GHC.Err.undefined', 'GHC.Err.error' and typed holes, this /will throw an error/ +-- if it is /evaluated at runtime/. This error can only be caught in 'System.IO.IO'. +-- +-- This is intended to /never/ stay in code but exists purely for signifying + +-- "work in progress" code. +-- +-- To make the emitted warning a compile error instead (e.g. for use in CI), add +-- the @-Werror=x-todo@ flag to your @OPTIONS_GHC@. +-- +-- ==== __Examples__ +-- +-- @ +-- superComplexFunction :: 'Data.Maybe.Maybe' a -> 'System.IO.IO' 'Data.Int.Int' +-- -- we already know how to implement this in the 'Data.Maybe.Nothing' case +-- superComplexFunction 'Data.Maybe.Nothing' = 'Control.Applicative.pure' 42 +-- -- but the 'Data.Maybe.Just' case is super complicated, so we leave it as 'todo' for now +-- superComplexFunction ('Data.Maybe.Just' a) = 'todo' +-- @ +-- +-- ==== __Representation Polymorphism__ +-- +-- 'todo', in contrast to 'TODO', is fully representation polymorphic +-- +-- @since base-4.21.0.0 +todo :: forall {r :: RuntimeRep} (a :: TYPE r). (HasCallStack) => a +todo = throw TodoException +{-# WARNING todo "'todo' left in code" #-} + +-- FUTUREWORK(mangoiv): should be: WARNING in "x-todo" from ghc 9.8 on + +-- | 'TODO' indicates unfinished code or an unimplemented pattern match +-- +-- You can use this in most positions where you could pass 'todo', but it /also/ can be used in +-- the position of a pattern to indicate that there are cases you have not yet considered. +-- +-- This pattern synonym is marked @COMPLETE@, implying that every match after matching on 'TODO' +-- will /emit a redundant pattern match warning/. Adding new options to your datatype, similarly +-- to how wildcard patterns (patterns starting with an underscore) work, will /not cause any warnings or errors/. +-- +-- ==== __Examples__ +-- +-- Since the pattern match is strict, even if the branch itself does not evaluate to bottom, matching on +-- 'TODO' will. +-- +-- @ +-- >>> x = [] +-- >>> case x of +-- ... (x : _) -> x +-- ... 'TODO' -> 42 +-- *** Exception: Develop.Placeholder.todo: not yet implemented +-- @ +-- +-- As usual, this behaviour can be reversed by using a @~@ in front of 'TODO' in pattern position. +-- +-- @ +-- >>> x = [] +-- >>> case x of +-- ... (x : _) -> x +-- ... ~'TODO' -> 42 +-- 42 +-- @ +-- +-- In most situations, 'TODO' can be used just like 'todo', where the above is equivalent to the below +-- +-- @ +-- >>> y :: 'Data.Int.Int' = 'todo' +-- >>> x :: 'Data.Int.Int' = 'TODO' +-- @ +-- +-- +-- ==== __Representation Polymorphism__ +-- +-- Mind that pattern synonyms may not be representation polymorphic, hence, if you need something +-- that can be used with some kind other than 'Data.Kind.Type', you have to use 'todo'. For example, +-- 'TODO' cannot stand instead of a pattern match on an @'GHC.Exts.Int#' :: 'TYPE' 'GHC.Exts.IntRep'@ +-- or as a placeholder for a @'GHC.Exts.ByteArray#' :: 'GHC.Exts.UnliftedType'@ +-- +-- @since base-4.21.0.0 +pattern TODO :: forall (a :: Type). (HasCallStack) => forall. a +pattern TODO <- (throw TodoException -> !_unused) + where + TODO = throw TodoException +{-# WARNING TODO "'TODO' left in code" #-} + +-- FUTUREWORK(mangoiv): should be WARNING in "x-todo" from ghc 9.8 on + +{-# COMPLETE TODO #-} + +-- | This is the 'Exception' thrown by 'todo'. +-- +-- This exception occurring indicates a bug as 'todo' should /never/ remain in code. +-- +-- @since base-4.21.0.0 +data TodoException = TodoException + deriving stock (Eq, Show) -readLn :: (Read a, MonadIO m) => m a -readLn = liftIO P.readLn +instance Exception TodoException where + displayException _ = "Develop.Placeholder.todo: not yet implemented" ---------------------------------------------------------------------- -- Functor diff --git a/libs/metrics-wai/default.nix b/libs/metrics-wai/default.nix index eb3a260e929..5a64f6b4f93 100644 --- a/libs/metrics-wai/default.nix +++ b/libs/metrics-wai/default.nix @@ -14,11 +14,11 @@ , servant , servant-multipart , text +, types-common , utf8-string , wai , wai-middleware-prometheus , wai-route -, wai-routing }: mkDerivation { pname = "metrics-wai"; @@ -32,11 +32,11 @@ mkDerivation { servant servant-multipart text + types-common utf8-string wai wai-middleware-prometheus wai-route - wai-routing ]; testHaskellDepends = [ base containers hspec imports ]; testToolDepends = [ hspec-discover ]; diff --git a/libs/metrics-wai/metrics-wai.cabal b/libs/metrics-wai/metrics-wai.cabal index ed848c893cb..779eda44ec6 100644 --- a/libs/metrics-wai/metrics-wai.cabal +++ b/libs/metrics-wai/metrics-wai.cabal @@ -76,11 +76,11 @@ library , servant , servant-multipart , text >=0.11 + , types-common , utf8-string , wai >=3 , wai-middleware-prometheus , wai-route >=0.3 - , wai-routing default-language: GHC2021 diff --git a/libs/metrics-wai/src/Data/Metrics/Middleware/Prometheus.hs b/libs/metrics-wai/src/Data/Metrics/Middleware/Prometheus.hs index f1f7c1ca562..b185b6b60da 100644 --- a/libs/metrics-wai/src/Data/Metrics/Middleware/Prometheus.hs +++ b/libs/metrics-wai/src/Data/Metrics/Middleware/Prometheus.hs @@ -16,29 +16,27 @@ -- with this program. If not, see . module Data.Metrics.Middleware.Prometheus - ( waiPrometheusMiddleware, + ( waiPrometheusMiddlewarePaths, normalizeWaiRequestRoute, ) where +import Data.Id import Data.Metrics.Types (Paths, treeLookup) -import Data.Metrics.WaiRoute (treeToPaths) import Data.Text.Encoding qualified as T import Imports import Network.Wai qualified as Wai import Network.Wai.Middleware.Prometheus qualified as Promth -import Network.Wai.Routing.Route (Routes, prepare) --- | Adds a prometheus metrics endpoint at @/i/metrics@ --- This middleware requires your servers 'Routes' because it does some normalization --- (e.g. removing params from calls) -waiPrometheusMiddleware :: (Monad m) => Routes a m b -> Wai.Middleware -waiPrometheusMiddleware routes = +-- | Helper function that should only be needed as long as we have wai-routing code left in +-- proxy: run 'treeToPaths' on old routing tables and 'routeToPaths' on the servant ones, and +-- feed both to this function. +waiPrometheusMiddlewarePaths :: Paths -> Wai.Middleware +waiPrometheusMiddlewarePaths paths = Promth.prometheus conf . instrument (normalizeWaiRequestRoute paths) where -- See Note [Raw Response] instrument = Promth.instrumentHandlerValueWithFilter Promth.ignoreRawResponses - paths = treeToPaths $ prepare routes conf = Promth.def { Promth.prometheusEndPoint = ["i", "metrics"], @@ -57,4 +55,4 @@ normalizeWaiRequestRoute paths req = pathInfo -- Use the normalized path info if available; otherwise dump the raw path info for -- debugging purposes pathInfo :: Text - pathInfo = T.decodeUtf8 $ fromMaybe "N/A" mPathInfo + pathInfo = T.decodeUtf8 $ fromMaybe defRequestId mPathInfo diff --git a/libs/metrics-wai/src/Data/Metrics/Servant.hs b/libs/metrics-wai/src/Data/Metrics/Servant.hs index 6d1df7d26ff..490ed13ded2 100644 --- a/libs/metrics-wai/src/Data/Metrics/Servant.hs +++ b/libs/metrics-wai/src/Data/Metrics/Servant.hs @@ -27,10 +27,9 @@ module Data.Metrics.Servant where import Data.ByteString.UTF8 qualified as UTF8 -import Data.Metrics.Middleware.Prometheus (normalizeWaiRequestRoute) +import Data.Id import Data.Metrics.Types import Data.Metrics.Types qualified as Metrics -import Data.Metrics.WaiRoute (treeToPaths) import Data.Proxy import Data.Text.Encoding import Data.Text.Encoding.Error @@ -40,7 +39,6 @@ import Imports import Network.Wai qualified as Wai import Network.Wai.Middleware.Prometheus import Network.Wai.Middleware.Prometheus qualified as Promth -import Network.Wai.Routing (Routes, prepare) import Servant.API import Servant.Multipart @@ -52,23 +50,11 @@ servantPrometheusMiddleware _ = Promth.prometheus conf . instrument promthNormal promthNormalize req = pathInfo where mPathInfo = Metrics.treeLookup (routesToPaths @api) $ encodeUtf8 <$> Wai.pathInfo req - pathInfo = decodeUtf8With lenientDecode $ fromMaybe "N/A" mPathInfo + pathInfo = decodeUtf8With lenientDecode $ fromMaybe defRequestId mPathInfo -- See Note [Raw Response] instrument = Promth.instrumentHandlerValueWithFilter Promth.ignoreRawResponses -servantPlusWAIPrometheusMiddleware :: forall proxy api a m b. (RoutesToPaths api, Monad m) => Routes a m b -> proxy api -> Wai.Middleware -servantPlusWAIPrometheusMiddleware routes _ = do - Promth.prometheus conf . instrument (normalizeWaiRequestRoute paths) - where - -- See Note [Raw Response] - instrument = Promth.instrumentHandlerValueWithFilter Promth.ignoreRawResponses - - paths = - let Paths servantPaths = routesToPaths @api - Paths waiPaths = treeToPaths (prepare routes) - in Paths (meltTree (servantPaths <> waiPaths)) - conf :: PrometheusSettings conf = Promth.def diff --git a/libs/metrics-wai/src/Data/Metrics/Types.hs b/libs/metrics-wai/src/Data/Metrics/Types.hs index 0d1a70903d0..4d83874789b 100644 --- a/libs/metrics-wai/src/Data/Metrics/Types.hs +++ b/libs/metrics-wai/src/Data/Metrics/Types.hs @@ -41,6 +41,7 @@ newtype PathTemplate = PathTemplate Text -- (e.g. user id). newtype Paths = Paths (Forest PathSegment) deriving (Eq, Show) + deriving newtype (Semigroup) type PathSegment = Either ByteString ByteString diff --git a/libs/polysemy-wire-zoo/default.nix b/libs/polysemy-wire-zoo/default.nix index ebf7e8de8c5..21bf9204774 100644 --- a/libs/polysemy-wire-zoo/default.nix +++ b/libs/polysemy-wire-zoo/default.nix @@ -19,6 +19,7 @@ , polysemy , polysemy-check , polysemy-plugin +, prometheus-client , QuickCheck , saml2-web-sso , time @@ -45,6 +46,7 @@ mkDerivation { polysemy polysemy-check polysemy-plugin + prometheus-client QuickCheck saml2-web-sso time diff --git a/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal b/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal index 505874d7b6c..5d8bb12ab31 100644 --- a/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal +++ b/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal @@ -11,6 +11,7 @@ license: AGPL-3 build-type: Simple library + -- cabal-fmt: expand src exposed-modules: Polysemy.Testing Polysemy.TinyLog @@ -23,6 +24,8 @@ library Wire.Sem.Logger Wire.Sem.Logger.Level Wire.Sem.Logger.TinyLog + Wire.Sem.Metrics + Wire.Sem.Metrics.IO Wire.Sem.Now Wire.Sem.Now.Input Wire.Sem.Now.IO @@ -83,7 +86,7 @@ library build-depends: aeson - , base >=4.6 && <5.0 + , base >=4.6 && <5.0 , bytestring , cassandra-util , crypton @@ -94,6 +97,7 @@ library , polysemy , polysemy-check , polysemy-plugin + , prometheus-client , QuickCheck , saml2-web-sso , time diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/Concurrency.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Concurrency.hs index 8cdf9f6600a..8d2b82bc758 100644 --- a/libs/polysemy-wire-zoo/src/Wire/Sem/Concurrency.hs +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Concurrency.hs @@ -5,7 +5,6 @@ module Wire.Sem.Concurrency where import Data.Kind (Type) import Imports import Polysemy -import Polysemy.Internal data ConcurrencySafety = Safe | Unsafe @@ -105,67 +104,3 @@ unsafePooledForConcurrentlyN_ n as f = send (UnsafePooledMapConcurrentlyN_ n f as :: Concurrency 'Unsafe (Sem r) ()) {-# INLINEABLE unsafePooledForConcurrentlyN_ #-} - -pooledMapConcurrentlyN :: - forall r' r t a b. - (r' ~ '[Final IO]) => - (Member (Concurrency 'Safe) r, Subsume r' r, Foldable t) => - -- | Max. number of threads. Should not be less than 1. - Int -> - (a -> Sem r' b) -> - t a -> - Sem r [b] -pooledMapConcurrentlyN n f as = - send - ( UnsafePooledMapConcurrentlyN n (subsume_ @r' @r . f) as :: - Concurrency 'Safe (Sem r) [b] - ) -{-# INLINEABLE pooledMapConcurrentlyN #-} - -pooledMapConcurrentlyN_ :: - forall r' r t a b. - (r' ~ '[Final IO]) => - (Member (Concurrency 'Safe) r, Subsume r' r, Foldable t) => - -- | Max. number of threads. Should not be less than 1. - Int -> - (a -> Sem r' b) -> - t a -> - Sem r () -pooledMapConcurrentlyN_ n f as = - send - ( UnsafePooledMapConcurrentlyN_ n (subsume_ @r' @r . f) as :: - Concurrency 'Safe (Sem r) () - ) -{-# INLINEABLE pooledMapConcurrentlyN_ #-} - -pooledForConcurrentlyN :: - forall r' r t a b. - (r' ~ '[Final IO]) => - (Member (Concurrency 'Safe) r, Subsume r' r, Foldable t) => - -- | Max. number of threads. Should not be less than 1. - Int -> - t a -> - (a -> Sem r' b) -> - Sem r [b] -pooledForConcurrentlyN n as f = - send - ( UnsafePooledMapConcurrentlyN n (subsume_ @r' @r . f) as :: - Concurrency 'Safe (Sem r) [b] - ) -{-# INLINEABLE pooledForConcurrentlyN #-} - -pooledForConcurrentlyN_ :: - forall r' r t a b. - (r' ~ '[Final IO]) => - (Member (Concurrency 'Safe) r, Subsume r' r, Foldable t) => - -- | Max. number of threads. Should not be less than 1. - Int -> - t a -> - (a -> Sem r' b) -> - Sem r () -pooledForConcurrentlyN_ n as f = - send - ( UnsafePooledMapConcurrentlyN_ n (subsume_ @r' @r . f) as :: - Concurrency 'Safe (Sem r) () - ) -{-# INLINEABLE pooledForConcurrentlyN_ #-} diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/Metrics.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Metrics.hs new file mode 100644 index 00000000000..63cba3bce8a --- /dev/null +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Metrics.hs @@ -0,0 +1,21 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.Sem.Metrics where + +import Imports +import Polysemy +import Prometheus (Counter, Gauge) + +-- | NOTE: Vectors would require non trival changes because +-- 'Prometheus.withLabel' take a paramter of type 'metric -> IO ()'. +data Metrics m a where + AddCounter :: Counter -> Double -> Metrics m () + AddGauge :: Gauge -> Double -> Metrics m () + +makeSem ''Metrics + +incCounter :: (Member Metrics r) => Counter -> Sem r () +incCounter c = addCounter c 1 + +incGauge :: (Member Metrics r) => Gauge -> Sem r () +incGauge c = addGauge c 1 diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/Metrics/IO.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Metrics/IO.hs new file mode 100644 index 00000000000..1b357927c87 --- /dev/null +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Metrics/IO.hs @@ -0,0 +1,16 @@ +module Wire.Sem.Metrics.IO where + +import Imports +import Polysemy +import qualified Prometheus as Prom +import Wire.Sem.Metrics + +runMetricsToIO :: (Member (Embed IO) r) => InterpreterFor Metrics r +runMetricsToIO = interpret $ \case + AddCounter c n -> embed . void $ Prom.addCounter @IO c n + AddGauge g n -> embed $ Prom.addGauge @IO g n + +ignoreMetrics :: InterpreterFor Metrics r +ignoreMetrics = interpret $ \case + AddCounter _ _ -> pure () + AddGauge _ _ -> pure () diff --git a/libs/tasty-cannon/src/Test/Tasty/Cannon.hs b/libs/tasty-cannon/src/Test/Tasty/Cannon.hs index 7fb72b5ca33..44503d63123 100644 --- a/libs/tasty-cannon/src/Test/Tasty/Cannon.hs +++ b/libs/tasty-cannon/src/Test/Tasty/Cannon.hs @@ -31,12 +31,8 @@ module Test.Tasty.Cannon close, bracket, bracketAsClient, - bracketN, - bracketAsClientN, -- ** Random Connection IDs - connectR, - connectAsClientR, bracketR, bracketAsClientR, bracketR2, @@ -142,36 +138,8 @@ bracketAsClient :: bracketAsClient can uid client conn = Catch.bracket (connectAsClient can uid client conn) close -bracketN :: - (MonadIO m, MonadMask m) => - Cannon -> - [(UserId, ConnId)] -> - ([WebSocket] -> m a) -> - m a -bracketN c us f = go [] us - where - go wss [] = f (reverse wss) - go wss ((x, y) : xs) = bracket c x y (\ws -> go (ws : wss) xs) - -bracketAsClientN :: - (MonadMask m, MonadIO m) => - Cannon -> - [(UserId, ClientId, ConnId)] -> - ([WebSocket] -> m a) -> - m a -bracketAsClientN c us f = go [] us - where - go wss [] = f (reverse wss) - go wss ((x, y, z) : xs) = bracketAsClient c x y z (\ws -> go (ws : wss) xs) - -- Random Connection IDs -connectR :: (MonadIO m) => Cannon -> UserId -> m WebSocket -connectR can uid = randomConnId >>= connect can uid - -connectAsClientR :: (MonadIO m) => Cannon -> UserId -> ClientId -> m WebSocket -connectAsClientR can uid clientId = randomConnId >>= connectAsClient can uid clientId - bracketR :: (MonadIO m, MonadMask m) => Cannon -> UserId -> (WebSocket -> m a) -> m a bracketR can usr f = do cid <- randomConnId diff --git a/libs/types-common-aws/src/Util/Test/SQS.hs b/libs/types-common-aws/src/Util/Test/SQS.hs index 6a527482150..f9d015a1631 100644 --- a/libs/types-common-aws/src/Util/Test/SQS.hs +++ b/libs/types-common-aws/src/Util/Test/SQS.hs @@ -135,12 +135,6 @@ execute env = AWS.runResourceT . flip runReaderT env ----------------------------------------------------------------------------- -- Internal. Most of these functions _can_ be used outside of this function -- but probably do not need to -receive :: Int -> Text -> SQS.ReceiveMessage -receive n url = - SQS.newReceiveMessage url - & set SQS.receiveMessage_waitTimeSeconds (Just 1) - . set SQS.receiveMessage_maxNumberOfMessages (Just n) - . set SQS.receiveMessage_visibilityTimeout (Just 1) deleteMessage :: (MonadReader AWS.Env m, MonadResource m) => Text -> SQS.Message -> m () deleteMessage url m = do diff --git a/libs/types-common-journal/src/Data/Proto/Id.hs b/libs/types-common-journal/src/Data/Proto/Id.hs index 6210c8e78ac..8a0c3ed6a25 100644 --- a/libs/types-common-journal/src/Data/Proto/Id.hs +++ b/libs/types-common-journal/src/Data/Proto/Id.hs @@ -17,13 +17,10 @@ module Data.Proto.Id where -import Data.ByteString.Lazy (fromStrict, toStrict) +import Data.ByteString.Lazy (toStrict) import Data.Id import Data.UUID qualified as UUID import Imports toBytes :: Id a -> ByteString toBytes = toStrict . UUID.toByteString . toUUID - -fromBytes :: ByteString -> Maybe (Id a) -fromBytes = fmap Id . UUID.fromByteString . fromStrict diff --git a/libs/types-common-journal/types-common-journal.cabal b/libs/types-common-journal/types-common-journal.cabal index d0dc0986382..958378b2367 100644 --- a/libs/types-common-journal/types-common-journal.cabal +++ b/libs/types-common-journal/types-common-journal.cabal @@ -17,7 +17,7 @@ extra-source-files: custom-setup setup-depends: base - , Cabal + , Cabal >=3.12 , proto-lens-setup library diff --git a/libs/types-common/default.nix b/libs/types-common/default.nix index 7421aae499c..0cafcabbfd7 100644 --- a/libs/types-common/default.nix +++ b/libs/types-common/default.nix @@ -19,11 +19,11 @@ , cryptohash-sha1 , crypton , currency-codes +, email-validate , generic-random , gitignoreSource , hashable , http-api-data -, http-types , imports , iproute , iso3166-country-codes @@ -40,12 +40,14 @@ , quickcheck-instances , random , schema-profunctor +, scientific , servant-server , string-conversions , tagged , tasty , tasty-hunit , tasty-quickcheck +, template-haskell , text , time , time-locale-compat @@ -77,10 +79,10 @@ mkDerivation { cryptohash-sha1 crypton currency-codes + email-validate generic-random hashable http-api-data - http-types imports iproute iso3166-country-codes @@ -96,10 +98,12 @@ mkDerivation { quickcheck-instances random schema-profunctor + scientific servant-server tagged tasty tasty-hunit + template-haskell text time time-locale-compat @@ -117,6 +121,7 @@ mkDerivation { bytestring bytestring-conversion cereal + email-validate imports protobuf string-conversions @@ -126,6 +131,7 @@ mkDerivation { text time unordered-containers + utf8-string uuid ]; description = "Shared type definitions"; diff --git a/libs/types-common/src/Data/Credentials.hs b/libs/types-common/src/Data/Credentials.hs index 52c632f9307..5423b574e7a 100644 --- a/libs/types-common/src/Data/Credentials.hs +++ b/libs/types-common/src/Data/Credentials.hs @@ -18,11 +18,8 @@ module Data.Credentials where import Data.Aeson (FromJSON) -import Data.ByteString.Base64 qualified as B64 import Data.Text -import Data.Text.Encoding qualified as TE import Imports -import Network.HTTP.Types.Header -- | Generic credentials for authenticating a user. Usually used for deserializing from a secret yaml file. data Credentials = Credentials @@ -32,6 +29,3 @@ data Credentials = Credentials deriving stock (Generic) instance FromJSON Credentials - -mkBasicAuthHeader :: Credentials -> Header -mkBasicAuthHeader (Credentials u p) = (hAuthorization, "Basic " <> B64.encode (TE.encodeUtf8 (u <> ":" <> p))) diff --git a/libs/types-common/src/Data/Handle.hs b/libs/types-common/src/Data/Handle.hs index 64842b51e70..ce8c8f1a4ac 100644 --- a/libs/types-common/src/Data/Handle.hs +++ b/libs/types-common/src/Data/Handle.hs @@ -21,7 +21,6 @@ module Data.Handle ( Handle (fromHandle), parseHandle, parseHandleEither, - isValidHandle, BadHandle (..), ) where @@ -73,9 +72,6 @@ instance FromByteString Handle where parseHandle :: Text -> Maybe Handle parseHandle = either (const Nothing) Just . parseHandleEither -isValidHandle :: Text -> Bool -isValidHandle = isRight . parseHandleEither - parseHandleEither :: Text -> Either String Handle parseHandleEither = Atto.parseOnly (handleParser <* Atto.endOfInput) . Text.E.encodeUtf8 diff --git a/libs/types-common/src/Data/HavePendingInvitations.hs b/libs/types-common/src/Data/HavePendingInvitations.hs new file mode 100644 index 00000000000..03afbe6c77c --- /dev/null +++ b/libs/types-common/src/Data/HavePendingInvitations.hs @@ -0,0 +1,14 @@ +module Data.HavePendingInvitations where + +import Imports +import Wire.Arbitrary + +data HavePendingInvitations + = WithPendingInvitations + | NoPendingInvitations + deriving (Eq, Show, Ord, Generic) + deriving (Arbitrary) via GenericUniform HavePendingInvitations + +fromBool :: Bool -> HavePendingInvitations +fromBool True = WithPendingInvitations +fromBool False = NoPendingInvitations diff --git a/libs/types-common/src/Data/Id.hs b/libs/types-common/src/Data/Id.hs index 3ef7152c913..9ae33bcc7df 100644 --- a/libs/types-common/src/Data/Id.hs +++ b/libs/types-common/src/Data/Id.hs @@ -48,10 +48,14 @@ module Data.Id -- * Other IDs ConnId (..), RequestId (..), + defRequestId, BotId (..), NoId, OAuthClientId, OAuthRefreshTokenId, + + -- * Utils + uuidSchema, ) where @@ -175,23 +179,23 @@ newtype Id a = Id deriving (ToJSON, FromJSON, S.ToSchema) via Schema (Id a) instance ToSchema (Id a) where - schema = Id <$> toUUID .= uuid - where - uuid :: ValueSchema NamedSwaggerDoc UUID - uuid = - mkSchema - (addExample (swaggerDoc @UUID)) - ( A.withText - "UUID" - ( maybe (fail "Invalid UUID") pure - . UUID.fromText - ) - ) - (pure . A.toJSON . UUID.toText) - - addExample = - S.schema . S.example - ?~ toJSON ("99db9768-04e3-4b5d-9268-831b6a25c4ab" :: Text) + schema = Id <$> toUUID .= uuidSchema + +uuidSchema :: ValueSchema NamedSwaggerDoc UUID +uuidSchema = + mkSchema + (addExample (swaggerDoc @UUID)) + ( A.withText + "UUID" + ( maybe (fail "Invalid UUID") pure + . UUID.fromText + ) + ) + (pure . A.toJSON . UUID.toText) + where + addExample = + S.schema . S.example + ?~ toJSON ("99db9768-04e3-4b5d-9268-831b6a25c4ab" :: Text) -- REFACTOR: non-derived, custom show instances break pretty-show and violate the law -- that @show . read == id@. can we derive Show here? @@ -418,6 +422,9 @@ newtype RequestId = RequestId ToBytes ) +defRequestId :: (IsString s) => s +defRequestId = "N/A" + instance ToSchema RequestId where schema = RequestId . encodeUtf8 diff --git a/libs/types-common/src/Data/Json/Util.hs b/libs/types-common/src/Data/Json/Util.hs index 87ac386a7c4..c152fc6539c 100644 --- a/libs/types-common/src/Data/Json/Util.hs +++ b/libs/types-common/src/Data/Json/Util.hs @@ -43,7 +43,6 @@ module Data.Json.Util base64Schema, base64URLSchema, Base64ByteStringL (..), - base64SchemaL, fromBase64TextLenient, fromBase64Text, toBase64Text, @@ -269,9 +268,6 @@ instance S.ToParamSchema Base64ByteStringL where base64SchemaLN :: ValueSchema NamedSwaggerDoc LByteString base64SchemaLN = L.toStrict .= fmap L.fromStrict base64SchemaN -base64SchemaL :: ValueSchema SwaggerDoc LByteString -base64SchemaL = unnamed base64SchemaLN - -------------------------------------------------------------------------------- -- Utilities diff --git a/libs/types-common/src/Data/Mailbox.hs b/libs/types-common/src/Data/Mailbox.hs new file mode 100644 index 00000000000..c9889d051f4 --- /dev/null +++ b/libs/types-common/src/Data/Mailbox.hs @@ -0,0 +1,214 @@ +module Data.Mailbox where + +import Control.Applicative (optional) +import Data.Aeson +import Data.Attoparsec.ByteString (Parser) +import Data.Attoparsec.ByteString qualified as BSParser +import Data.Attoparsec.ByteString.Char8 qualified as Char8Parser +import Data.Char qualified as Char +import Data.Text qualified as Text +import Data.Text.Encoding qualified as Text +import Imports +import Text.Email.Parser + +-- | Mailbox address according to +-- https://www.rfc-editor.org/rfc/rfc5322#section-3.4 +data Mailbox = Mailbox + { name :: Maybe [Text], + address :: EmailAddress + } + deriving (Show, Eq) + +parseMailbox :: ByteString -> Either String Mailbox +parseMailbox = BSParser.parseOnly (mailboxParser <* BSParser.endOfInput) + +instance FromJSON Mailbox where + parseJSON = + withText "Mailbox" $ + either fail pure . parseMailbox . Text.encodeUtf8 + +-- * Internal + +newtype Comment = Comment [CommentContent] + +data CommentContent = CommentChar Char | SubComment Comment + +atextParser :: Parser Char +atextParser = + alpha + <|> num + <|> allowedSpecials + where + alpha = Char8Parser.satisfy (\c -> Char.isAlpha c && Char.isAscii c) + num = Char8Parser.satisfy Char.isNumber + allowedSpecials = + Char8Parser.satisfy $ + -- Make sure the - is the first or the last symbol, otherwise inClass + -- treats it as a signifier of range + Char8Parser.inClass "-!#$%&'*+/=?^_`{|}~" + +wspParser :: Parser Char +wspParser = Char8Parser.satisfy (\c -> c == ' ' || c == '\t') + +crlfParser :: Parser String +crlfParser = do + void $ Char8Parser.string "\r\n" + pure "\r\n" + +fwsParser :: Parser String +fwsParser = + let wspsAndCrlf = do + wsps <- Char8Parser.many' wspParser + crlf <- crlfParser + pure $ wsps <> crlf + notObs = do + mWspsAndCrlf <- optional wspsAndCrlf + wsps <- Char8Parser.many1' wspParser + pure $ fromMaybe "" mWspsAndCrlf <> wsps + in notObs <|> obsFwsParser + +obsFwsParser :: Parser String +obsFwsParser = do + wsps <- Char8Parser.many1' wspParser + crlfWsps <- Char8Parser.many' $ do + crlf <- crlfParser + wspsAfterCrlf <- Char8Parser.many1' wspParser + pure $ crlf <> wspsAfterCrlf + pure $ concat $ wsps : crlfWsps + +ctextParser :: Parser Char +ctextParser = do + let isAllowedChar w = + (w >= 33 && w <= 39) + || (w >= 42 && w <= 91) + || (w >= 93 && w <= 126) + Char8Parser.satisfy (isAllowedChar . Char.ord) <|> obsNoWsCtl + +-- | US-ASCII control characters that do not include the carriage return, line +-- feed, and white space characters +obsNoWsCtl :: Parser Char +obsNoWsCtl = do + Char8Parser.satisfy + ( \(ord -> c) -> + (c >= 1 && c <= 8) + || c == 11 + || c == 12 + || (c >= 14 && c <= 31) + || (c == 127) + ) + +obsCtextParser, obsQtextParser :: Parser Char +obsCtextParser = obsNoWsCtl +obsQtextParser = obsNoWsCtl + +quotedPairParser :: Parser Char +quotedPairParser = do + void $ Char8Parser.char '\\' + vCharParser <|> wspParser + +vCharParser :: Parser Char +vCharParser = + Char8Parser.satisfy (\c -> ord c >= 0x21 && ord c <= 0x7E) + +ccontentParser :: Parser CommentContent +ccontentParser = + fmap CommentChar ctextParser + <|> fmap CommentChar quotedPairParser + <|> fmap SubComment commentParser + +commentParser :: Parser Comment +commentParser = do + _ <- Char8Parser.char '(' + comment <- Char8Parser.many' $ do + _ <- optional fwsParser + ccontentParser + _ <- Char8Parser.char ')' + pure $ Comment comment + +cfwsParser :: Parser [Comment] +cfwsParser = do + let commentWithFws = do + comment <- Char8Parser.many1' $ do + _ <- optional fwsParser + commentParser + _ <- optional fwsParser + pure comment + commentWithFws <|> fmap (const []) fwsParser + +atomParser :: Parser String +atomParser = do + _ <- optional cfwsParser + atom <- Char8Parser.many1' atextParser + _ <- optional cfwsParser + pure atom + +qtextParser :: Parser Char +qtextParser = + let newParser = Char8Parser.satisfy $ \(ord -> c) -> + c == 33 || (c >= 35 && c <= 91) || (c >= 93 && c <= 126) + in newParser <|> obsQtextParser + +qcontentParser :: Parser Char +qcontentParser = qtextParser <|> quotedPairParser + +quotedStringParser :: Parser String +quotedStringParser = do + _ <- optional cfwsParser + _ <- Char8Parser.char '"' + str <- fmap concat . Char8Parser.many' $ do + mLeadingSpace <- optional fwsParser + c <- qcontentParser + pure $ fromMaybe "" mLeadingSpace <> [c] + mTrailingSpace <- optional fwsParser + _ <- Char8Parser.char '"' + pure $ str <> fromMaybe "" mTrailingSpace + +wordParser :: Parser String +wordParser = atomParser <|> quotedStringParser + +-- | The spec says +-- +-- @ +-- phrase = 1*word / obs-phrase +-- @ +-- +-- Here if we tried to write it using '<|>', parising "John Q. Doe" would +-- succeed with a 'many1 wordParser' while having parsed up to "John Q" and the +-- rest of the string will be left for next parsers, which would likely fail. To +-- avoid all that we can use just the obsPhraseParser, which forces the first +-- thing to be a word and then allows for dots and CFWS. +phraseParser :: Parser [String] +phraseParser = obsPhraseParser + +-- | Ignores comments +obsPhraseParser :: Parser [String] +obsPhraseParser = do + w1 <- wordParser + ws <- fmap catMaybes . Char8Parser.many' $ do + fmap Just wordParser + <|> fmap (Just . (: [])) (Char8Parser.char '.') + <|> fmap (const Nothing) cfwsParser + pure $ w1 : ws + +nameParser :: Parser [Text] +nameParser = map Text.pack <$> phraseParser + +-- | Does not implement parsing for obs-angle-addr +angleAddrParser :: Parser EmailAddress +angleAddrParser = do + _ <- optional cfwsParser + _ <- Char8Parser.char '<' + addr <- addrSpec + _ <- Char8Parser.char '>' + _ <- optional cfwsParser + pure addr + +nameAddrParser :: Parser Mailbox +nameAddrParser = + Mailbox + <$> optional nameParser + <*> angleAddrParser + +mailboxParser :: Parser Mailbox +mailboxParser = + nameAddrParser <|> fmap (Mailbox Nothing) addrSpec diff --git a/libs/types-common/src/Data/Misc.hs b/libs/types-common/src/Data/Misc.hs index fc896fb1e59..a72995b8667 100644 --- a/libs/types-common/src/Data/Misc.hs +++ b/libs/types-common/src/Data/Misc.hs @@ -49,11 +49,9 @@ module Data.Misc PlainTextPassword6, PlainTextPassword8, plainTextPassword6, - plainTextPassword8, fromPlainTextPassword, plainTextPassword8Unsafe, plainTextPassword6Unsafe, - plainTextPassword8To6, -- * Typesafe FUTUREWORKS FutureWork (..), @@ -324,19 +322,12 @@ plainTextPassword6 = fmap PlainTextPassword' . checked plainTextPassword6Unsafe :: Text -> PlainTextPassword6 plainTextPassword6Unsafe = PlainTextPassword' . unsafeRange -plainTextPassword8 :: Text -> Maybe PlainTextPassword8 -plainTextPassword8 = fmap PlainTextPassword' . checked - plainTextPassword8Unsafe :: Text -> PlainTextPassword8 plainTextPassword8Unsafe = PlainTextPassword' . unsafeRange fromPlainTextPassword :: PlainTextPassword' t -> Text fromPlainTextPassword = fromRange . fromPlainTextPassword' --- | Convert a 'PlainTextPassword8' to a legacy 'PlainTextPassword'. -plainTextPassword8To6 :: PlainTextPassword8 -> PlainTextPassword6 -plainTextPassword8To6 = PlainTextPassword' . unsafeRange . fromPlainTextPassword - newtype PlainTextPassword' (minLen :: Nat) = PlainTextPassword' {fromPlainTextPassword' :: Range minLen (1024 :: Nat) Text} deriving stock (Eq, Generic) @@ -378,8 +369,6 @@ showT = Text.pack . show {-# INLINE showT #-} -- | Decodes a base64 'Text' to a regular 'ByteString' (if possible) -from64 :: Text -> Maybe ByteString -from64 = hush . B64.decode . encodeUtf8 - where - hush = either (const Nothing) Just +from64 :: Text -> Either String ByteString +from64 = B64.decode . encodeUtf8 {-# INLINE from64 #-} diff --git a/libs/types-common/src/Data/Qualified.hs b/libs/types-common/src/Data/Qualified.hs index 0d1632ad4b1..18407f557fd 100644 --- a/libs/types-common/src/Data/Qualified.hs +++ b/libs/types-common/src/Data/Qualified.hs @@ -25,10 +25,12 @@ module Data.Qualified Qualified (..), qToPair, QualifiedWithTag, + RelativeTo (..), + relativeTo, tUnqualified, - tUnqualifiedL, tDomain, tUntagged, + tSplit, qTagUnsafe, Remote, toRemoteUnsafe, @@ -48,7 +50,7 @@ module Data.Qualified ) where -import Control.Lens (Lens, lens, over, (?~)) +import Control.Lens (over, (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Bifunctor (first) import Data.Domain (Domain) @@ -93,8 +95,9 @@ tUnqualified = qUnqualified . tUntagged tDomain :: QualifiedWithTag t a -> Domain tDomain = qDomain . tUntagged -tUnqualifiedL :: Lens (QualifiedWithTag t a) (QualifiedWithTag t b) a b -tUnqualifiedL = lens tUnqualified qualifyAs +-- | perform 'qUnqualified' and 'tDomain' at once. Useful in ViewPatterns. +tSplit :: QualifiedWithTag t a -> (Domain, a) +tSplit (tUntagged -> q) = (q.qDomain, q.qUnqualified) -- | A type representing a 'Qualified' value where the domain is guaranteed to -- be remote. @@ -109,22 +112,37 @@ toRemoteUnsafe d a = qTagUnsafe $ Qualified a d -- be local. type Local = QualifiedWithTag 'QLocal --- | Convert a 'Domain' and an @a@ to a 'Local' value. This is only safe if we --- already know that the domain is local. -toLocalUnsafe :: Domain -> a -> Local a -toLocalUnsafe d a = qTagUnsafe $ Qualified a d - -- | Convert an unqualified value to a qualified one, with the same tag as the -- given tagged qualified value. qualifyAs :: QualifiedWithTag t x -> a -> QualifiedWithTag t a qualifyAs = ($>) +data RelativeTo a + = Local (Local a) + | Remote (Remote a) + foldQualified :: Local x -> (Local a -> b) -> (Remote a -> b) -> Qualified a -> b -foldQualified loc f g q +foldQualified loc kLocal kRemote q = + case q `relativeTo` loc of + Local l -> kLocal l + Remote r -> kRemote r + +relativeTo :: Qualified a -> Local loc -> RelativeTo a +relativeTo q loc | tDomain loc == qDomain q = - f (qTagUnsafe q) + Local (qTagUnsafe q) | otherwise = - g (qTagUnsafe q) + Remote (qTagUnsafe q) + +isLocal :: Local x -> Qualified a -> Bool +isLocal loc q = case q `relativeTo` loc of + Local _ -> True + Remote _ -> False + +-- | Convert a 'Domain' and an @a@ to a 'Local' value. This is only safe if we +-- already know that the domain is local. +toLocalUnsafe :: Domain -> a -> Local a +toLocalUnsafe d a = qTagUnsafe $ Qualified a d -- Partition a collection of qualified values into locals and remotes. -- @@ -159,9 +177,6 @@ bucketRemote = . indexQualified . fmap tUntagged -isLocal :: Local x -> Qualified a -> Bool -isLocal loc = foldQualified loc (const True) (const False) - ---------------------------------------------------------------------- deprecatedSchema :: (S.HasDeprecated doc (Maybe Bool), S.HasDescription doc (Maybe Text)) => Text -> ValueSchema doc a -> ValueSchema doc a diff --git a/libs/types-common/src/Data/Range.hs b/libs/types-common/src/Data/Range.hs index 73cdda9fea7..b2dd1b60332 100644 --- a/libs/types-common/src/Data/Range.hs +++ b/libs/types-common/src/Data/Range.hs @@ -26,6 +26,7 @@ module Data.Range ( Range, toRange, + mapRange, Within, Bounds (..), checked, @@ -98,7 +99,10 @@ import Test.QuickCheck qualified as QC newtype Range (n :: Nat) (m :: Nat) a = Range { fromRange :: a } - deriving (Eq, Ord, Show, Functor) + deriving (Eq, Ord, Show) + +mapRange :: forall (n :: Nat) (m :: Nat) a b. (a -> b) -> Range n m [a] -> Range n m [b] +mapRange f (Range as) = Range (f `map` as) toRange :: (n <= x, x <= m, KnownNat x, Num a) => Proxy x -> Range n m a toRange = Range . fromIntegral . natVal @@ -221,20 +225,20 @@ instance (ToParamSchema a, KnownNat n, KnownNat m) => ToParamSchema (Range n m [ instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m String) where toParamSchema _ = toParamSchema (Proxy @String) - & S.maxLength ?~ fromKnownNat (Proxy @n) - & S.minLength ?~ fromKnownNat (Proxy @m) + & S.minLength ?~ fromKnownNat (Proxy @n) + & S.maxLength ?~ fromKnownNat (Proxy @m) instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m T.Text) where toParamSchema _ = toParamSchema (Proxy @T.Text) - & S.maxLength ?~ fromKnownNat (Proxy @n) - & S.minLength ?~ fromKnownNat (Proxy @m) + & S.minLength ?~ fromKnownNat (Proxy @n) + & S.maxLength ?~ fromKnownNat (Proxy @m) instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m TL.Text) where toParamSchema _ = toParamSchema (Proxy @TL.Text) - & S.maxLength ?~ fromKnownNat (Proxy @n) - & S.minLength ?~ fromKnownNat (Proxy @m) + & S.minLength ?~ fromKnownNat (Proxy @n) + & S.maxLength ?~ fromKnownNat (Proxy @m) instance (KnownNat n, S.ToSchema a, KnownNat m) => S.ToSchema (Range n m a) where declareNamedSchema _ = diff --git a/libs/types-common/src/Data/Text/Ascii.hs b/libs/types-common/src/Data/Text/Ascii.hs index aed072030c0..2299facd5c7 100644 --- a/libs/types-common/src/Data/Text/Ascii.hs +++ b/libs/types-common/src/Data/Text/Ascii.hs @@ -38,7 +38,6 @@ module Data.Text.Ascii -- * Standard Characters Standard (..), Ascii, - validateStandard, -- * Printable Characters Printable (..), @@ -67,10 +66,6 @@ module Data.Text.Ascii encodeBase16, decodeBase16, - -- * Safe Widening - widen, - widenChar, - -- * Unsafe Construction unsafeFromText, unsafeFromByteString, @@ -80,6 +75,7 @@ where import Cassandra hiding (Ascii) import Data.Aeson (FromJSON (..), FromJSONKey, ToJSON (..), ToJSONKey) import Data.Attoparsec.ByteString (Parser) +import Data.Bifunctor (first) import Data.ByteString.Base16 qualified as B16 import Data.ByteString.Base64 qualified as B64 import Data.ByteString.Base64.URL qualified as B64Url @@ -104,11 +100,9 @@ newtype AsciiText c = AsciiText {toText :: Text} Monoid, NFData, ToByteString, - FromJSONKey, ToJSONKey, Hashable, - ToHttpApiData, - FromHttpApiData + ToHttpApiData ) newtype AsciiChar c = AsciiChar {toChar :: Char} @@ -141,6 +135,9 @@ class AsciiChars c where instance (AsciiChars c) => FromByteString (AsciiText c) where parser = parseBytes validate +instance (AsciiChars c) => FromHttpApiData (AsciiText c) where + parseUrlPiece = first Text.pack . validate + -- | Note: 'fromString' is a partial function that will 'error' when given -- a string containing characters not in the set @c@. It is only intended to be used -- via the @OverloadedStrings@ extension, i.e. for known ASCII string literals. @@ -156,6 +153,8 @@ instance (AsciiChars c) => ToJSON (AsciiText c) where instance (AsciiChars c) => FromJSON (AsciiText c) where parseJSON = schemaParseJSON +instance (FromJSON (AsciiText c)) => FromJSONKey (AsciiText c) + instance (Typeable c, AsciiChars c) => S.ToSchema (AsciiText c) where declareNamedSchema = schemaToSwagger @@ -194,9 +193,6 @@ instance AsciiChars Standard where contains Standard = isAscii {-# INLINE contains #-} -validateStandard :: Text -> Either String Ascii -validateStandard = validate - -------------------------------------------------------------------------------- -- Printable @@ -360,19 +356,6 @@ encodeBase16 = unsafeFromByteString . B16.encode decodeBase16 :: AsciiBase16 -> Maybe ByteString decodeBase16 t = either (const Nothing) Just (B16.decode (toByteString' t)) --------------------------------------------------------------------------------- --- Safe Widening - --- | Safely widen an ASCII text into another ASCII text with a larger --- character set. -widen :: (Subset c c' ~ 'True) => AsciiText c -> AsciiText c' -widen (AsciiText t) = AsciiText t - --- | Safely widen an ASCII character into another ASCII character with a larger --- character set. -widenChar :: (Subset c c' ~ 'True) => AsciiChar c -> AsciiChar c' -widenChar (AsciiChar t) = AsciiChar t - -------------------------------------------------------------------------------- -- Unsafe Construction diff --git a/libs/types-common/src/Data/UUID/Tagged.hs b/libs/types-common/src/Data/UUID/Tagged.hs index fa6eb11ce5f..14aceb1f5d2 100644 --- a/libs/types-common/src/Data/UUID/Tagged.hs +++ b/libs/types-common/src/Data/UUID/Tagged.hs @@ -22,17 +22,14 @@ module Data.UUID.Tagged V5, Version (..), version, - variant, addv4, unpack, - create, mk, ) where import Data.Bits import Data.UUID qualified as D -import Data.UUID.V4 qualified as D4 import Imports -- | Versioned UUID. @@ -68,10 +65,6 @@ mk u = UUID $ (retainVariant 2 x2) x3 --- | Create a fresh UUIDv4. -create :: IO (UUID V4) -create = UUID <$> D4.nextRandom - -- | Extract the 'D.UUID' from a versioned UUID. unpack :: UUID v -> D.UUID unpack (UUID x) = x @@ -100,12 +93,6 @@ version u = let (_, x, _, _) = D.toWords u in (x .&. 0x0000F000) `shiftR` 12 --- | Tell the variant of a 'D.UUID' value. -variant :: D.UUID -> Word32 -variant u = - let (_, _, x, _) = D.toWords u - in (x .&. 0xC0000000) `shiftR` 30 - -- Internal: retainVersion :: Word32 -> Word32 -> Word32 diff --git a/libs/types-common/src/Util/Logging.hs b/libs/types-common/src/Util/Logging.hs index 318785c7578..8a242a3d664 100644 --- a/libs/types-common/src/Util/Logging.hs +++ b/libs/types-common/src/Util/Logging.hs @@ -25,6 +25,7 @@ import Data.Text.Encoding (encodeUtf8) import Imports import System.Logger.Class qualified as Log import System.Logger.Message (Msg) +import Text.Email.Parser sha256String :: Text -> Text sha256String t = @@ -48,3 +49,7 @@ logUser uid = Log.field "user" (T.pack . show $ uid) logTeam :: TeamId -> (Msg -> Msg) logTeam tid = Log.field "team" (T.pack . show $ tid) + +logEmail :: EmailAddress -> (Msg -> Msg) +logEmail email = + Log.field "email_sha256" (sha256String . T.pack . show $ email) diff --git a/libs/types-common/src/Util/Options.hs b/libs/types-common/src/Util/Options.hs index 2d46e74097a..9fa6117aede 100644 --- a/libs/types-common/src/Util/Options.hs +++ b/libs/types-common/src/Util/Options.hs @@ -1,6 +1,7 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} -- This file is part of the Wire Server implementation. @@ -37,7 +38,6 @@ import Imports import Options.Applicative import Options.Applicative.Types import URI.ByteString -import Util.Options.Common data AWSEndpoint = AWSEndpoint { _awsHost :: !ByteString, @@ -148,9 +148,28 @@ getOptions desc mp defaultPath = do parseAWSEndpoint :: ReadM AWSEndpoint parseAWSEndpoint = readerAsk >>= maybe (error "Could not parse AWS endpoint") pure . fromByteString . fromString -discoUrlParser :: Parser Text -discoUrlParser = - textOption $ - long "disco-url" - <> metavar "URL" - <> help "klabautermann url" +data PasswordHashingOptions + = PasswordHashingArgon2id Argon2idOptions + | PasswordHashingScrypt + deriving (Show, Generic) + +data Argon2idOptions = Argon2idOptions + { iterations :: !Word32, + memory :: !Word32, + parallelism :: !Word32 + } + deriving (Show, Generic) + +instance FromJSON PasswordHashingOptions where + parseJSON = + withObject "PasswordHashingOptions" $ \obj -> do + algo :: String <- obj .: "algorithm" + case algo of + "argon2id" -> do + iterations <- obj .: "iterations" + memory <- obj .: "memory" + parallelism <- obj .: "parallelism" + pure . PasswordHashingArgon2id $ Argon2idOptions {..} + "scrypt" -> + pure PasswordHashingScrypt + x -> fail $ "Unknown password hashing algorithm: " <> x diff --git a/libs/types-common/src/Util/Options/Common.hs b/libs/types-common/src/Util/Options/Common.hs index 14b997bee7e..e7b3eaf3dac 100644 --- a/libs/types-common/src/Util/Options/Common.hs +++ b/libs/types-common/src/Util/Options/Common.hs @@ -22,10 +22,7 @@ module Util.Options.Common where import Cassandra.Helpers (toOptionFieldName) -import Data.ByteString.Char8 qualified as C -import Data.Text qualified as T import Imports hiding (reader) -import Options.Applicative import System.Posix.Env qualified as Posix optOrEnv :: (a -> b) -> Maybe a -> (String -> b) -> String -> IO b @@ -37,9 +34,3 @@ optOrEnvSafe :: (a -> b) -> Maybe a -> (String -> b) -> String -> IO (Maybe b) optOrEnvSafe getter conf reader var = case conf of Nothing -> fmap reader <$> Posix.getEnv var Just c -> pure $ Just (getter c) - -bytesOption :: Mod OptionFields String -> Parser ByteString -bytesOption = fmap C.pack . strOption - -textOption :: Mod OptionFields String -> Parser Text -textOption = fmap T.pack . strOption diff --git a/libs/types-common/src/Util/SuffixNamer.hs b/libs/types-common/src/Util/SuffixNamer.hs new file mode 100644 index 00000000000..8fd3ddbaf91 --- /dev/null +++ b/libs/types-common/src/Util/SuffixNamer.hs @@ -0,0 +1,10 @@ +module Util.SuffixNamer where + +import Control.Lens +import Imports +import Language.Haskell.TH (mkName, nameBase) + +suffixNamer :: FieldNamer +suffixNamer _ _ n = + let name = nameBase n + in [TopName (mkName (name <> "Lens"))] diff --git a/libs/types-common/src/Util/Timeout.hs b/libs/types-common/src/Util/Timeout.hs new file mode 100644 index 00000000000..35dcde3a52f --- /dev/null +++ b/libs/types-common/src/Util/Timeout.hs @@ -0,0 +1,36 @@ +module Util.Timeout + ( Timeout (..), + module Data.Time.Clock, + ) +where + +import Data.Aeson +import Data.Aeson.Types +import Data.Scientific +import Data.Time.Clock +import Imports +import Test.QuickCheck (Arbitrary (arbitrary), choose) + +newtype Timeout = Timeout + { timeoutDiff :: NominalDiffTime + } + deriving newtype (Eq, Enum, Ord, Num, Real, Fractional, RealFrac, Show) + +instance Arbitrary Timeout where + arbitrary = Timeout . fromIntegral <$> choose (60 :: Int, 10 * 24 * 3600) + +instance Read Timeout where + readsPrec i s = + case readsPrec i s of + [(x :: Int, s')] -> [(Timeout (fromIntegral x), s')] + _ -> [] + +instance FromJSON Timeout where + parseJSON (Number n) = + let defaultV = 3600 + bounded = toBoundedInteger n :: Maybe Int64 + in pure $ + Timeout $ + fromIntegral @Int $ + maybe defaultV fromIntegral bounded + parseJSON v = typeMismatch "activationTimeout" v diff --git a/libs/types-common/test/Main.hs b/libs/types-common/test/Main.hs index 045e49e5e7e..4814492dfd0 100644 --- a/libs/types-common/test/Main.hs +++ b/libs/types-common/test/Main.hs @@ -21,6 +21,7 @@ module Main where import Imports +import Test.Data.Mailbox qualified as Mailbox import Test.Data.PEMKeys qualified as PEMKeys import Test.Domain qualified as Domain import Test.Handle qualified as Handle @@ -39,5 +40,6 @@ main = Domain.tests, Handle.tests, Qualified.tests, - PEMKeys.tests + PEMKeys.tests, + Mailbox.tests ] diff --git a/libs/types-common/test/Test/Data/Mailbox.hs b/libs/types-common/test/Test/Data/Mailbox.hs new file mode 100644 index 00000000000..caa01307f34 --- /dev/null +++ b/libs/types-common/test/Test/Data/Mailbox.hs @@ -0,0 +1,69 @@ +module Test.Data.Mailbox (tests) where + +import Data.ByteString.UTF8 qualified as UTF8BS +import Data.Mailbox +import Imports +import Test.Tasty +import Test.Tasty.HUnit +import Text.Email.Parser + +validAddresses :: [(ByteString, Mailbox)] +validAddresses = + [ ("john@doe.example", Mailbox Nothing $ unsafeEmailAddress "john" "doe.example"), + ("", Mailbox Nothing $ unsafeEmailAddress "john" "doe.example"), + ("John Doe", Mailbox (Just ["John", "Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("John Doe ", Mailbox (Just ["John", "Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("John Q. Doe ", Mailbox (Just ["John", "Q", ".", "Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John Doe\" ", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John Doe\" (My Best Friend) ", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John@Doe.Example\" (My Friend @ Doe) ", Mailbox (Just ["John@Doe.Example"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John Doe\" (My Best Friend) ", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John \\\"The J\\\" Doe\" ", Mailbox (Just ["John \"The J\" Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John not \\tab\" ", Mailbox (Just ["John not tab"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John [Quoted Special]\" ", Mailbox (Just ["John [Quoted Special]"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John \" ", Mailbox (Just ["John "]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John \r\n NewLine\" ", Mailbox (Just ["John \r\n NewLine"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John Doe\" <(local comment)john(local trailing comment)@doe.example>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John Doe\" <(local comment)\"john\"(local trailing comment)@doe.example>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "\"john\"" "doe.example"), + ("\"John Doe\" <\"john@funkylocal\"@doe.example>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "\"john@funkylocal\"" "doe.example"), + ("\"John Doe\" (trailing comments)", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John Doe\" ", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "[funky@domain.example]"), + ("\"John Doe\" ", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "[doe.example]"), + -- This is wrong, but its how the `email-validate` library does it + ("\"John Doe\" <\"john (not comment)\"@doe.example>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "\"john(notcomment)\"" "doe.example") + ] + +invalidAddresses :: [ByteString] +invalidAddresses = + [ "john", + "john@", + "@doe.example", + "\"john@doe.example\"", + "(john@doe.example)", + "\"John UnendingQuote ", + "John [Unquoted Special] ", + " ", + "\"John \n NoCR\" ", + "\"John \r NoLF\" " + ] + +tests :: TestTree +tests = + testGroup "Mailbox" $ + [ testGroup "valid addressses" $ + map + ( \(addr, expected) -> + testCase (UTF8BS.toString addr) $ + Right expected @=? parseMailbox addr + ) + validAddresses, + testGroup "invalid addresses" $ + map + ( \addr -> + testCase (UTF8BS.toString addr) $ + case parseMailbox addr of + Left _ -> pure () + Right mb -> assertFailure $ "Expected to fail parising, but got: " <> show mb + ) + invalidAddresses + ] diff --git a/libs/types-common/types-common.cabal b/libs/types-common/types-common.cabal index 5fb1c0ca72c..369659432cc 100644 --- a/libs/types-common/types-common.cabal +++ b/libs/types-common/types-common.cabal @@ -12,6 +12,7 @@ license-file: LICENSE build-type: Simple library + -- cabal-fmt: expand src exposed-modules: Data.Code Data.CommaSeparatedList @@ -19,10 +20,12 @@ library Data.Domain Data.ETag Data.Handle + Data.HavePendingInvitations Data.Id Data.Json.Util Data.LegalHold Data.List1 + Data.Mailbox Data.Misc Data.Nonce Data.PEMKeys @@ -37,7 +40,9 @@ library Util.Logging Util.Options Util.Options.Common + Util.SuffixNamer Util.Test + Util.Timeout Wire.Arbitrary other-modules: Paths_types_common @@ -106,10 +111,10 @@ library , cryptohash-sha1 >=0.11.7.2 , crypton >=0.26 , currency-codes >=3.0.0.1 + , email-validate , generic-random >=1.4.0.0 , hashable >=1.2 , http-api-data - , http-types , imports , iproute >=1.5 , iso3166-country-codes >=0.20140203.8 @@ -125,10 +130,12 @@ library , quickcheck-instances >=0.3.16 , random >=1.1 , schema-profunctor + , scientific , servant-server , tagged >=0.8 , tasty >=0.11 , tasty-hunit + , template-haskell , text >=0.11 , time >=1.6 , time-locale-compat >=0.1 @@ -145,8 +152,12 @@ library test-suite tests type: exitcode-stdio-1.0 main-is: Main.hs + + -- cabal-fmt: expand test other-modules: + Main Paths_types_common + Test.Data.Mailbox Test.Data.PEMKeys Test.Domain Test.Handle @@ -208,6 +219,7 @@ test-suite tests , bytestring , bytestring-conversion , cereal + , email-validate , imports , protobuf , string-conversions @@ -218,6 +230,7 @@ test-suite tests , time , types-common , unordered-containers + , utf8-string , uuid default-language: GHC2021 diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Error.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Error.hs index aea3d8b41f3..09abc767e1a 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Error.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Error.hs @@ -23,7 +23,6 @@ module Network.Wai.Utilities.Error ErrorData (..), mkError, (!>>), - byteStringError, ) where @@ -31,7 +30,6 @@ import Control.Error import Data.Aeson hiding (Error) import Data.Aeson.Types (Pair) import Data.Domain -import Data.Text.Lazy.Encoding (decodeUtf8) import Imports import Network.HTTP.Types @@ -69,10 +67,6 @@ instance FromJSON ErrorData where <$> o .: "domain" <*> o .: "path" --- | Assumes UTF-8 encoding. -byteStringError :: Status -> LByteString -> LByteString -> Error -byteStringError s l m = mkError s (decodeUtf8 l) (decodeUtf8 m) - instance ToJSON Error where toJSON (Error c l m md inner) = object $ diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Request.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Request.hs index 7da25d4449b..484c1b34643 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Request.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Request.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -Wno-orphans #-} -- This file is part of the Wire Server implementation. @@ -23,7 +22,6 @@ module Network.Wai.Utilities.Request where import Control.Error -import Control.Monad.Catch (MonadThrow, throwM) import Data.Aeson import Data.ByteString qualified as B import Data.ByteString.Lazy qualified as Lazy @@ -32,10 +30,7 @@ import Data.Text.Lazy qualified as Text import Imports import Network.HTTP.Types import Network.Wai -import Network.Wai.Predicate import Network.Wai.Predicate.Request -import Network.Wai.Utilities.Error qualified as Wai -import Network.Wai.Utilities.ZAuth ((.&>)) import Pipes import Pipes.Prelude qualified as P @@ -54,52 +49,21 @@ parseBody :: ExceptT LText m a parseBody r = readBody r >>= hoistEither . fmapL Text.pack . eitherDecode' -parseBody' :: (FromJSON a, MonadIO m, MonadThrow m) => JsonRequest a -> m a -parseBody' r = either thrw pure =<< runExceptT (parseBody r) - where - thrw msg = throwM $ Wai.mkError status400 "bad-request" msg - -parseOptionalBody :: - (MonadIO m, FromJSON a) => - OptionalJsonRequest a -> - ExceptT LText m (Maybe a) -parseOptionalBody r = - hoistEither . fmapL Text.pack . traverse eitherDecode' . nonEmptyBody =<< readBody r - where - nonEmptyBody "" = Nothing - nonEmptyBody ne = Just ne - lookupRequestId :: HeaderName -> Request -> Maybe ByteString lookupRequestId reqIdHeaderName = lookup reqIdHeaderName . requestHeaders getRequestId :: HeaderName -> Request -> RequestId getRequestId reqIdHeaderName req = - RequestId $ fromMaybe "N/A" $ lookupRequestId reqIdHeaderName req + RequestId $ fromMaybe defRequestId $ lookupRequestId reqIdHeaderName req ---------------------------------------------------------------------------- -- Typed JSON 'Request' newtype JsonRequest body = JsonRequest {fromJsonRequest :: Request} -jsonRequest :: - forall body r. - (HasRequest r, HasHeaders r) => - Predicate r Error (JsonRequest body) -jsonRequest = - contentType "application" "json" - .&> (pure . JsonRequest . getRequest) - newtype OptionalJsonRequest body = OptionalJsonRequest {fromOptionalJsonRequest :: Request} -optionalJsonRequest :: - forall body r. - (HasRequest r, HasHeaders r) => - Predicate r Error (OptionalJsonRequest body) -optionalJsonRequest = - opt (contentType "application" "json") - .&> (pure . OptionalJsonRequest . getRequest) - ---------------------------------------------------------------------------- -- Instances diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Response.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Response.hs index ce838ff5463..bb27d08fdee 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Response.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Response.hs @@ -30,9 +30,6 @@ import Network.Wai.Utilities.Error empty :: Response empty = plain "" -noContent :: Response -noContent = empty & setStatus status204 - plain :: Lazy.ByteString -> Response plain = responseLBS status200 [plainContent] @@ -45,11 +42,8 @@ json = responseLBS status200 [jsonContent] . encode jsonContent :: Header jsonContent = (hContentType, "application/json") -errorRs :: Status -> LText -> LText -> Response -errorRs s l m = errorRs' (mkError s l m) - -errorRs' :: Error -> Response -errorRs' e = setStatus (code e) (json e) +errorRs :: Error -> Response +errorRs e = setStatus (code e) (json e) addHeader :: HeaderName -> ByteString -> Response -> Response addHeader k v (ResponseFile s h f ff) = ResponseFile s ((k, v) : h) f ff diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs index 55cc81dfe49..20f8fc9b934 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs @@ -42,7 +42,6 @@ module Network.Wai.Utilities.Server logError, logError', logErrorMsg, - restrict, flushRequestBody, -- * Constants @@ -61,11 +60,11 @@ import Data.ByteString.Builder import Data.ByteString.Char8 qualified as C import Data.ByteString.Lazy qualified as LBS import Data.Domain (domainText) +import Data.Id import Data.Metrics.GC (spawnGCMetricsCollector) import Data.Streaming.Zlib (ZlibException (..)) import Data.Text.Encoding qualified as Text import Data.Text.Encoding.Error (lenientDecode) -import Data.Text.Lazy qualified as LT import Data.Text.Lazy.Encoding qualified as LT import Data.UUID qualified as UUID import Data.UUID.V4 qualified as UUID @@ -170,7 +169,7 @@ compile routes = Route.prepare (Route.renderer predicateError >> routes) r = reasonStr <$> reason e t = message e in case catMaybes [l, s, r] of - [] -> maybe "N/A" (LT.decodeUtf8With lenientDecode . LBS.fromStrict) t + [] -> maybe defRequestId (LT.decodeUtf8With lenientDecode . LBS.fromStrict) t bs -> LT.decodeUtf8With lenientDecode . toLazyByteString $ mconcat bs <> messageStr t labelStr [] = Nothing labelStr ls = @@ -186,7 +185,7 @@ compile routes = Route.prepare (Route.renderer predicateError >> routes) messageStr Nothing = mempty route :: (MonadIO m) => Tree (App m) -> Request -> Continue IO -> m ResponseReceived -route rt rq k = Route.routeWith (Route.Config $ errorRs' noEndpoint) rt rq (liftIO . k) +route rt rq k = Route.routeWith (Route.Config $ errorRs noEndpoint) rt rq (liftIO . k) where noEndpoint = Wai.mkError status404 "no-endpoint" "The requested endpoint does not exist" {-# INLINEABLE route #-} @@ -253,7 +252,7 @@ errorHandlers = ThreadKilled -> throwIO x _ -> pure . Left $ - Wai.mkError status500 "server-error" ("Server Error. " <> LT.pack (displayException x)), + Wai.mkError status500 "server-error" "Server Error", Handler $ \(_ :: InvalidRequest) -> pure . Left $ Wai.mkError status400 "client-error" "Invalid Request", @@ -267,9 +266,9 @@ errorHandlers = ZlibException _ -> pure . Left $ Wai.mkError status500 "server-error" "Server Error", - Handler $ \(e :: SomeException) -> + Handler $ \(_ :: SomeException) -> pure . Left $ - Wai.mkError status500 "server-error" ("Server Error. " <> LT.pack (displayException e)) + Wai.mkError status500 "server-error" "Server Error" ] {-# INLINE errorHandlers #-} @@ -313,7 +312,7 @@ heavyDebugLogging sanitizeReq lvl lgr reqIdHeaderName app = \req cont -> do logMostlyEverything req bdy resp = Log.debug lgr logMsg where logMsg = - field "request" (fromMaybe "N/A" $ lookupRequestId reqIdHeaderName req) + field "request" (fromMaybe defRequestId $ lookupRequestId reqIdHeaderName req) . field "request_details" (show req) . field "request_body" bdy . field "response_status" (show $ responseStatus resp) @@ -352,7 +351,7 @@ rethrow5xx getRequestId logger app req k = app req k' let logMsg = field "canoncalpath" (show $ pathInfo req) . field "rawpath" (rawPathInfo req) - . field "request" (fromMaybe "N/A" $ getRequestId req) + . field "request" (fromMaybe defRequestId $ getRequestId req) . msg (val "ResponseRaw - cannot collect metrics or log info on errors") Log.log logger Log.Debug logMsg k resp @@ -438,7 +437,7 @@ logError' g mr e = liftIO $ doLog g (logErrorMsgWithRequest mr e) logJSONResponse :: (MonadIO m) => Logger -> Maybe ByteString -> JSONResponse -> m () logJSONResponse g mReqId e = do - let r = fromMaybe "N/A" mReqId + let r = fromMaybe defRequestId mReqId liftIO $ doLog g $ field "request" r @@ -464,28 +463,12 @@ logErrorMsg (Wai.Error c l m md inner) = logErrorMsgWithRequest :: Maybe ByteString -> Wai.Error -> Msg -> Msg logErrorMsgWithRequest mr e = - field "request" (fromMaybe "N/A" mr) . logErrorMsg e + field "request" (fromMaybe defRequestId mr) . logErrorMsg e runHandlers :: SomeException -> [Handler IO a] -> IO a runHandlers e [] = throwIO e runHandlers e (Handler h : hs) = maybe (runHandlers e hs) h (fromException e) -restrict :: Int -> Int -> Predicate r P.Error Int -> Predicate r P.Error Int -restrict l u = fmap $ \x -> - x >>= \v -> - if v >= l && v <= u - then x - else Fail (setMessage (emsg v) . setReason TypeError $ e400) - where - emsg v = - LBS.toStrict . toLazyByteString $ - byteString "outside range [" - <> intDec l - <> byteString ", " - <> intDec u - <> byteString "]: " - <> intDec v - flushRequestBody :: Request -> IO () flushRequestBody req = do bs <- getRequestBodyChunk req diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/ZAuth.hs b/libs/wai-utilities/src/Network/Wai/Utilities/ZAuth.hs index 5733203a0bd..a96a16f2032 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/ZAuth.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/ZAuth.hs @@ -19,25 +19,14 @@ module Network.Wai.Utilities.ZAuth ( ZAuthType (..), - zauthType, - zauth, - zauthUserId, - zauthConnId, - zauthBotId, - zauthConvId, - zauthProviderId, (<&.), (.&>), ) where import Data.ByteString.Conversion -import Data.Id import Imports -import Network.HTTP.Types.Header -import Network.HTTP.Types.Status import Network.Wai.Predicate -import Network.Wai.Predicate.Request -- ZAuth headers -------------------------------------------------------------- @@ -65,40 +54,6 @@ instance FromByteString ZAuthType where "provider" -> pure ZAuthProvider _ -> fail $ "Invalid ZAuth type: " ++ show t --- | A token type is present if the request was authenticated. -zauthType :: (HasHeaders r) => Predicate r Error ZAuthType -zauthType = zheader "Z-Type" - --- | Require a specific token type to be used. -zauth :: (HasHeaders r) => ZAuthType -> Predicate r Error () -zauth t = do - r <- zauthType - pure $ case r of - Okay _ z | z == t -> Okay 0 () - _ -> Fail accessDenied - --- | A zauth user ID is present if 'zauthType' is either 'ZAuthAccess' --- or 'ZAuthUser'. -zauthUserId :: (HasHeaders r) => Predicate r Error UserId -zauthUserId = zheader "Z-User" - --- | A zauth connection ID is present if 'zauthType' is 'ZAuthAccess'. -zauthConnId :: (HasHeaders r) => Predicate r Error ConnId -zauthConnId = zheader "Z-Connection" - --- | A zauth bot ID is present if 'zauthType' is 'ZAuthBot'. -zauthBotId :: (HasHeaders r) => Predicate r Error BotId -zauthBotId = zheader "Z-Bot" - --- | A zauth conversation ID is present if 'zauthType' is 'ZAuthBot'. -zauthConvId :: (HasHeaders r) => Predicate r Error ConvId -zauthConvId = zheader "Z-Conversation" - --- | A provider ID is present if 'zauthType' is either 'ZAuthBot' --- or 'ZAuthProvider'. -zauthProviderId :: (HasHeaders r) => Predicate r Error ProviderId -zauthProviderId = zheader "Z-Provider" - -- Extra Predicate Combinators ------------------------------------------------ -- Variations of '.&.' that keep only the result of the left or right @@ -114,14 +69,3 @@ infixr 3 .&> (.&>) :: Predicate a f t -> Predicate a f t' -> Predicate a f t' (.&>) a b = fmap (fmap tl) (a .&. b) - --- Internal ------------------------------------------------------------------- - --- | Missing or invalid zauth-related headers due to a misconfiguration --- between the zauth ACL and / or API handlers should yield an opaque 403 --- error, in order not to leak such details to clients on public API endpoints. -zheader :: (HasHeaders r, FromByteString a) => HeaderName -> Predicate r Error a -zheader = fmap (result (Fail . const accessDenied) Okay) . header - -accessDenied :: Error -accessDenied = setMessage "Access denied" (err status403) diff --git a/libs/wire-api-federation/default.nix b/libs/wire-api-federation/default.nix index b287e8fdc21..822dcbfa9dd 100644 --- a/libs/wire-api-federation/default.nix +++ b/libs/wire-api-federation/default.nix @@ -41,7 +41,6 @@ , text , time , transformers -, transitive-anns , types-common , uuid , wai-utilities @@ -83,7 +82,6 @@ mkDerivation { text time transformers - transitive-anns types-common wai-utilities wire-api diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API.hs b/libs/wire-api-federation/src/Wire/API/Federation/API.hs index bf33723b172..73dbbeded3b 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API.hs @@ -29,8 +29,7 @@ module Wire.API.Federation.API fedQueueClient, sendBundle, fedClientIn, - unsafeFedClientIn, - module Wire.API.MakesFederatedCall, + module X, -- * Re-exports Component (..), @@ -49,6 +48,7 @@ import Imports import Network.AMQP import Servant.Client import Servant.Client.Core +import Wire.API.Component as X import Wire.API.Federation.API.Brig import Wire.API.Federation.API.Cargohold import Wire.API.Federation.API.Galley @@ -59,7 +59,6 @@ import Wire.API.Federation.Component import Wire.API.Federation.Endpoint import Wire.API.Federation.HasNotificationEndpoint import Wire.API.Federation.Version -import Wire.API.MakesFederatedCall import Wire.API.Routes.Named -- Note: this type family being injective means that in most cases there is no need @@ -109,16 +108,9 @@ instance FederationMonad FederatorClient where fedClientWithProxy _ = clientIn -- | Return a client for a named endpoint. --- --- This function introduces an 'AddAnnotation' constraint, which is --- automatically solved by the @transitive-anns@ plugin, and pushes the --- resulting information around in a side-channel. See the documentation at --- 'Wire.API.MakesFederatedCall.exposeAnnotations' for a better understanding --- of the information flow here. fedClient :: - forall (comp :: Component) name fedM (showcomp :: Symbol) api x. - ( AddAnnotation 'Remote showcomp (FedPath name) x, - showcomp ~ ShowComponent comp, + forall (comp :: Component) name fedM (showcomp :: Symbol) api. + ( showcomp ~ ShowComponent comp, HasFedEndpoint comp api name, HasClient (fedM comp) api, KnownComponent comp, @@ -165,11 +157,3 @@ fedQueueClient :: Payload tag -> FedQueueClient c () fedQueueClient payload = sendBundle =<< makeBundle @tag payload - --- | Like 'fedClientIn', but doesn't propagate a 'CallsFed' constraint. Intended --- to be used in test situations only. -unsafeFedClientIn :: - forall (comp :: Component) (name :: Symbol) m api. - (HasUnsafeFedEndpoint comp api name, HasClient m api) => - Client m api -unsafeFedClientIn = clientIn (Proxy @api) (Proxy @m) diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs index 77eaa112cad..7002ac22090 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs @@ -44,11 +44,14 @@ import Wire.API.Federation.API.Common import Wire.API.Federation.API.Galley.Notifications as Notifications import Wire.API.Federation.Endpoint import Wire.API.Federation.Version +import Wire.API.MLS.Keys import Wire.API.MLS.SubConversation -import Wire.API.MakesFederatedCall import Wire.API.Message +import Wire.API.Routes.Named import Wire.API.Routes.Public.Galley.Messaging import Wire.API.Routes.SpecialiseToVersion +import Wire.API.Routes.Version qualified as ClientAPI +import Wire.API.Routes.Versioned qualified as ClientAPI import Wire.API.Util.Aeson (CustomEncoded (..)) import Wire.API.VersionInfo import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -66,59 +69,45 @@ type GalleyApi = FedEndpoint "on-conversation-created" (ConversationCreated ConvId) EmptyResponse -- This endpoint is called the first time a user from this backend is -- added to a remote conversation. - :<|> FedEndpoint "get-conversations" GetConversationsRequest GetConversationsResponse + :<|> Named + "get-conversations@v1" + ( UnnamedFedEndpointWithMods + '[Until 'V2] + "get-conversations" + GetConversationsRequest + GetConversationsResponse + ) :<|> FedEndpointWithMods - '[ MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Brig "get-users-by-ids", - MakesFederatedCall 'Brig "api-version" - ] + '[From 'V2] + "get-conversations" + GetConversationsRequest + GetConversationsResponseV2 + :<|> FedEndpoint "leave-conversation" LeaveConversationRequest LeaveConversationResponse -- used by a remote backend to send a message to a conversation owned by -- this backend - :<|> FedEndpointWithMods - '[ MakesFederatedCall 'Galley "on-message-sent", - MakesFederatedCall 'Brig "get-user-clients" - ] + :<|> FedEndpoint "send-message" ProteusMessageSendRequest MessageSendResponse - :<|> FedEndpointWithMods - '[ MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Brig "get-users-by-ids", - MakesFederatedCall 'Galley "on-mls-message-sent" - ] + :<|> FedEndpoint "update-conversation" ConversationUpdateRequest ConversationUpdateResponse :<|> FedEndpoint "mls-welcome" MLSWelcomeRequest MLSWelcomeResponse - :<|> FedEndpointWithMods - '[ MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Galley "send-mls-message", - MakesFederatedCall 'Brig "get-mls-clients" - ] + :<|> FedEndpoint "send-mls-message" MLSMessageSendRequest MLSMessageResponse - :<|> FedEndpointWithMods - '[ MakesFederatedCall 'Galley "mls-welcome", - MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Galley "send-mls-commit-bundle", - MakesFederatedCall 'Brig "get-mls-clients", - MakesFederatedCall 'Brig "get-users-by-ids", - MakesFederatedCall 'Brig "api-version" - ] + :<|> FedEndpoint "send-mls-commit-bundle" MLSMessageSendRequest MLSMessageResponse :<|> FedEndpoint "query-group-info" GetGroupInfoRequest GetGroupInfoResponse :<|> FedEndpointWithMods - '[ MakesFederatedCall 'Galley "on-typing-indicator-updated" + '[ ] "update-typing-indicator" TypingDataUpdateRequest @@ -137,17 +126,24 @@ type GalleyApi = DeleteSubConversationFedRequest DeleteSubConversationResponse :<|> FedEndpointWithMods - '[ MakesFederatedCall 'Galley "on-mls-message-sent", - From 'V1 + '[ From 'V1 ] "leave-sub-conversation" LeaveSubConversationRequest LeaveSubConversationResponse + :<|> Named + "get-one2one-conversation@v1" + ( UnnamedFedEndpointWithMods + '[From 'V1, Until 'V2] + "get-one2one-conversation" + GetOne2OneConversationRequest + GetOne2OneConversationResponse + ) :<|> FedEndpointWithMods - '[From 'V1] + '[From 'V2] "get-one2one-conversation" GetOne2OneConversationRequest - GetOne2OneConversationResponse + GetOne2OneConversationResponseV2 -- All the notification endpoints that go through the queue-based -- federation client ('fedQueueClient'). :<|> GalleyNotificationAPI @@ -226,7 +222,7 @@ data RemoteConversation = RemoteConversation id :: ConvId, metadata :: ConversationMetadata, members :: RemoteConvMembers, - protocol :: Protocol + protocol :: ClientAPI.Versioned 'ClientAPI.V5 Protocol } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform RemoteConversation) @@ -234,6 +230,42 @@ data RemoteConversation = RemoteConversation instance ToSchema RemoteConversation +-- | A conversation hosted on a remote backend. This contains the same +-- information as a 'Conversation', with the exception that conversation status +-- fields (muted\/archived\/hidden) are omitted, since they are not known by the +-- remote backend. +data RemoteConversationV2 = RemoteConversationV2 + { -- | Id of the conversation, implicitly qualified with the domain of the + -- backend that created this value. + id :: ConvId, + metadata :: ConversationMetadata, + members :: RemoteConvMembers, + protocol :: Protocol + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform RemoteConversationV2) + deriving (FromJSON, ToJSON) via (CustomEncoded RemoteConversationV2) + +instance ToSchema RemoteConversationV2 + +remoteConversationFromV2 :: RemoteConversationV2 -> RemoteConversation +remoteConversationFromV2 rc = + RemoteConversation + { id = rc.id, + metadata = rc.metadata, + members = rc.members, + protocol = ClientAPI.Versioned rc.protocol + } + +remoteConversationToV2 :: RemoteConversation -> RemoteConversationV2 +remoteConversationToV2 rc = + RemoteConversationV2 + { id = rc.id, + metadata = rc.metadata, + members = rc.members, + protocol = rc.protocol.unVersioned + } + newtype GetConversationsResponse = GetConversationsResponse { convs :: [RemoteConversation] } @@ -243,6 +275,21 @@ newtype GetConversationsResponse = GetConversationsResponse instance ToSchema GetConversationsResponse +newtype GetConversationsResponseV2 = GetConversationsResponseV2 + { convs :: [RemoteConversationV2] + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform GetConversationsResponseV2) + deriving (ToJSON, FromJSON) via (CustomEncoded GetConversationsResponseV2) + +instance ToSchema GetConversationsResponseV2 + +getConversationsResponseToV2 :: GetConversationsResponse -> GetConversationsResponseV2 +getConversationsResponseToV2 res = GetConversationsResponseV2 (map remoteConversationToV2 res.convs) + +getConversationsResponseFromV2 :: GetConversationsResponseV2 -> GetConversationsResponse +getConversationsResponseFromV2 res = GetConversationsResponse (map remoteConversationFromV2 res.convs) + data GetOne2OneConversationResponse = GetOne2OneConversationOk RemoteConversation | -- | This is returned when the local backend is asked for a 1-1 conversation @@ -257,6 +304,29 @@ data GetOne2OneConversationResponse instance ToSchema GetOne2OneConversationResponse +data GetOne2OneConversationResponseV2 + = GetOne2OneConversationV2Ok RemoteMLSOne2OneConversation + | -- | This is returned when the local backend is asked for a 1-1 conversation + -- that should reside on the other backend. + GetOne2OneConversationV2BackendMismatch + | -- | This is returned when a 1-1 conversation between two unconnected users + -- is requested. + GetOne2OneConversationV2NotConnected + | GetOne2OneConversationV2MLSNotEnabled + deriving stock (Eq, Show, Generic) + deriving (ToJSON, FromJSON) via (CustomEncoded GetOne2OneConversationResponseV2) + +instance ToSchema GetOne2OneConversationResponseV2 + +data RemoteMLSOne2OneConversation = RemoteMLSOne2OneConversation + { conversation :: RemoteConversationV2, + publicKeys :: MLSKeysByPurpose MLSPublicKeys + } + deriving stock (Eq, Show, Generic) + deriving (ToJSON, FromJSON) via (CustomEncoded RemoteMLSOne2OneConversation) + +instance ToSchema RemoteMLSOne2OneConversation + -- | A record type describing a new federated conversation -- -- FUTUREWORK: Think about extracting common conversation metadata into a diff --git a/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs b/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs index 2e3f4b8d488..7cde4f2b733 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs @@ -160,9 +160,6 @@ routingKey t = "backend-notifications." <> t -- they are stored in Rabbit. type DefederationDomain = Domain -defederationQueue :: Text -defederationQueue = "delete-federation" - -- | If you ever change this function and modify -- queue parameters, know that it will start failing in the -- next release! So be prepared to write migrations. diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Client.hs b/libs/wire-api-federation/src/Wire/API/Federation/Client.hs index 98f653e6083..1a83e8c9adb 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Client.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Client.hs @@ -27,6 +27,7 @@ module Wire.API.Federation.Client runVersionedFederatorClient, runFederatorClientToCodensity, runVersionedFederatorClientToCodensity, + getNegotiatedVersion, performHTTP2Request, consumeStreamingResponseWith, streamingResponseStrictBody, @@ -117,6 +118,9 @@ instance VersionedMonad Version (FederatorClient c) where v <- asks cveVersion guard (maybe True p v) +getNegotiatedVersion :: FederatorClient c (Maybe Version) +getNegotiatedVersion = asks cveVersion + liftCodensity :: Codensity IO a -> FederatorClient c a liftCodensity = FederatorClient . lift . lift . lift @@ -134,8 +138,8 @@ withNewHttpRequest target req k = do sendReqMVar <- newEmptyMVar thread <- liftIO . async $ H2Manager.startPersistentHTTP2Connection ctx target cacheLimit sslRemoveTrailingDot tcpConnectionTimeout sendReqMVar let newConn = H2Manager.HTTP2Conn thread (putMVar sendReqMVar H2Manager.CloseConnection) sendReqMVar - H2Manager.sendRequestWithConnection newConn req $ \resp -> do - k resp <* newConn.disconnect + H2Manager.sendRequestWithConnection newConn req \resp -> + k resp `finally` newConn.disconnect performHTTP2Request :: Http2Manager -> diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Component.hs b/libs/wire-api-federation/src/Wire/API/Federation/Component.hs index aef5cc95980..ecf07fad60a 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Component.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Component.hs @@ -23,13 +23,7 @@ where import Data.Proxy import Imports -import Wire.API.MakesFederatedCall (Component (..)) - -parseComponent :: Text -> Maybe Component -parseComponent "brig" = Just Brig -parseComponent "galley" = Just Galley -parseComponent "cargohold" = Just Cargohold -parseComponent _ = Nothing +import Wire.API.Component (Component (..)) componentName :: Component -> Text componentName Brig = "brig" diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs b/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs index 910a6c2d4b1..cc565259e44 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs @@ -35,6 +35,7 @@ data Versioned v name instance {-# OVERLAPPING #-} (RenderableSymbol a) => RenderableSymbol (Versioned v a) where renderSymbol = renderSymbol @a + renderOperationId = renderOperationId @a type family FedPath (name :: k) :: Symbol diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Error.hs b/libs/wire-api-federation/src/Wire/API/Federation/Error.hs index 830a9f062fc..d13d2c23503 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Error.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Error.hs @@ -150,6 +150,8 @@ data FederationError | -- | No federator endpoint has been set, so no call to federator client can -- be made. FederationNotConfigured + | -- | Federation is disabled for the given protocol + FederationDisabledForProtocol | -- | An error occurred while invoking federator client (see -- 'FederatorClientError' for more details). FederationCallFailure FederatorClientError @@ -188,6 +190,7 @@ instance APIError FederationError where federationErrorToWai :: FederationError -> Wai.Error federationErrorToWai FederationNotImplemented = federationNotImplemented federationErrorToWai FederationNotConfigured = federationNotConfigured +federationErrorToWai FederationDisabledForProtocol = federationDisabledForProtocol federationErrorToWai (FederationCallFailure err) = federationClientErrorToWai err federationErrorToWai (FederationUnexpectedBody s) = federationUnexpectedBody s federationErrorToWai (FederationUnexpectedError t) = federationUnexpectedError t @@ -358,6 +361,13 @@ federationNotConfigured = "federation-not-enabled" "no federator configured" +federationDisabledForProtocol :: Wai.Error +federationDisabledForProtocol = + Wai.mkError + HTTP.status409 + "federation-disabled-for-protocol" + "Federation is disabled for the given protocol" + federationUnavailable :: Text -> Wai.Error federationUnavailable err = Wai.mkError diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Version.hs b/libs/wire-api-federation/src/Wire/API/Federation/Version.hs index c6f14413058..a7347fbeb5a 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Version.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Version.hs @@ -22,6 +22,7 @@ module Wire.API.Federation.Version Version (..), V0Sym0, V1Sym0, + V2Sym0, intToVersion, versionInt, versionText, @@ -32,36 +33,43 @@ module Wire.API.Federation.Version -- * VersionRange VersionUpperBound (..), VersionRange (..), - fromVersion, - toVersionExcl, allVersions, latestCommonVersion, rangeFromVersion, rangeUntilVersion, - enumVersionRange, ) where -import Control.Lens (makeLenses, (?~)) +import Control.Lens ((?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.ByteString.Char8 qualified as BS import Data.OpenApi qualified as S import Data.Schema import Data.Set qualified as Set import Data.Singletons.Base.TH import Data.Text qualified as Text import Imports +import Servant.API (ToHttpApiData (..)) -data Version = V0 | V1 +data Version = V0 | V1 | V2 deriving stock (Eq, Ord, Bounded, Enum, Show, Generic) deriving (FromJSON, ToJSON) via (Schema Version) +instance ToHttpApiData Version where + toHeader = versionByteString + toUrlPiece = versionText + versionInt :: Version -> Int versionInt V0 = 0 versionInt V1 = 1 +versionInt V2 = 2 versionText :: Version -> Text versionText = ("v" <>) . Text.pack . show . versionInt +versionByteString :: Version -> ByteString +versionByteString = ("v" <>) . BS.pack . show . versionInt + intToVersion :: Int -> Maybe Version intToVersion intV = find (\v -> versionInt v == intV) [minBound ..] @@ -69,7 +77,8 @@ instance ToSchema Version where schema = enum @Integer "Version" . mconcat $ [ element 0 V0, - element 1 V1 + element 1 V1, + element 2 V2 ] supportedVersions :: Set Version @@ -131,8 +140,6 @@ deriving instance Show VersionRange deriving instance Ord VersionRange -makeLenses ''VersionRange - instance ToSchema VersionRange where schema = object "VersionRange" $ @@ -165,12 +172,6 @@ rangeFromVersion v = VersionRange v Unbounded rangeUntilVersion :: Version -> VersionRange rangeUntilVersion v = VersionRange minBound (VersionUpperBound v) -enumVersionRange :: VersionRange -> Set Version -enumVersionRange = - Set.fromList . \case - VersionRange l Unbounded -> [l ..] - VersionRange l (VersionUpperBound u) -> init [l .. u] - -- | For a version range of a local backend and for a set of versions that a -- remote backend supports, compute the newest version supported by both. The -- remote versions are given as integers as the range of versions supported by diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GetOne2OneConversationResponse.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GetOne2OneConversationResponse.hs new file mode 100644 index 00000000000..9ea43d45966 --- /dev/null +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GetOne2OneConversationResponse.hs @@ -0,0 +1,130 @@ +module Test.Wire.API.Federation.Golden.GetOne2OneConversationResponse where + +import Data.Domain +import Data.Id +import Data.Json.Util +import Data.Qualified +import Data.Set qualified as Set +import Data.UUID qualified as UUID +import Imports +import Wire.API.Conversation +import Wire.API.Conversation.Protocol +import Wire.API.Conversation.Role +import Wire.API.Federation.API.Galley +import Wire.API.MLS.Keys +import Wire.API.Routes.Versioned qualified as ClientAPI + +testObject_GetOne2OneConversationResponseOk :: GetOne2OneConversationResponse +testObject_GetOne2OneConversationResponseOk = + GetOne2OneConversationOk remoteConversation + +testObject_GetOne2OneConversationResponseBackendMismatch :: GetOne2OneConversationResponse +testObject_GetOne2OneConversationResponseBackendMismatch = GetOne2OneConversationBackendMismatch + +testObject_GetOne2OneConversationResponseNotConnected :: GetOne2OneConversationResponse +testObject_GetOne2OneConversationResponseNotConnected = GetOne2OneConversationNotConnected + +testObject_GetOne2OneConversationResponseV2Ok :: GetOne2OneConversationResponseV2 +testObject_GetOne2OneConversationResponseV2Ok = + GetOne2OneConversationV2Ok $ + RemoteMLSOne2OneConversation + { conversation = remoteConversationV2, + publicKeys = + MLSKeysByPurpose + { removal = + MLSKeys + { ed25519 = + MLSPublicKey + (fromBase64TextLenient "7C8PpP91rzMnD4VHuWTI3yNuInfbzIk937uF0Cg/Piw="), + ecdsa_secp256r1_sha256 = + MLSPublicKey + (fromBase64TextLenient "ArUTSywmqya1wAGwrK+pJuA7KSpKm06y3eZq8Py2NMM="), + ecdsa_secp384r1_sha384 = + MLSPublicKey + (fromBase64TextLenient "7pKiTLf72OfpQIeVeXF0mJKfWsBnhTtMUy0zuKasYjlTQUW5fGtcyAFXinM3FahV"), + ecdsa_secp521r1_sha512 = + MLSPublicKey + (fromBase64TextLenient "9twvhZ57ytiujWXFtSmxd8I5r9iZjgdCtGtReJT3yQL2BCGZ80Vzq/MrmV+O0i7lZEI1gqbr8vL1xKk+2h2LyQ==") + } + } + } + +testObject_GetOne2OneConversationResponseV2BackendMismatch :: GetOne2OneConversationResponseV2 +testObject_GetOne2OneConversationResponseV2BackendMismatch = GetOne2OneConversationV2BackendMismatch + +testObject_GetOne2OneConversationResponseV2NotConnected :: GetOne2OneConversationResponseV2 +testObject_GetOne2OneConversationResponseV2NotConnected = GetOne2OneConversationV2NotConnected + +remoteConversation :: RemoteConversation +remoteConversation = + RemoteConversation + { id = (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200040001"))), + metadata = + ConversationMetadata + { cnvmType = One2OneConv, + cnvmCreator = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001"))), + cnvmAccess = [], + cnvmAccessRoles = Set.empty, + cnvmName = Just " 0", + cnvmTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000002"))), + cnvmMessageTimer = Nothing, + cnvmReceiptMode = Just (ReceiptMode {unReceiptMode = -2}) + }, + members = + RemoteConvMembers + { selfRole = roleNameWireAdmin, + others = + [ OtherMember + { omQualifiedId = + Qualified + (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001"))) + (Domain "example.com"), + omService = Nothing, + omConvRoleName = roleNameWireMember + } + ] + }, + protocol = + ClientAPI.Versioned . ProtocolMLS $ + ConversationMLSData + { cnvmlsGroupId = GroupId "group", + cnvmlsActiveData = Nothing + } + } + +remoteConversationV2 :: RemoteConversationV2 +remoteConversationV2 = + RemoteConversationV2 + { id = (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200040001"))), + metadata = + ConversationMetadata + { cnvmType = One2OneConv, + cnvmCreator = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001"))), + cnvmAccess = [], + cnvmAccessRoles = Set.empty, + cnvmName = Just " 0", + cnvmTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000002"))), + cnvmMessageTimer = Nothing, + cnvmReceiptMode = Just (ReceiptMode {unReceiptMode = -2}) + }, + members = + RemoteConvMembers + { selfRole = roleNameWireAdmin, + others = + [ OtherMember + { omQualifiedId = + Qualified + (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001"))) + (Domain "example.com"), + omService = Nothing, + omConvRoleName = roleNameWireMember + } + ] + }, + protocol = + ProtocolMLS $ + ConversationMLSData + { cnvmlsGroupId = GroupId "group", + cnvmlsActiveData = Nothing + } + } diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GoldenSpec.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GoldenSpec.hs index b691cd8e962..038f98b0d1e 100644 --- a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GoldenSpec.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GoldenSpec.hs @@ -21,6 +21,7 @@ import Imports import Test.Hspec import Test.Wire.API.Federation.Golden.ConversationCreated qualified as ConversationCreated import Test.Wire.API.Federation.Golden.ConversationUpdate qualified as ConversationUpdate +import Test.Wire.API.Federation.Golden.GetOne2OneConversationResponse qualified as GetOne2OneConversationResponse import Test.Wire.API.Federation.Golden.LeaveConversationRequest qualified as LeaveConversationRequest import Test.Wire.API.Federation.Golden.LeaveConversationResponse qualified as LeaveConversationResponse import Test.Wire.API.Federation.Golden.MLSMessageSendingStatus qualified as MLSMessageSendingStatus @@ -74,3 +75,13 @@ spec = [ (ConversationCreated.testObject_ConversationCreated1, "testObject_ConversationCreated1.json"), (ConversationCreated.testObject_ConversationCreated2, "testObject_ConversationCreated2.json") ] + testObjects + [ (GetOne2OneConversationResponse.testObject_GetOne2OneConversationResponseV2Ok, "testObject_GetOne2OneConversationResponseV2Ok.json"), + (GetOne2OneConversationResponse.testObject_GetOne2OneConversationResponseV2BackendMismatch, "testObject_GetOne2OneConversationResponseV2BackendMismatch.json"), + (GetOne2OneConversationResponse.testObject_GetOne2OneConversationResponseV2NotConnected, "testObject_GetOne2OneConversationResponseV2NotConnected.json") + ] + testObjects + [ (GetOne2OneConversationResponse.testObject_GetOne2OneConversationResponseOk, "testObject_GetOne2OneConversationResponseOk.json"), + (GetOne2OneConversationResponse.testObject_GetOne2OneConversationResponseBackendMismatch, "testObject_GetOne2OneConversationResponseBackendMismatch.json"), + (GetOne2OneConversationResponse.testObject_GetOne2OneConversationResponseNotConnected, "testObject_GetOne2OneConversationResponseNotConnected.json") + ] diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/MLSMessageSendingStatus.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/MLSMessageSendingStatus.hs index 27fba120068..17842d6df14 100644 --- a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/MLSMessageSendingStatus.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/MLSMessageSendingStatus.hs @@ -17,11 +17,7 @@ module Test.Wire.API.Federation.Golden.MLSMessageSendingStatus where -import Data.Domain -import Data.Id import Data.Json.Util -import Data.Qualified -import Data.UUID qualified as UUID import Imports import Wire.API.MLS.Message @@ -45,16 +41,3 @@ testObject_MLSMessageSendingStatus3 = { mmssEvents = [], mmssTime = toUTCTimeMillis (read "1999-04-12 12:22:43.673 UTC") } - -failed1 :: [Qualified UserId] -failed1 = - let domain = Domain "offline.example.com" - in [Qualified (Id . fromJust . UUID.fromString $ "00000000-0000-0000-0000-000200000008") domain] - -failed2 :: [Qualified UserId] -failed2 = - let domain = Domain "golden.example.com" - in flip Qualified domain . Id . fromJust . UUID.fromString - <$> [ "00000000-0000-0000-0000-000200000008", - "00000000-0000-0000-0000-000100000007" - ] diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/Runner.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/Runner.hs index ba2c5e02e54..030d84247cd 100644 --- a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/Runner.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/Runner.hs @@ -17,8 +17,6 @@ module Test.Wire.API.Federation.Golden.Runner ( testObjects, - testFromJSONFailure, - testFromJSONObjects, ) where @@ -60,25 +58,6 @@ testObject obj path = do pure exists -testFromJSONObjects :: forall a. (Typeable a, FromJSON a, Eq a, Show a) => [(a, FilePath)] -> IO () -testFromJSONObjects = traverse_ (uncurry testFromJSONObject) - -testFromJSONObject :: forall a. (Typeable a, FromJSON a, Eq a, Show a) => a -> FilePath -> IO () -testFromJSONObject expected path = do - let dir = "test/golden/fromJSON" - fullPath = dir <> "/" <> path - parsed <- eitherDecodeFileStrict fullPath - assertEqual (show (typeRep @a) <> ": FromJSON of " <> path <> " should match object") (Right expected) parsed - -testFromJSONFailure :: forall a. (Typeable a, FromJSON a, Show a) => FilePath -> IO () -testFromJSONFailure path = do - let dir = "test/golden/fromJSON" - fullPath = dir <> "/" <> path - parsed <- eitherDecodeFileStrict @a fullPath - case parsed of - Right x -> assertFailure $ show (typeRep @a) <> ": FromJSON of " <> path <> ": expected failure, got " <> show x - Left _ -> pure () - assertRight :: (Show a) => Either a b -> IO b assertRight = \case diff --git a/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseBackendMismatch.json b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseBackendMismatch.json new file mode 100644 index 00000000000..be96bcaa144 --- /dev/null +++ b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseBackendMismatch.json @@ -0,0 +1,3 @@ +{ + "tag": "GetOne2OneConversationBackendMismatch" +} \ No newline at end of file diff --git a/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseNotConnected.json b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseNotConnected.json new file mode 100644 index 00000000000..3920fe3bed6 --- /dev/null +++ b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseNotConnected.json @@ -0,0 +1,3 @@ +{ + "tag": "GetOne2OneConversationNotConnected" +} \ No newline at end of file diff --git a/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseOk.json b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseOk.json new file mode 100644 index 00000000000..dbfa3343671 --- /dev/null +++ b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseOk.json @@ -0,0 +1,39 @@ +{ + "contents": { + "id": "00000001-0000-0001-0000-000200040001", + "members": { + "others": [ + { + "conversation_role": "wire_member", + "id": "00000001-0000-0001-0000-000200000001", + "qualified_id": { + "domain": "example.com", + "id": "00000001-0000-0001-0000-000200000001" + }, + "status": 0 + } + ], + "self_role": "wire_admin" + }, + "metadata": { + "access": [], + "access_role": [], + "creator": "00000001-0000-0001-0000-000200000001", + "last_event": "0.0", + "last_event_time": "1970-01-01T00:00:00.000Z", + "message_timer": null, + "name": " 0", + "receipt_mode": -2, + "team": "00000001-0000-0001-0000-000100000002", + "type": 2 + }, + "protocol": { + "cipher_suite": 1, + "epoch": 0, + "epoch_timestamp": null, + "group_id": "Z3JvdXA=", + "protocol": "mls" + } + }, + "tag": "GetOne2OneConversationOk" +} \ No newline at end of file diff --git a/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2BackendMismatch.json b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2BackendMismatch.json new file mode 100644 index 00000000000..49390ae4674 --- /dev/null +++ b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2BackendMismatch.json @@ -0,0 +1,3 @@ +{ + "tag": "GetOne2OneConversationV2BackendMismatch" +} \ No newline at end of file diff --git a/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2NotConnected.json b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2NotConnected.json new file mode 100644 index 00000000000..22355bf11e6 --- /dev/null +++ b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2NotConnected.json @@ -0,0 +1,3 @@ +{ + "tag": "GetOne2OneConversationV2NotConnected" +} \ No newline at end of file diff --git a/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2Ok.json b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2Ok.json new file mode 100644 index 00000000000..d14e627e6be --- /dev/null +++ b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2Ok.json @@ -0,0 +1,47 @@ +{ + "contents": { + "conversation": { + "id": "00000001-0000-0001-0000-000200040001", + "members": { + "others": [ + { + "conversation_role": "wire_member", + "id": "00000001-0000-0001-0000-000200000001", + "qualified_id": { + "domain": "example.com", + "id": "00000001-0000-0001-0000-000200000001" + }, + "status": 0 + } + ], + "self_role": "wire_admin" + }, + "metadata": { + "access": [], + "access_role": [], + "creator": "00000001-0000-0001-0000-000200000001", + "last_event": "0.0", + "last_event_time": "1970-01-01T00:00:00.000Z", + "message_timer": null, + "name": " 0", + "receipt_mode": -2, + "team": "00000001-0000-0001-0000-000100000002", + "type": 2 + }, + "protocol": { + "epoch": 0, + "group_id": "Z3JvdXA=", + "protocol": "mls" + } + }, + "public_keys": { + "removal": { + "ecdsa_secp256r1_sha256": "ArUTSywmqya1wAGwrK+pJuA7KSpKm06y3eZq8Py2NMM=", + "ecdsa_secp384r1_sha384": "7pKiTLf72OfpQIeVeXF0mJKfWsBnhTtMUy0zuKasYjlTQUW5fGtcyAFXinM3FahV", + "ecdsa_secp521r1_sha512": "9twvhZ57ytiujWXFtSmxd8I5r9iZjgdCtGtReJT3yQL2BCGZ80Vzq/MrmV+O0i7lZEI1gqbr8vL1xKk+2h2LyQ==", + "ed25519": "7C8PpP91rzMnD4VHuWTI3yNuInfbzIk937uF0Cg/Piw=" + } + } + }, + "tag": "GetOne2OneConversationV2Ok" +} \ No newline at end of file diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index e2490419ca0..73d028de763 100644 --- a/libs/wire-api-federation/wire-api-federation.cabal +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -38,6 +38,7 @@ library default-extensions: AllowAmbiguousTypes BangPatterns + BlockArguments ConstraintKinds DataKinds DefaultSignatures @@ -81,7 +82,6 @@ library -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -Wredundant-constraints -Wunused-packages - -fplugin=TransitiveAnns.Plugin build-depends: aeson >=2.0.1.0 @@ -115,7 +115,6 @@ library , text >=0.11 , time >=1.8 , transformers - , transitive-anns , types-common , wai-utilities , wire-api @@ -132,6 +131,7 @@ test-suite spec Test.Wire.API.Federation.API.BrigSpec Test.Wire.API.Federation.Golden.ConversationCreated Test.Wire.API.Federation.Golden.ConversationUpdate + Test.Wire.API.Federation.Golden.GetOne2OneConversationResponse Test.Wire.API.Federation.Golden.GoldenSpec Test.Wire.API.Federation.Golden.LeaveConversationRequest Test.Wire.API.Federation.Golden.LeaveConversationResponse diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index ce55894b212..6249b98c695 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -23,7 +23,6 @@ , cereal , comonad , conduit -, constraints , containers , cookie , crypton @@ -32,7 +31,6 @@ , data-default , deriving-aeson , deriving-swagger2 -, either , email-validate , errors , extended @@ -58,12 +56,14 @@ , iso3166-country-codes , iso639 , jose +, kan-extensions , lens , lib , memory , metrics-wai , mime , mtl +, network-uri , openapi3 , pem , polysemy @@ -83,6 +83,7 @@ , servant-client-core , servant-conduit , servant-multipart +, servant-multipart-api , servant-openapi3 , servant-server , singletons @@ -96,9 +97,9 @@ , tasty-hunit , tasty-quickcheck , text +, these , time , tinylog -, transitive-anns , types-common , unliftio , unordered-containers @@ -134,7 +135,6 @@ mkDerivation { cereal comonad conduit - constraints containers cookie crypton @@ -143,7 +143,6 @@ mkDerivation { data-default deriving-aeson deriving-swagger2 - either email-validate errors extended @@ -165,11 +164,13 @@ mkDerivation { iso3166-country-codes iso639 jose + kan-extensions lens memory metrics-wai mime mtl + network-uri openapi3 pem polysemy @@ -188,6 +189,7 @@ mkDerivation { servant-client-core servant-conduit servant-multipart + servant-multipart-api servant-openapi3 servant-server singletons @@ -196,9 +198,9 @@ mkDerivation { sop-core tagged text + these time tinylog - transitive-anns types-common unordered-containers uri-bytestring @@ -228,7 +230,6 @@ mkDerivation { containers crypton currency-codes - either filepath hex hspec diff --git a/libs/wire-api/src/Wire/API/Allowlists.hs b/libs/wire-api/src/Wire/API/Allowlists.hs index 244a5e8cb85..ef986a2d5a0 100644 --- a/libs/wire-api/src/Wire/API/Allowlists.hs +++ b/libs/wire-api/src/Wire/API/Allowlists.hs @@ -25,6 +25,7 @@ module Wire.API.Allowlists where import Data.Aeson +import Data.Text.Encoding (decodeUtf8) import Imports import Wire.API.User.Identity @@ -36,6 +37,6 @@ instance FromJSON AllowlistEmailDomains -- | Consult the whitelist settings in brig's config file and verify that the provided -- email address is whitelisted. -verify :: Maybe AllowlistEmailDomains -> Email -> Bool -verify (Just (AllowlistEmailDomains allowed)) email = emailDomain email `elem` allowed +verify :: Maybe AllowlistEmailDomains -> EmailAddress -> Bool +verify (Just (AllowlistEmailDomains allowed)) email = (decodeUtf8 . domainPart $ email) `elem` allowed verify Nothing (_) = True diff --git a/libs/wire-api/src/Wire/API/Call/Config.hs b/libs/wire-api/src/Wire/API/Call/Config.hs index b48d771e20d..e0fafcf1f6f 100644 --- a/libs/wire-api/src/Wire/API/Call/Config.hs +++ b/libs/wire-api/src/Wire/API/Call/Config.hs @@ -46,16 +46,10 @@ module Wire.API.Call.Config turiTransport, Transport (..), TurnHost (..), - isHostName, -- * SFTUsername SFTUsername, mkSFTUsername, - suExpiresAt, - suVersion, - suKeyindex, - suShared, - suRandom, -- * TurnUsername TurnUsername, @@ -409,10 +403,6 @@ instance Arbitrary TurnHost where "xn--mgbh0fb.xn--kgbechtv" ] -isHostName :: TurnHost -> Bool -isHostName (TurnHostIp _) = False -isHostName (TurnHostName _) = True - parseTurnHost :: Text -> Maybe TurnHost parseTurnHost h = case BC.fromByteString host of Just ip@(IpAddr _) -> Just $ TurnHostIp ip @@ -476,7 +466,7 @@ mkSFTUsername shared expires rnd = } instance ToSchema SFTUsername where - schema = toText .= parsedText "" fromText + schema = toText .= parsedText "SFTUsername" fromText where fromText :: Text -> Either String SFTUsername fromText = parseOnly (parseSFTUsername <* endOfInput) @@ -553,7 +543,7 @@ turnUsername expires rnd = } instance ToSchema TurnUsername where - schema = toText .= parsedText "" fromText + schema = toText .= parsedText "TurnUsername" fromText where fromText :: Text -> Either String TurnUsername fromText = parseOnly (parseTurnUsername <* endOfInput) @@ -645,7 +635,6 @@ isTls uri = makeLenses ''RTCConfiguration makeLenses ''RTCIceServer makeLenses ''TurnURI -makeLenses ''SFTUsername makeLenses ''TurnUsername makeLenses ''SFTServer makeLenses ''AuthSFTServer diff --git a/libs/wire-api/src/Wire/API/CannonId.hs b/libs/wire-api/src/Wire/API/CannonId.hs new file mode 100644 index 00000000000..fb537887876 --- /dev/null +++ b/libs/wire-api/src/Wire/API/CannonId.hs @@ -0,0 +1,18 @@ +module Wire.API.CannonId where + +import Data.Aeson +import Data.OpenApi +import Data.Proxy +import Imports +import Web.HttpApiData + +newtype CannonId = CannonId + { cannonId :: Text + } + deriving (Eq, Ord, Show, FromJSON, ToJSON) + +instance ToParamSchema CannonId where + toParamSchema _ = toParamSchema (Proxy @Text) + +instance FromHttpApiData CannonId where + parseUrlPiece = pure . CannonId diff --git a/libs/wire-api/src/Wire/API/Component.hs b/libs/wire-api/src/Wire/API/Component.hs new file mode 100644 index 00000000000..486c870d655 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Component.hs @@ -0,0 +1,69 @@ +-- 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 OverloadedLists #-} +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + +module Wire.API.Component + ( Component (..), + ShowComponent, + ) +where + +import Data.Aeson +import Data.Schema +import GHC.TypeLits +import Imports +import Servant.API +import Test.QuickCheck (Arbitrary) +import Wire.Arbitrary (GenericUniform (..)) + +data Component + = Brig + | Galley + | Cargohold + deriving (Show, Eq, Generic) + deriving (Arbitrary) via (GenericUniform Component) + deriving (ToJSON, FromJSON) via (Schema Component) + +instance ToSchema Component where + schema = + enum @Text "Component" $ + mconcat + [ element "brig" Brig, + element "galley" Galley, + element "cargohold" Cargohold + ] + +instance FromHttpApiData Component where + parseUrlPiece :: Text -> Either Text Component + parseUrlPiece = \case + "brig" -> Right Brig + "galley" -> Right Galley + "cargohold" -> Right Cargohold + c -> Left $ "Invalid component: " <> c + +instance ToHttpApiData Component where + toUrlPiece = \case + Brig -> "brig" + Galley -> "galley" + Cargohold -> "cargohold" + +-- | Get a symbol representation of our component. +type family ShowComponent (x :: Component) = (res :: Symbol) | res -> x where + ShowComponent 'Brig = "brig" + ShowComponent 'Galley = "galley" + ShowComponent 'Cargohold = "cargohold" diff --git a/libs/wire-api/src/Wire/API/ConverProtoLens.hs b/libs/wire-api/src/Wire/API/ConverProtoLens.hs deleted file mode 100644 index 6e4398c47f7..00000000000 --- a/libs/wire-api/src/Wire/API/ConverProtoLens.hs +++ /dev/null @@ -1,33 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Wire.API.ConverProtoLens where - -import Data.Bifunctor (Bifunctor (first)) -import Imports - --- | This typeclass exists to provide overloaded function names for convertion --- between data types generated by proto-lens and data types used in wire --- We added fundeps here for better type inference, but we can't be as explicit as we wanted --- with @a -> b, b -> a@, since our instances would be orphaned on the left hand side argument. -class ConvertProtoLens a b | b -> a where - fromProtolens :: a -> Either Text b - toProtolens :: b -> a - --- | Add labels to error messages -protoLabel :: Text -> Either Text a -> Either Text a -protoLabel lbl = first ((lbl <> ": ") <>) diff --git a/libs/wire-api/src/Wire/API/Conversation.hs b/libs/wire-api/src/Wire/API/Conversation.hs index 0aa78bd25c6..fb851d4be9c 100644 --- a/libs/wire-api/src/Wire/API/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Conversation.hs @@ -34,6 +34,7 @@ module Wire.API.Conversation cnvMessageTimer, cnvReceiptMode, cnvAccessRoles, + MLSOne2OneConversation (..), CreateGroupConversation (..), ConversationCoverView (..), ConversationList (..), @@ -68,7 +69,6 @@ module Wire.API.Conversation -- * invite Invite (..), InviteQualified (..), - newInvite, -- * update ConversationRename (..), @@ -97,7 +97,6 @@ import Data.List.NonEmpty (NonEmpty) import Data.List1 import Data.Map qualified as Map import Data.Misc -import Data.OpenApi (deprecated) import Data.OpenApi qualified as S import Data.Qualified import Data.Range (Range, fromRange, rangedSchema) @@ -115,6 +114,7 @@ import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role (RoleName, roleNameWireAdmin) import Wire.API.Event.LeaveReason import Wire.API.MLS.Group +import Wire.API.MLS.Keys import Wire.API.Routes.MultiTablePaging import Wire.API.Routes.MultiVerb import Wire.API.Routes.Version @@ -290,6 +290,20 @@ conversationSchema v = (description ?~ "A conversation object as returned from the server") (conversationObjectSchema v) +data MLSOne2OneConversation a = MLSOne2OneConversation + { conversation :: Conversation, + publicKeys :: MLSKeysByPurpose (MLSKeys a) + } + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema (MLSOne2OneConversation a)) + +instance (ToSchema a) => ToSchema (MLSOne2OneConversation a) where + schema = + let aName = maybe "" ("_" <>) $ getName (schemaDoc (schema @a)) + in object ("MLSOne2OneConversation" <> aName) $ + MLSOne2OneConversation + <$> conversation .= field "conversation" schema + <*> publicKeys .= field "public_keys" schema + -- | The public-facing conversation type extended with information on which -- remote users could not be added when creating the conversation. data CreateGroupConversation = CreateGroupConversation @@ -683,7 +697,7 @@ newConvSchema v sch = <$> newConvUsers .= ( fieldWithDocModifier "users" - ( (deprecated ?~ True) + ( (S.deprecated ?~ True) . (description ?~ usersDesc) ) (array schema) @@ -805,9 +819,6 @@ instance ToSchema InviteQualified where <*> invQRoleName .= (fromMaybe roleNameWireAdmin <$> optField "conversation_role" schema) -newInvite :: List1 UserId -> Invite -newInvite us = Invite us roleNameWireAdmin - -------------------------------------------------------------------------------- -- update diff --git a/libs/wire-api/src/Wire/API/Conversation/Member.hs b/libs/wire-api/src/Wire/API/Conversation/Member.hs index 1443e158af8..7d3636f3f40 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Member.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Member.hs @@ -26,7 +26,6 @@ module Wire.API.Conversation.Member defMember, MutedStatus (..), OtherMember (..), - defOtherMember, -- * Member Update MemberUpdate (..), @@ -150,14 +149,6 @@ data OtherMember = OtherMember deriving (Arbitrary) via (GenericUniform OtherMember) deriving (FromJSON, ToJSON, S.ToSchema) via Schema OtherMember -defOtherMember :: Qualified UserId -> OtherMember -defOtherMember uid = - OtherMember - { omQualifiedId = uid, - omService = Nothing, - omConvRoleName = roleNameWireMember - } - instance ToSchema OtherMember where schema = object "OtherMember" $ diff --git a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs index c0060347b7b..ef4be957f28 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs @@ -28,7 +28,6 @@ module Wire.API.Conversation.Protocol _ProtocolMLS, _ProtocolMixed, _ProtocolProteus, - conversationMLSData, protocolSchema, ConversationMLSData (..), ActiveMLSConversationData (..), @@ -40,7 +39,7 @@ where import Control.Applicative import Control.Arrow -import Control.Lens (Traversal', makePrisms, (?~)) +import Control.Lens (makePrisms, (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Json.Util import Data.OpenApi qualified as S @@ -109,12 +108,7 @@ optionalActiveMLSConversationDataSchema (Just v) (description ?~ "The epoch number of the corresponding MLS group") schema <*> fmap (.epochTimestamp) - .= maybe_ - ( optFieldWithDocModifier - "epoch_timestamp" - (description ?~ "The timestamp of the epoch number") - utcTimeSchema - ) + .= field "epoch_timestamp" (named "EpochTimestamp" . nullable . unnamed $ utcTimeSchema) <*> maybe MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 (.ciphersuite) .= fieldWithDocModifier "cipher_suite" @@ -201,11 +195,6 @@ data Protocol $(makePrisms ''Protocol) -conversationMLSData :: Traversal' Protocol ConversationMLSData -conversationMLSData _ ProtocolProteus = pure ProtocolProteus -conversationMLSData f (ProtocolMLS mls) = ProtocolMLS <$> f mls -conversationMLSData f (ProtocolMixed mls) = ProtocolMixed <$> f mls - protocolTag :: Protocol -> ProtocolTag protocolTag ProtocolProteus = ProtocolProteusTag protocolTag (ProtocolMLS _) = ProtocolMLSTag @@ -251,6 +240,9 @@ protocolSchema v = instance ToSchema Protocol where schema = object "Protocol" (protocolSchema Nothing) +instance ToSchema (Versioned 'V5 Protocol) where + schema = object "Protocol" (Versioned <$> unVersioned .= protocolSchema (Just V5)) + deriving via (Schema Protocol) instance FromJSON Protocol deriving via (Schema Protocol) instance ToJSON Protocol diff --git a/libs/wire-api/src/Wire/API/Conversation/Role.hs b/libs/wire-api/src/Wire/API/Conversation/Role.hs index edb97c23f42..c22cccf72c1 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Role.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Role.hs @@ -33,7 +33,6 @@ module Wire.API.Conversation.Role RoleName, fromRoleName, parseRoleName, - wireConvRoleNames, roleNameWireAdmin, roleNameWireMember, @@ -246,9 +245,6 @@ instance Arbitrary RoleName where where genChar = QC.elements $ ['a' .. 'z'] <> ['0' .. '9'] <> ['_'] -wireConvRoleNames :: [RoleName] -wireConvRoleNames = [roleNameWireAdmin, roleNameWireMember] - roleNameWireAdmin :: RoleName roleNameWireAdmin = RoleName "wire_admin" diff --git a/libs/wire-api/src/Wire/API/Error.hs b/libs/wire-api/src/Wire/API/Error.hs index 275d554a139..a1899f9f6ca 100644 --- a/libs/wire-api/src/Wire/API/Error.hs +++ b/libs/wire-api/src/Wire/API/Error.hs @@ -41,11 +41,13 @@ module Wire.API.Error throwS, noteS, mapErrorS, + runErrorS, mapToRuntimeError, mapToDynamicError, ) where +import Control.Error (hush) import Control.Lens (at, (%~), (.~), (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A @@ -164,7 +166,7 @@ instance (KnownError e) => ToSchema (SStaticError e) where data CanThrow e -data CanThrowMany e +data CanThrowMany (es :: [k]) instance (RoutesToPaths api) => RoutesToPaths (CanThrow err :> api) where getRoutes = getRoutes @api @@ -203,18 +205,18 @@ type instance SpecialiseToVersion v (CanThrowMany es :> api) = CanThrowMany es :> SpecialiseToVersion v api -instance (HasOpenApi api) => HasOpenApi (CanThrowMany '() :> api) where +instance (HasOpenApi api) => HasOpenApi (CanThrowMany '[] :> api) where toOpenApi _ = toOpenApi (Proxy @api) instance (HasOpenApi (CanThrowMany es :> api), IsSwaggerError e) => - HasOpenApi (CanThrowMany '(e, es) :> api) + HasOpenApi (CanThrowMany (e : es) :> api) where toOpenApi _ = addToOpenApi @e (toOpenApi (Proxy @(CanThrowMany es :> api))) type family DeclaredErrorEffects api :: EffectRow where DeclaredErrorEffects (CanThrow e :> api) = (ErrorEffect e ': DeclaredErrorEffects api) - DeclaredErrorEffects (CanThrowMany '(e, es) :> api) = + DeclaredErrorEffects (CanThrowMany (e : es) :> api) = DeclaredErrorEffects (CanThrow e :> CanThrowMany es :> api) DeclaredErrorEffects (x :> api) = DeclaredErrorEffects api DeclaredErrorEffects (Named n api) = DeclaredErrorEffects api @@ -272,6 +274,9 @@ throwS = throw (Tagged @e ()) noteS :: forall e r a. (Member (ErrorS e) r) => Maybe a -> Sem r a noteS = note (Tagged @e ()) +runErrorS :: forall e r a. Sem (ErrorS e : r) a -> Sem r (Maybe a) +runErrorS = fmap hush . runError @(Tagged e ()) + mapErrorS :: forall e e' r a. (Member (ErrorS e') r) => diff --git a/libs/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index e84846c1620..9c397736cc2 100644 --- a/libs/wire-api/src/Wire/API/Error/Brig.hs +++ b/libs/wire-api/src/Wire/API/Error/Brig.hs @@ -40,6 +40,7 @@ data BrigError | NotConnected | InvalidTransition | NoIdentity + | NoUser | HandleExists | InvalidHandle | HandleNotFound @@ -62,14 +63,15 @@ data BrigError | AccountEphemeral | AccountPending | UserKeyExists + | EmailExists | NameManagedByScim | HandleManagedByScim | LocaleManagedByScim | LastIdentity | NoPassword | ChangePasswordMustDiffer - | PasswordAuthenticationFailed | TooManyTeamInvitations + | CannotJoinMultipleTeams | InsufficientTeamPermissions | KeyPackageDecodingError | InvalidKeyPackageRef @@ -94,6 +96,11 @@ data BrigError | ProviderNotFound | TeamsNotFederating | PasswordIsStale + | TooManyProperties + | PropertyKeyTooLarge + | PropertyValueTooLarge + | UserAlreadyInATeam + | MLSServicesNotAllowed instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: BrigError) where addToOpenApi = addStaticErrorToSwagger @(MapError e) @@ -232,6 +239,8 @@ type instance MapError 'AccountPending = 'StaticError 403 "pending-activation" " type instance MapError 'UserKeyExists = 'StaticError 409 "key-exists" "The given e-mail address is in use." +type instance MapError 'EmailExists = 'StaticError 409 "email-exists" "The given e-mail address is in use." + type instance MapError 'NameManagedByScim = 'StaticError 403 "managed-by-scim" "Updating name is not allowed, because it is managed by SCIM, or E2EId is enabled" type instance MapError 'HandleManagedByScim = 'StaticError 403 "managed-by-scim" "Updating handle is not allowed, because it is managed by SCIM, or E2EId is enabled" @@ -244,10 +253,10 @@ type instance MapError 'NoPassword = 'StaticError 403 "no-password" "The user ha type instance MapError 'ChangePasswordMustDiffer = 'StaticError 409 "password-must-differ" "For password change, new and old password must be different." -type instance MapError 'PasswordAuthenticationFailed = 'StaticError 403 "password-authentication-failed" "Password authentication failed." - type instance MapError 'TooManyTeamInvitations = 'StaticError 403 "too-many-team-invitations" "Too many team invitations for this team" +type instance MapError 'CannotJoinMultipleTeams = 'StaticError 403 "cannot-join-multiple-teams" "Cannot accept invitations from multiple teams" + type instance MapError 'InsufficientTeamPermissions = 'StaticError 403 "insufficient-permissions" "Insufficient team permissions" type instance MapError 'KeyPackageDecodingError = 'StaticError 409 "decoding-error" "Key package could not be TLS-decoded" @@ -282,3 +291,13 @@ type instance MapError 'ConflictingInvitations = 'StaticError 409 "conflicting-i type instance MapError 'TeamsNotFederating = 'StaticError 403 "team-not-federating" "The target user is owned by a federated backend, but is not in an allow-listed team" type instance MapError 'PasswordIsStale = 'StaticError 403 "password-is-stale" "The password is too old, please update your password." + +type instance MapError 'TooManyProperties = 'StaticError 403 "too-many-properties" "Too many properties" + +type instance MapError 'PropertyKeyTooLarge = 'StaticError 403 "property-key-too-large" "The property key is too large." + +type instance MapError 'PropertyValueTooLarge = 'StaticError 403 "property-value-too-large" "The property value is too large" + +type instance MapError 'UserAlreadyInATeam = 'StaticError 403 "user-already-in-a-team" "Switching teams is not allowed" + +type instance MapError 'MLSServicesNotAllowed = 'StaticError 409 "mls-services-not-allowed" "Services not allowed in MLS" diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index ed4d3a0e226..55d122012d4 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -104,6 +104,10 @@ data GalleyError | MLSSubConvUnsupportedConvType | MLSSubConvClientNotInParent | MLSMigrationCriteriaNotSatisfied + | MLSFederatedOne2OneNotSupported + | -- | MLS and federation are incompatible with legalhold - this error is thrown if a user + -- tries to create an MLS group while being under legalhold + MLSLegalholdIncompatible | -- NoBindingTeamMembers | NoBindingTeam @@ -253,6 +257,10 @@ type instance MapError 'MLSSubConvClientNotInParent = 'StaticError 403 "mls-subc type instance MapError 'MLSMigrationCriteriaNotSatisfied = 'StaticError 400 "mls-migration-criteria-not-satisfied" "The migration criteria for mixed to MLS protocol transition are not satisfied for this conversation" +type instance MapError 'MLSFederatedOne2OneNotSupported = 'StaticError 400 "mls-federated-one2one-not-supported" "Federated One2One MLS conversations are only supported in API version >= 6" + +type instance MapError MLSLegalholdIncompatible = 'StaticError 409 "mls-legal-hold-not-allowed" "A user who is under legal-hold may not participate in MLS conversations" + type instance MapError 'NoBindingTeamMembers = 'StaticError 403 "non-binding-team-members" "Both users must be members of the same binding team" type instance MapError 'NoBindingTeam = 'StaticError 403 "no-binding-team" "Operation allowed only on binding teams" @@ -367,50 +375,41 @@ data TeamFeatureError | FeatureLocked | MLSProtocolMismatch | MLSE2EIDMissingCrlProxy + | EmptyDownloadLocation instance IsSwaggerError TeamFeatureError where -- Do not display in Swagger addToOpenApi = id -type instance MapError 'AppLockInactivityTimeoutTooLow = 'StaticError 400 "inactivity-timeout-too-low" "Applock inactivity timeout must be at least 30 seconds" - -type instance MapError 'LegalHoldFeatureFlagNotEnabled = 'StaticError 403 "legalhold-not-enabled" "Legal hold is not enabled for this wire instance" - -type instance MapError 'LegalHoldWhitelistedOnly = 'StaticError 403 "legalhold-whitelisted-only" "Legal hold is enabled for teams via server config and cannot be changed here" - -type instance - MapError 'DisableSsoNotImplemented = - 'StaticError - 403 - "not-implemented" - "The SSO feature flag is disabled by default. It can only be enabled once for any team, never disabled.\n\ - \\n\ - \Rationale: there are two services in the backend that need to keep their status in sync: galley (teams),\n\ - \and spar (SSO). Galley keeps track of team features. If galley creates an idp, the feature flag is\n\ - \checked. For authentication, spar avoids this expensive check and assumes that the idp can only have\n\ - \been created if the SSO is enabled. This assumption does not hold any more if the switch is turned off\n\ - \again, so we do not support this.\n\ - \\n\ - \It is definitely feasible to change this. If you have a use case, please contact customer support, or\n\ - \open an issue on https://github.com/wireapp/wire-server." - -type instance MapError 'FeatureLocked = 'StaticError 409 "feature-locked" "Feature config cannot be updated (e.g. because it is configured to be locked, or because you need to upgrade your plan)" - -type instance MapError 'MLSProtocolMismatch = 'StaticError 400 "mls-protocol-mismatch" "The default protocol needs to be part of the supported protocols" - -type instance MapError 'MLSE2EIDMissingCrlProxy = 'StaticError 400 "mls-e2eid-missing-crl-proxy" "The field 'crlProxy' is missing in the request payload" - -type instance ErrorEffect TeamFeatureError = Error TeamFeatureError - instance (Member (Error DynError) r) => ServerEffect (Error TeamFeatureError) r where interpretServerEffect = mapError $ \case - AppLockInactivityTimeoutTooLow -> dynError @(MapError 'AppLockInactivityTimeoutTooLow) - LegalHoldFeatureFlagNotEnabled -> dynError @(MapError 'LegalHoldFeatureFlagNotEnabled) - LegalHoldWhitelistedOnly -> dynError @(MapError 'LegalHoldWhitelistedOnly) - DisableSsoNotImplemented -> dynError @(MapError 'DisableSsoNotImplemented) - FeatureLocked -> dynError @(MapError 'FeatureLocked) - MLSProtocolMismatch -> dynError @(MapError 'MLSProtocolMismatch) - MLSE2EIDMissingCrlProxy -> dynError @(MapError 'MLSE2EIDMissingCrlProxy) + AppLockInactivityTimeoutTooLow -> + DynError + 400 + "inactivity-timeout-too-low" + "Applock inactivity timeout must be at least 30 seconds" + LegalHoldFeatureFlagNotEnabled -> DynError 403 "legalhold-not-enabled" "Legal hold is not enabled for this wire instance" + LegalHoldWhitelistedOnly -> DynError 403 "legalhold-whitelisted-only" "Legal hold is enabled for teams via server config and cannot be changed here" + DisableSsoNotImplemented -> + DynError + 403 + "not-implemented" + "The SSO feature flag is disabled by default. It can only be enabled once for any team, never disabled.\n\ + \\n\ + \Rationale: there are two services in the backend that need to keep their status in sync: galley (teams),\n\ + \and spar (SSO). Galley keeps track of team features. If galley creates an idp, the feature flag is\n\ + \checked. For authentication, spar avoids this expensive check and assumes that the idp can only have\n\ + \been created if the SSO is enabled. This assumption does not hold any more if the switch is turned off\n\ + \again, so we do not support this.\n\ + \\n\ + \It is definitely feasible to change this. If you have a use case, please contact customer support, or\n\ + \open an issue on https://github.com/wireapp/wire-server." + FeatureLocked -> DynError 409 "feature-locked" "Feature config cannot be updated (e.g. because it is configured to be locked, or because you need to upgrade your plan)" + MLSProtocolMismatch -> DynError 400 "mls-protocol-mismatch" "The default protocol needs to be part of the supported protocols" + MLSE2EIDMissingCrlProxy -> DynError 400 "mls-e2eid-missing-crl-proxy" "The field 'crlProxy' is missing in the request payload" + EmptyDownloadLocation -> DynError 400 "empty-download-location" "Download location cannot be empty" + +type instance ErrorEffect TeamFeatureError = Error TeamFeatureError -------------------------------------------------------------------------------- -- Proposal failure diff --git a/libs/wire-api/src/Wire/API/Event/Conversation.hs b/libs/wire-api/src/Wire/API/Event/Conversation.hs index f06e8d62973..74d537136a4 100644 --- a/libs/wire-api/src/Wire/API/Event/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Event/Conversation.hs @@ -409,7 +409,7 @@ taggedEventDataSchema = memberLeaveSchema :: ValueSchema NamedSwaggerDoc (EdMemberLeftReason, QualifiedUserIdList) memberLeaveSchema = - object "QualifiedUserIdList with EdMemberLeftReason" $ + object "QualifiedUserIdList_with_EdMemberLeftReason" $ (,) <$> fst .= field "reason" schema <*> snd .= qualifiedUserIdListObjectSchema instance ToSchema Event where diff --git a/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs b/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs index 32e67dcfaf6..30242dccf7f 100644 --- a/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs +++ b/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs @@ -28,9 +28,8 @@ import Data.Aeson.KeyMap qualified as KeyMap import Data.Json.Util (ToJSONObject (toJSONObject)) import Data.OpenApi qualified as S import Data.Schema -import GHC.TypeLits (KnownSymbol) import Imports -import Test.QuickCheck.Gen (oneof) +import Test.QuickCheck.Gen import Wire.API.Team.Feature import Wire.Arbitrary (Arbitrary (..), GenericUniform (..)) @@ -42,30 +41,30 @@ data Event = Event deriving (Eq, Show, Generic) deriving (A.ToJSON, A.FromJSON) via Schema Event +arbitraryFeature :: forall cfg. (IsFeatureConfig cfg, Arbitrary cfg) => Gen A.Value +arbitraryFeature = toJSON <$> arbitrary @(LockableFeature cfg) + +class AllArbitraryFeatures cfgs where + allArbitraryFeatures :: [Gen A.Value] + +instance AllArbitraryFeatures '[] where + allArbitraryFeatures = [] + +instance + ( IsFeatureConfig cfg, + Arbitrary cfg, + AllArbitraryFeatures cfgs + ) => + AllArbitraryFeatures (cfg : cfgs) + where + allArbitraryFeatures = arbitraryFeature @cfg : allArbitraryFeatures @cfgs + instance Arbitrary Event where arbitrary = - do - let arbConfig = - oneof - [ arbitrary @(WithStatus SSOConfig) <&> toJSON, - arbitrary @(WithStatus SearchVisibilityAvailableConfig) <&> toJSON, - arbitrary @(WithStatus ValidateSAMLEmailsConfig) <&> toJSON, - arbitrary @(WithStatus DigitalSignaturesConfig) <&> toJSON, - arbitrary @(WithStatus AppLockConfig) <&> toJSON, - arbitrary @(WithStatus FileSharingConfig) <&> toJSON, - arbitrary @(WithStatus ClassifiedDomainsConfig) <&> toJSON, - arbitrary @(WithStatus ConferenceCallingConfig) <&> toJSON, - arbitrary @(WithStatus SelfDeletingMessagesConfig) <&> toJSON, - arbitrary @(WithStatus GuestLinksConfig) <&> toJSON, - arbitrary @(WithStatus SndFactorPasswordChallengeConfig) <&> toJSON, - arbitrary @(WithStatus SearchVisibilityInboundConfig) <&> toJSON, - arbitrary @(WithStatus MLSConfig) <&> toJSON, - arbitrary @(WithStatus ExposeInvitationURLsToTeamAdminConfig) <&> toJSON - ] - Event - <$> arbitrary - <*> arbitrary - <*> arbConfig + Event + <$> arbitrary + <*> arbitrary + <*> oneof (allArbitraryFeatures @Features) data EventType = Update deriving (Eq, Show, Generic) @@ -98,5 +97,5 @@ instance ToJSONObject Event where instance S.ToSchema Event where declareNamedSchema = schemaToSwagger -mkUpdateEvent :: forall cfg. (IsFeatureConfig cfg, ToSchema cfg, KnownSymbol (FeatureSymbol cfg)) => WithStatus cfg -> Event +mkUpdateEvent :: forall cfg. (IsFeatureConfig cfg) => LockableFeature cfg -> Event mkUpdateEvent ws = Event Update (featureName @cfg) (toJSON ws) diff --git a/libs/wire-api/src/Wire/API/Event/Gundeck.hs b/libs/wire-api/src/Wire/API/Event/Gundeck.hs new file mode 100644 index 00000000000..86ec39c2e27 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Event/Gundeck.hs @@ -0,0 +1,24 @@ +module Wire.API.Event.Gundeck where + +import Data.Aeson +import Data.Aeson.KeyMap qualified as KeyMap +import Data.Json.Util +import Imports +import Wire.API.Push.V2.Token + +newtype PushRemove = PushRemove PushToken + deriving (Eq, Show) + +instance FromJSON PushRemove where + parseJSON = withObject "push-removed object" $ \o -> + PushRemove <$> o .: "token" + +instance ToJSON PushRemove where + toJSON = Object . toJSONObject + +instance ToJSONObject PushRemove where + toJSONObject (PushRemove t) = + KeyMap.fromList + [ "type" .= ("user.push-remove" :: Text), + "token" .= t + ] diff --git a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs index a286a02d0a1..5f93d01f8c3 100644 --- a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs +++ b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs @@ -30,9 +30,7 @@ module Wire.API.MLS.CipherSuite IsSignatureScheme, SignatureSchemeTag (..), SignatureSchemeCurve, - signatureScheme, signatureSchemeName, - signatureSchemeTag, csSignatureScheme, -- * Key pairs @@ -282,9 +280,6 @@ newtype SignatureScheme = SignatureScheme {unSignatureScheme :: Word16} deriving stock (Eq, Show) deriving newtype (ParseMLS, Arbitrary) -signatureScheme :: SignatureSchemeTag -> SignatureScheme -signatureScheme = SignatureScheme . signatureSchemeNumber - data SignatureSchemeTag = Ed25519 | Ecdsa_secp256r1_sha256 @@ -330,23 +325,12 @@ instance Cql SignatureSchemeTag where signatureSchemeFromName name fromCql _ = Left "SignatureScheme: Text expected" -signatureSchemeNumber :: SignatureSchemeTag -> Word16 -signatureSchemeNumber Ed25519 = 0x807 -signatureSchemeNumber Ecdsa_secp256r1_sha256 = 0x403 -signatureSchemeNumber Ecdsa_secp384r1_sha384 = 0x503 -signatureSchemeNumber Ecdsa_secp521r1_sha512 = 0x603 - signatureSchemeName :: SignatureSchemeTag -> Text signatureSchemeName Ed25519 = "ed25519" signatureSchemeName Ecdsa_secp256r1_sha256 = "ecdsa_secp256r1_sha256" signatureSchemeName Ecdsa_secp384r1_sha384 = "ecdsa_secp384r1_sha384" signatureSchemeName Ecdsa_secp521r1_sha512 = "ecdsa_secp521r1_sha512" -signatureSchemeTag :: SignatureScheme -> Maybe SignatureSchemeTag -signatureSchemeTag (SignatureScheme n) = getAlt $ - flip foldMap [minBound .. maxBound] $ \s -> - guard (signatureSchemeNumber s == n) $> s - signatureSchemeFromName :: Text -> Maybe SignatureSchemeTag signatureSchemeFromName name = getAlt $ flip foldMap [minBound .. maxBound] $ \s -> diff --git a/libs/wire-api/src/Wire/API/MLS/Keys.hs b/libs/wire-api/src/Wire/API/MLS/Keys.hs index 545a9c1c5cf..d7b8ce85375 100644 --- a/libs/wire-api/src/Wire/API/MLS/Keys.hs +++ b/libs/wire-api/src/Wire/API/MLS/Keys.hs @@ -17,16 +17,21 @@ module Wire.API.MLS.Keys where +import Control.Lens ((?~)) import Crypto.ECC (Curve_P256R1, Curve_P384R1, Curve_P521R1) import Crypto.PubKey.ECDSA qualified as ECDSA import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.Aeson qualified as A import Data.Bifunctor +import Data.ByteArray (ByteArray) import Data.ByteArray qualified as BA +import Data.Default import Data.Json.Util import Data.OpenApi qualified as S import Data.Proxy import Data.Schema hiding (HasField) import Imports hiding (First, getFirst) +import Web.HttpApiData import Wire.API.MLS.CipherSuite data MLSKeysByPurpose a = MLSKeysByPurpose @@ -47,7 +52,7 @@ data MLSKeys a = MLSKeys ecdsa_secp384r1_sha384 :: a, ecdsa_secp521r1_sha512 :: a } - deriving (Eq, Show) + deriving (Eq, Show, Functor, Foldable, Traversable) deriving (FromJSON, ToJSON, S.ToSchema) via Schema (MLSKeys a) instance (ToSchema a) => ToSchema (MLSKeys a) where @@ -70,6 +75,7 @@ type MLSPublicKeys = MLSKeys MLSPublicKey newtype MLSPublicKey = MLSPublicKey {unwrapMLSPublicKey :: ByteString} deriving (Eq, Show) + deriving (ToJSON) via Schema MLSPublicKey instance ToSchema MLSPublicKey where schema = named "MLSPublicKey" $ MLSPublicKey <$> unwrapMLSPublicKey .= base64Schema @@ -83,6 +89,30 @@ mlsKeysToPublic (MLSPrivateKeys (_, ed) (_, ec256) (_, ec384) (_, ec521)) = ecdsa_secp521r1_sha512 = MLSPublicKey $ ECDSA.encodePublic (Proxy @Curve_P521R1) ec521 } +data MLSPublicKeyFormat = MLSPublicKeyFormatRaw | MLSPublicKeyFormatJWK + deriving (Eq, Ord, Show) + +instance Default MLSPublicKeyFormat where + def = MLSPublicKeyFormatRaw + +instance FromHttpApiData MLSPublicKeyFormat where + parseQueryParam "raw" = pure MLSPublicKeyFormatRaw + parseQueryParam "jwk" = pure MLSPublicKeyFormatJWK + parseQueryParam _ = Left "invalid MLSPublicKeyFormat" + +instance ToHttpApiData MLSPublicKeyFormat where + toQueryParam MLSPublicKeyFormatRaw = "raw" + toQueryParam MLSPublicKeyFormatJWK = "jwk" + +instance S.ToParamSchema MLSPublicKeyFormat where + toParamSchema _ = + mempty + & S.type_ ?~ S.OpenApiString + & S.enum_ + ?~ map + (toJSON . toQueryParam) + [MLSPublicKeyFormatRaw, MLSPublicKeyFormatJWK] + data JWK = JWK { keyType :: String, curve :: String, @@ -90,6 +120,7 @@ data JWK = JWK pubY :: Maybe ByteString } deriving (Show, Ord, Eq) + deriving (ToJSON) via Schema JWK instance ToSchema JWK where schema = @@ -102,10 +133,8 @@ instance ToSchema JWK where type MLSPublicKeysJWK = MLSKeys JWK -mlsKeysToPublicJWK :: - MLSPrivateKeys -> - Maybe MLSPublicKeysJWK -mlsKeysToPublicJWK (MLSPrivateKeys (_, ed) (_, ec256) (_, ec384) (_, ec521)) = +mlsPublicKeysToJWK :: MLSPublicKeys -> Maybe MLSPublicKeysJWK +mlsPublicKeysToJWK (MLSKeys (MLSPublicKey ed) (MLSPublicKey ec256) (MLSPublicKey ec384) (MLSPublicKey ec521)) = -- The kty parameter for ECDSA is "EC", for Ed25519 it's "OKP" (octet key -- pair). -- https://www.rfc-editor.org/rfc/rfc7518.html#section-6.1 @@ -116,16 +145,18 @@ mlsKeysToPublicJWK (MLSPrivateKeys (_, ed) (_, ec256) (_, ec384) (_, ec521)) = -- The x parameter is mandatory for all keys, the y parameter is mandatory for -- all ECDSA keys. -- https://www.rfc-editor.org/rfc/rfc7518.html#section-6.2.1 - MLSKeys (JWK "OKP" "Ed25519" (BA.convert ed) Nothing) - <$> (uncurry (JWK "EC" "P-256") . second Just <$> splitXY (ECDSA.encodePublic (Proxy @Curve_P256R1) ec256)) - <*> (uncurry (JWK "EC" "P-384") . second Just <$> splitXY (ECDSA.encodePublic (Proxy @Curve_P384R1) ec384)) - <*> (uncurry (JWK "EC" "P-521") . second Just <$> splitXY (ECDSA.encodePublic (Proxy @Curve_P521R1) ec521)) + MLSKeys + (JWK "OKP" "Ed25519" (BA.convert ed) Nothing) + <$> (uncurry (JWK "EC" "P-256") . second Just <$> splitXY ec256) + <*> (uncurry (JWK "EC" "P-384") . second Just <$> splitXY ec384) + <*> (uncurry (JWK "EC" "P-521") . second Just <$> splitXY ec521) where -- Obtaining X and Y from an encoded curve point follows the logic of -- Crypto.ECC's encodeECPoint and decodeECPoint (the module is not -- exported). Points need to be encoded in uncompressed representation. This -- is true for ECDSA.encodePublic. -- https://www.rfc-editor.org/rfc/rfc8422#section-5.4.1 + splitXY :: forall {bs}. (ByteArray bs) => bs -> Maybe (bs, bs) splitXY mxy = do (m, xy) <- BA.uncons mxy -- The first Byte m is 4 for the uncompressed representation of curve points. @@ -134,3 +165,15 @@ mlsKeysToPublicJWK (MLSPrivateKeys (_, ed) (_, ec256) (_, ec384) (_, ec521)) = -- to Y. let size = BA.length xy `div` 2 pure $ BA.splitAt size xy + +data SomeKey = SomeKey A.Value + +instance ToSchema SomeKey where + schema = mkSchema d r w + where + d = pure $ S.NamedSchema (Just "SomeKey") mempty + r = fmap SomeKey . parseJSON + w (SomeKey x) = Just (toJSON x) + +mkSomeKey :: (ToJSON a) => a -> SomeKey +mkSomeKey = SomeKey . toJSON diff --git a/libs/wire-api/src/Wire/API/MLS/Message.hs b/libs/wire-api/src/Wire/API/MLS/Message.hs index 342bb739e23..cb1003ab8fe 100644 --- a/libs/wire-api/src/Wire/API/MLS/Message.hs +++ b/libs/wire-api/src/Wire/API/MLS/Message.hs @@ -30,9 +30,6 @@ module Wire.API.MLS.Message FramedContentAuthData (..), Sender (..), - -- * Utilities - verifyMessageSignature, - -- * Servant types MLSMessageSendingStatus (..), ) @@ -48,7 +45,6 @@ import GHC.Records import Imports import Test.QuickCheck hiding (label) import Wire.API.Event.Conversation -import Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit import Wire.API.MLS.Epoch import Wire.API.MLS.Group @@ -238,11 +234,6 @@ instance SerialiseMLS Sender where serialiseMLS SenderNewMemberCommit = serialiseMLS SenderNewMemberCommitTag -needsGroupContext :: Sender -> Bool -needsGroupContext (SenderMember _) = True -needsGroupContext (SenderExternal _) = True -needsGroupContext _ = False - -- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-6-4 data FramedContent = FramedContent { groupId :: GroupId, @@ -329,15 +320,6 @@ instance SerialiseMLS FramedContentTBS where serialiseMLS tbs.content traverse_ serialiseMLS tbs.groupContext -framedContentTBS :: RawMLS GroupContext -> RawMLS FramedContent -> FramedContentTBS -framedContentTBS ctx msgContent = - FramedContentTBS - { protocolVersion = defaultProtocolVersion, - wireFormat = WireFormatPublicTag, - content = msgContent, - groupContext = guard (needsGroupContext msgContent.value.sender) $> ctx - } - -- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-6.1-2 data FramedContentAuthData = FramedContentAuthData { signature_ :: ByteString, @@ -359,18 +341,6 @@ instance SerialiseMLS FramedContentAuthData where serialiseMLSBytes @VarInt ad.signature_ traverse_ (serialiseMLSBytes @VarInt) ad.confirmationTag -verifyMessageSignature :: - RawMLS GroupContext -> - RawMLS FramedContent -> - RawMLS FramedContentAuthData -> - ByteString -> - Bool -verifyMessageSignature ctx msgContent authData pubkey = isJust $ do - let tbs = mkRawMLS (framedContentTBS ctx msgContent) - sig = authData.value.signature_ - cs <- cipherSuiteTag ctx.value.cipherSuite - guard $ csVerifySignature cs pubkey tbs sig - -------------------------------------------------------------------------------- -- Servant diff --git a/libs/wire-api/src/Wire/API/MLS/Serialisation.hs b/libs/wire-api/src/Wire/API/MLS/Serialisation.hs index 618d26201bf..ca6783cd192 100644 --- a/libs/wire-api/src/Wire/API/MLS/Serialisation.hs +++ b/libs/wire-api/src/Wire/API/MLS/Serialisation.hs @@ -23,7 +23,6 @@ module Wire.API.MLS.Serialisation SerialiseMLS (..), VarInt (..), parseMLSStream, - serialiseMLSStream, parseMLSVector, serialiseMLSVector, parseMLSBytes, @@ -129,9 +128,6 @@ parseMLSStream p = do then pure [] else (:) <$> p <*> parseMLSStream p -serialiseMLSStream :: (a -> Put) -> [a] -> Put -serialiseMLSStream = traverse_ - parseMLSVector :: forall w a. (Binary w, Integral w) => Get a -> Get [a] parseMLSVector getItem = do len <- get @w diff --git a/libs/wire-api/src/Wire/API/MakesFederatedCall.hs b/libs/wire-api/src/Wire/API/MakesFederatedCall.hs deleted file mode 100644 index 2a59e5648fb..00000000000 --- a/libs/wire-api/src/Wire/API/MakesFederatedCall.hs +++ /dev/null @@ -1,412 +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 . -{-# LANGUAGE OverloadedLists #-} -{-# OPTIONS_GHC -Wno-redundant-constraints #-} - -module Wire.API.MakesFederatedCall - ( CallsFed, - MakesFederatedCall, - Component (..), - callsFed, - unsafeCallsFed, - AddAnnotation, - Location (..), - ShowComponent, - Annotation, - HasFeds (..), - FedCallFrom' (..), - Calls (..), - Wire.API.MakesFederatedCall.exposeAnnotations, - ) -where - -import Control.Lens ((%~)) -import Control.Monad.State (State, evalState, get, gets, modify) -import Data.Aeson -import Data.ByteString.Char8 (unpack) -import Data.Constraint -import Data.Kind -import Data.Map qualified as M -import Data.Metrics.Servant -import Data.OpenApi qualified as S -import Data.Proxy -import Data.Schema -import Data.Text qualified as T -import GHC.TypeLits -import Imports -import Servant.API -import Servant.API.Extended (ReqBodyCustomError') -import Servant.API.Extended.RawM qualified as RawM -import Servant.Client -import Servant.Multipart -import Servant.OpenApi -import Servant.Server -import Test.QuickCheck (Arbitrary) -import TransitiveAnns.Types -import Unsafe.Coerce (unsafeCoerce) -import Wire.API.Deprecated (Deprecated) -import Wire.API.Error (CanThrow, CanThrowMany) -import Wire.API.Routes.Bearer (Bearer) -import Wire.API.Routes.Cookies (Cookies) -import Wire.API.Routes.LowLevelStream (LowLevelStream) -import Wire.API.Routes.MultiVerb (MultiVerb) -import Wire.API.Routes.Named -import Wire.API.Routes.Public -import Wire.API.Routes.QualifiedCapture (QualifiedCapture', WithDomain) -import Wire.API.Routes.Version -import Wire.API.Routes.Versioned (VersionedReqBody) -import Wire.API.Routes.WebSocket (WebSocketPending) -import Wire.API.SwaggerServant (OmitDocs) -import Wire.Arbitrary (GenericUniform (..)) - --- | This function exists only to provide a convenient place for the --- @transitive-anns@ plugin to solve the 'ToHasAnnotations' constraint. This is --- highly magical and warrants a note. --- --- The call @'exposeAnnotations' (some expr here)@ will expand to @some expr --- here@, additionally generating wanted 'HasAnnotation' constraints for every --- 'AddAnnotation' constraint in the _transitive call closure_ of @some expr --- here@. --- --- The use case is always going to be @'callsFed' ('exposeAnnotations' expr)@, --- where 'exposeAnnotations' re-introduces all of the constraints we've been --- squirreling away, and 'callsFed' is responsible for discharging them. It --- would be very desirable to combine these into one call, but the semantics of --- solving 'ToHasAnnotations' attaches the wanted calls to the same place as --- the call itself, which means the wanteds appear just after our opportunity --- to solve them via 'callsFed'. This is likely not a hard limitation. --- --- The @x@ parameter here is intentionally ambiguous, existing as a unique --- skolem to prevent GHC from caching the results of solving --- 'ToHasAnnotations'. Callers needn't worry about it. -exposeAnnotations :: (ToHasAnnotations x) => a -> a -exposeAnnotations = id - -data Component - = Brig - | Galley - | Cargohold - deriving (Show, Eq, Generic) - deriving (Arbitrary) via (GenericUniform Component) - deriving (ToJSON, FromJSON) via (Schema Component) - -instance ToSchema Component where - schema = - enum @Text "Component" $ - mconcat - [ element "brig" Brig, - element "galley" Galley, - element "cargohold" Cargohold - ] - -instance FromHttpApiData Component where - parseUrlPiece :: Text -> Either Text Component - parseUrlPiece = \case - "brig" -> Right Brig - "galley" -> Right Galley - "cargohold" -> Right Cargohold - c -> Left $ "Invalid component: " <> c - -instance ToHttpApiData Component where - toUrlPiece = \case - Brig -> "brig" - Galley -> "galley" - Cargohold -> "cargohold" - --- | A typeclass corresponding to calls to federated services. This class has --- no methods, and exists only to automatically propagate information up to --- servant. --- --- The only way to discharge this constraint is via 'callsFed', which should be --- invoked for each federated call when connecting handlers to the server --- definition. -type CallsFed (comp :: Component) = HasAnnotation 'Remote (ShowComponent comp) - --- | A typeclass with the same layout as 'CallsFed', which exists only so we --- can discharge 'CallsFeds' constraints by unsafely coercing this one. -class Nullary - -instance Nullary - --- | Construct a dictionary for 'CallsFed'. -synthesizeCallsFed :: forall (comp :: Component) (name :: Symbol). Dict (CallsFed comp name) -synthesizeCallsFed = unsafeCoerce $ Dict @Nullary - --- | Servant combinator for tracking calls to federated calls. Annotating API --- endpoints with 'MakesFederatedCall' is the only way to eliminate 'CallsFed' --- constraints on handlers. -data MakesFederatedCall (comp :: Component) (name :: Symbol) - -instance (HasServer api ctx) => HasServer (MakesFederatedCall comp name :> api :: Type) ctx where - -- \| This should have type @CallsFed comp name => ServerT api m@, but GHC - -- complains loudly thinking this is a polytype. We need to introduce the - -- 'CallsFed' constraint so that we can eliminate it via - -- 'synthesizeCallsFed', which otherwise is too-high rank for GHC to notice - -- we've solved our constraint. - type ServerT (MakesFederatedCall comp name :> api) m = Dict (CallsFed comp name) -> ServerT api m - route _ ctx f = route (Proxy @api) ctx $ fmap ($ synthesizeCallsFed @comp @name) f - hoistServerWithContext _ ctx f s = hoistServerWithContext (Proxy @api) ctx f . s - -instance (HasLink api) => HasLink (MakesFederatedCall comp name :> api :: Type) where - type MkLink (MakesFederatedCall comp name :> api) x = MkLink api x - toLink f _ l = toLink f (Proxy @api) l - -instance (RoutesToPaths api) => RoutesToPaths (MakesFederatedCall comp name :> api :: Type) where - getRoutes = getRoutes @api - --- | Get a symbol representation of our component. -type family ShowComponent (x :: Component) = (res :: Symbol) | res -> x where - ShowComponent 'Brig = "brig" - ShowComponent 'Galley = "galley" - ShowComponent 'Cargohold = "cargohold" - -type instance - SpecialiseToVersion v (MakesFederatedCall comp name :> api) = - MakesFederatedCall comp name :> SpecialiseToVersion v api - --- | 'MakesFederatedCall' annotates the swagger documentation with an extension --- tag @x-wire-makes-federated-calls-to@. -instance (HasOpenApi api, KnownSymbol name, KnownSymbol (ShowComponent comp)) => HasOpenApi (MakesFederatedCall comp name :> api :: Type) where - toOpenApi _ = - toOpenApi (Proxy @api) - -- Append federated call line to the description of routes - -- that perform calls to federation members. - & S.allOperations - . S.description - %~ pure . maybe call (\d -> d <> "
" <> call) - where - call :: Text - call = - T.pack "Calls federation service " - <> T.pack (symbolVal $ Proxy @(ShowComponent comp)) - <> T.pack " on " - <> T.pack (symbolVal $ Proxy @name) - -instance (HasClient m api) => HasClient m (MakesFederatedCall comp name :> api :: Type) where - type Client m (MakesFederatedCall comp name :> api) = Client m api - clientWithRoute p _ = clientWithRoute p $ Proxy @api - hoistClientMonad p _ f c = hoistClientMonad p (Proxy @api) f c - --- | Type class to automatically lift a function of the form @(c1, c2, ...) => --- r@ into @Dict c1 -> Dict c2 -> ... -> r@. -class SolveCallsFed c r a where - -- | Safely discharge a 'CallsFed' constraint. Intended to be used when - -- connecting your handler to the server router. - -- - -- This function should always be called with an argument of - -- 'exposeAnnotations'. See the documentation there for more information on - -- why. - callsFed :: ((c) => r) -> a - -instance (c ~ ((k, d) :: Constraint), SolveCallsFed d r a) => SolveCallsFed c r (Dict k -> a) where - callsFed f Dict = callsFed @d @r @a f - -instance {-# OVERLAPPABLE #-} (c ~ (() :: Constraint), r ~ a) => SolveCallsFed c r a where - callsFed f = f - --- | Unsafely discharge a 'CallsFed' constraint. Necessary for interacting with --- wai-routes. --- --- This is unsafe in the sense that it will drop the 'CallsFed' constraint, and --- thus might mean a federated call gets forgotten in the documentation. -unsafeCallsFed :: forall (comp :: Component) (name :: Symbol) r. ((CallsFed comp name) => r) -> r -unsafeCallsFed f = withDict (synthesizeCallsFed @comp @name) f - -data FedCallFrom' f = FedCallFrom - { name :: f String, - method :: f String, - fedCalls :: Calls - } - -deriving instance Show (FedCallFrom' Maybe) - -deriving instance Show (FedCallFrom' Identity) - -type FedCallFrom = FedCallFrom' Maybe - --- Merge the maps, perserving as much unique info as possible. -instance Semigroup (FedCallFrom' Maybe) where - a <> b = - FedCallFrom - (name a <|> name b) - (method a <|> method b) - (fedCalls a <> fedCalls b) - -instance Semigroup (FedCallFrom' Identity) where - a <> b = - FedCallFrom - (name a) - (method a) - (fedCalls a <> fedCalls b) - -instance Monoid FedCallFrom where - mempty = FedCallFrom mempty mempty mempty - -newtype Calls = Calls - { unCalls :: Map String [String] - } - deriving (Eq, Ord, Show) - -instance Semigroup Calls where - Calls a <> Calls b = Calls $ M.unionWith (\na nb -> nub . sort $ na <> nb) a b - -instance Monoid Calls where - mempty = Calls mempty - -class HasFeds a where - getFedCalls :: Proxy a -> State FedCallFrom [FedCallFrom] - --- Here onwards are all of the interesting instances that have something we care about -instance (KnownSymbol seg, HasFeds rest) => HasFeds (seg :> rest) where - getFedCalls _ = do - let segString = "/" <> T.unpack (T.dropAround (== '"') $ renderSymbol @seg) - modify $ appendName segString - getFedCalls $ Proxy @rest - -instance (KnownSymbol capture, HasFeds rest) => HasFeds (Capture' mods capture a :> rest) where - getFedCalls _ = do - let segString = "/{" <> T.unpack (T.dropAround (== '"') $ renderSymbol @capture) <> "}" - modify $ appendName segString - getFedCalls $ Proxy @rest - -instance (KnownSymbol capture, KnownSymbol (AppendSymbol capture "_domain"), HasFeds rest) => HasFeds (QualifiedCapture' mods capture a :> rest) where - getFedCalls _ = getFedCalls $ Proxy @(WithDomain mods capture a rest) - -instance (ReflectMethod method) => HasFeds (LowLevelStream method status headers desc ctype) where - getFedCalls _ = do - modify $ \s -> s {method = getMethod @method} - gets pure - -instance (HasFeds rest, KnownSymbol (ShowComponent comp), KnownSymbol name) => HasFeds (MakesFederatedCall comp name :> rest) where - getFedCalls _ = do - let call = - M.singleton - (symbolVal $ Proxy @(ShowComponent comp)) - (pure (symbolVal $ Proxy @name)) - modify $ \s -> s {fedCalls = fedCalls s <> Calls call} - getFedCalls $ Proxy @rest - -instance (ReflectMethod method) => HasFeds (MultiVerb method cs as r) where - getFedCalls _ = do - modify $ \s -> s {method = getMethod @method} - gets pure - -instance (ReflectMethod method) => HasFeds (Verb method status cts a) where - getFedCalls _ = do - modify $ \s -> s {method = getMethod @method} - gets pure - -instance (ReflectMethod method) => HasFeds (NoContentVerb method) where - getFedCalls _ = do - modify $ \s -> s {method = getMethod @method} - gets pure - -instance (ReflectMethod method) => HasFeds (Stream method status framing ct a) where - getFedCalls _ = do - modify $ \s -> s {method = getMethod @method} - gets pure - -instance HasFeds WebSocketPending where - getFedCalls _ = do - modify $ \s -> s {method = pure $ show GET} - gets pure - -instance (HasFeds route, HasFeds routes) => HasFeds (route :<|> routes) where - getFedCalls _ = do - s <- get - -- Use what state we have up until now, as it might be a funky style of endpoint. - -- Routes will usually specify their own name, as we don't have a style of sharing - -- a route name between several HTTP methods. - let a = evalState (getFedCalls $ Proxy @route) s - b = evalState (getFedCalls $ Proxy @routes) s - pure $ a <> b - -instance HasFeds EmptyAPI where - getFedCalls _ = gets pure - -instance HasFeds Raw where - getFedCalls _ = gets pure - -instance HasFeds RawM.RawM where - getFedCalls _ = gets pure - -getMethod :: forall method. (ReflectMethod method) => Maybe String -getMethod = pure . fmap toLower . unpack . reflectMethod $ Proxy @method - -appendName :: String -> FedCallFrom -> FedCallFrom -appendName toAppend s = s {name = pure $ maybe toAppend (<> toAppend) $ name s} - --- All of the boring instances live here. -instance (RenderableSymbol name, HasFeds rest) => HasFeds (Named name rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (Header' mods name a :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (ReqBody' mods cts a :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (StreamBody' opts framing ct a :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (Summary summary :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (QueryParam' mods name a :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (MultipartForm tag a :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (QueryFlag a :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (Description desc :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (Deprecated :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (CanThrow e :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (CanThrowMany es :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (Bearer a :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (Cookies cs :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (ZHostOpt :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (ZAuthServant ztype opts :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (ReqBodyCustomError' mods cts tag a :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (DescriptionOAuthScope scope :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (OmitDocs :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (VersionedReqBody v cts a :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest diff --git a/libs/wire-api/src/Wire/API/Message/Proto.hs b/libs/wire-api/src/Wire/API/Message/Proto.hs index d20ecd75ab6..68632c74b47 100644 --- a/libs/wire-api/src/Wire/API/Message/Proto.hs +++ b/libs/wire-api/src/Wire/API/Message/Proto.hs @@ -24,7 +24,6 @@ module Wire.API.Message.Proto userId, fromUserId, ClientId, - clientId, newClientId, fromClientId, toClientId, @@ -86,9 +85,6 @@ instance Decode ClientId newClientId :: Word64 -> ClientId newClientId c = ClientId {_client = putField c} -clientId :: (Functor f) => (Word64 -> f Word64) -> ClientId -> f ClientId -clientId f c = (\x -> c {_client = x}) <$> field f (_client c) - toClientId :: ClientId -> Id.ClientId toClientId c = Id.ClientId $ getField (_client c) @@ -150,6 +146,7 @@ userEntryClients f c = (\x -> c {_userVal = x}) <$> field f (_userVal c) -------------------------------------------------------------------------------- -- Priority +-- | See also `Wire.API.Message.Priority` data Priority = LowPriority | HighPriority deriving stock (Eq, Show, Ord, Generic) diff --git a/libs/wire-api/src/Wire/API/OAuth.hs b/libs/wire-api/src/Wire/API/OAuth.hs index 89c28f98370..97c8d0bc223 100644 --- a/libs/wire-api/src/Wire/API/OAuth.hs +++ b/libs/wire-api/src/Wire/API/OAuth.hs @@ -26,9 +26,9 @@ import Data.Aeson.Types qualified as A import Data.ByteArray (convert) import Data.ByteString.Conversion import Data.ByteString.Lazy (fromStrict, toStrict) -import Data.Either.Combinators (mapLeft) import Data.HashMap.Strict qualified as HM import Data.Id as Id +import Data.Json.Util import Data.OpenApi (ToParamSchema (..)) import Data.OpenApi qualified as S import Data.Range @@ -650,9 +650,28 @@ instance ToSchema OAuthRevokeRefreshTokenRequest where clientIdDescription = description ?~ "The OAuth client's ID" refreshTokenDescription = description ?~ "The refresh token" +data OAuthSession = OAuthSession + { refreshTokenId :: OAuthRefreshTokenId, + createdAt :: UTCTimeMillis + } + deriving (Eq, Show, Ord, Generic) + deriving (Arbitrary) via (GenericUniform OAuthSession) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema OAuthSession) + +instance ToSchema OAuthSession where + schema = + object "OAuthSession" $ + OAuthSession + <$> (.refreshTokenId) .= fieldWithDocModifier "refresh_token_id" refreshTokenIdDescription schema + <*> (.createdAt) .= fieldWithDocModifier "created_at" createdAtDescription schema + where + refreshTokenIdDescription = description ?~ "The ID of the refresh token" + createdAtDescription = description ?~ "The time when the session was created" + data OAuthApplication = OAuthApplication { applicationId :: OAuthClientId, - name :: OAuthApplicationName + name :: OAuthApplicationName, + sessions :: [OAuthSession] } deriving (Eq, Show, Ord, Generic) deriving (Arbitrary) via (GenericUniform OAuthApplication) @@ -662,13 +681,13 @@ instance ToSchema OAuthApplication where schema = object "OAuthApplication" $ OAuthApplication - <$> applicationId - .= fieldWithDocModifier "id" idDescription schema - <*> (.name) - .= fieldWithDocModifier "name" nameDescription schema + <$> applicationId .= fieldWithDocModifier "id" idDescription schema + <*> (.name) .= fieldWithDocModifier "name" nameDescription schema + <*> sessions .= fieldWithDocModifier "sessions" sessionsDescription (array schema) where idDescription = description ?~ "The OAuth client's ID" nameDescription = description ?~ "The OAuth client's name" + sessionsDescription = description ?~ "The OAuth client's sessions" -------------------------------------------------------------------------------- -- Errors diff --git a/libs/wire-api/src/Wire/API/Password.hs b/libs/wire-api/src/Wire/API/Password.hs index 6090f9ae6c7..172c987b4d1 100644 --- a/libs/wire-api/src/Wire/API/Password.hs +++ b/libs/wire-api/src/Wire/API/Password.hs @@ -1,4 +1,6 @@ {-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE StrictData #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -15,41 +17,47 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} -{-# OPTIONS_GHC -Wno-unused-top-binds #-} module Wire.API.Password - ( Password, + ( Password (..), PasswordStatus (..), genPassword, - mkSafePasswordScrypt, - mkSafePasswordArgon2id, + mkSafePassword, verifyPassword, verifyPasswordWithStatus, - unsafeMkPassword, + PasswordReqBody (..), + argon2OptsFromHashingOpts, + + -- * Only for testing hashPasswordArgon2idWithSalt, - hashPasswordArgon2idWithOptions, + mkSafePasswordScrypt, + parsePassword, ) where -import Cassandra +import Cassandra hiding (params) import Crypto.Error import Crypto.KDF.Argon2 qualified as Argon2 import Crypto.KDF.Scrypt as Scrypt import Crypto.Random +import Data.Aeson qualified as A import Data.ByteArray hiding (length) import Data.ByteString.Base64 qualified as B64 import Data.ByteString.Char8 qualified as C8 import Data.ByteString.Lazy (fromStrict, toStrict) import Data.Misc +import Data.OpenApi qualified as S +import Data.Schema import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import Imports import OpenSSL.Random (randBytes) +import Util.Options -- | A derived, stretched password that can be safely stored. -newtype Password = Password - {fromPassword :: Text} +data Password + = Argon2Password Argon2HashedPassword + | ScryptPassword ScryptHashedPassword instance Show Password where show _ = "" @@ -57,13 +65,26 @@ instance Show Password where instance Cql Password where ctype = Tagged BlobColumn - fromCql (CqlBlob lbs) = pure . Password . Text.decodeUtf8 . toStrict $ lbs + fromCql (CqlBlob lbs) = parsePassword . Text.decodeUtf8 . toStrict $ lbs fromCql _ = Left "password: expected blob" - toCql = CqlBlob . fromStrict . Text.encodeUtf8 . fromPassword + toCql pw = CqlBlob . fromStrict $ Text.encodeUtf8 encoded + where + encoded = case pw of + Argon2Password argon2pw -> encodeArgon2HashedPassword argon2pw + ScryptPassword scryptpw -> encodeScryptPassword scryptpw + +data Argon2HashedPassword = Argon2HashedPassword + { opts :: Argon2.Options, + salt :: ByteString, + hashedKey :: ByteString + } -unsafeMkPassword :: Text -> Password -unsafeMkPassword = Password +data ScryptHashedPassword = ScryptHashedPassword + { params :: ScryptParameters, + salt :: ByteString, + hashedKey :: ByteString + } data PasswordStatus = PasswordStatusOk @@ -72,8 +93,6 @@ data PasswordStatus ------------------------------------------------------------------------------- -type Argon2idOptions = Argon2.Options - data ScryptParameters = ScryptParameters { -- | Bytes to randomly generate as a unique salt, default is __32__ saltLength :: Word32, @@ -102,17 +121,6 @@ defaultScryptParams = outputLength = 64 } --- | These are the default values suggested, as extracted from the crypton library. -defaultOptions :: Argon2idOptions -defaultOptions = - Argon2.Options - { iterations = 5, - memory = 2 ^ (17 :: Int), - parallelism = 4, - variant = Argon2.Argon2id, - version = Argon2.Version13 - } - fromScrypt :: ScryptParameters -> Parameters fromScrypt scryptParams = Parameters @@ -122,6 +130,16 @@ fromScrypt scryptParams = outputLength = 64 } +argon2OptsFromHashingOpts :: Argon2idOptions -> Argon2.Options +argon2OptsFromHashingOpts Argon2idOptions {..} = + Argon2.Options + { variant = Argon2.Argon2id, + version = Argon2.Version13, + iterations = iterations, + memory = memory, + parallelism = parallelism + } + ------------------------------------------------------------------------------- -- | Generate a strong, random plaintext password of length 16 @@ -132,10 +150,10 @@ genPassword = randBytes 12 mkSafePasswordScrypt :: (MonadIO m) => PlainTextPassword' t -> m Password -mkSafePasswordScrypt = fmap Password . hashPasswordScrypt . Text.encodeUtf8 . fromPlainTextPassword +mkSafePasswordScrypt = fmap ScryptPassword . hashPasswordScrypt . Text.encodeUtf8 . fromPlainTextPassword -mkSafePasswordArgon2id :: (MonadIO m) => PlainTextPassword' t -> m Password -mkSafePasswordArgon2id = fmap Password . hashPasswordArgon2id . Text.encodeUtf8 . fromPlainTextPassword +mkSafePassword :: (MonadIO m) => Argon2.Options -> PlainTextPassword' t -> m Password +mkSafePassword opts = fmap Argon2Password . hashPasswordArgon2id opts . Text.encodeUtf8 . fromPlainTextPassword -- | Verify a plaintext password from user input against a stretched -- password from persistent storage. @@ -143,37 +161,46 @@ verifyPassword :: PlainTextPassword' t -> Password -> Bool verifyPassword = (fst .) . verifyPasswordWithStatus verifyPasswordWithStatus :: PlainTextPassword' t -> Password -> (Bool, PasswordStatus) -verifyPasswordWithStatus plain opaque = - let actual = fromPlainTextPassword plain - expected = fromPassword opaque - in checkPassword actual expected +verifyPasswordWithStatus (fromPlainTextPassword -> plain) hashed = + case hashed of + (Argon2Password Argon2HashedPassword {..}) -> + let producedKey = hashPasswordWithOptions opts (Text.encodeUtf8 plain) salt + in (hashedKey `constEq` producedKey, PasswordStatusOk) + (ScryptPassword ScryptHashedPassword {..}) -> + let producedKey = hashPasswordWithParams params (Text.encodeUtf8 plain) salt + in (hashedKey `constEq` producedKey, PasswordStatusNeedsUpdate) -hashPasswordScrypt :: (MonadIO m) => ByteString -> m Text +hashPasswordScrypt :: (MonadIO m) => ByteString -> m ScryptHashedPassword hashPasswordScrypt password = do salt <- newSalt $ fromIntegral defaultScryptParams.saltLength - let key = hashPasswordWithParams defaultScryptParams password salt - pure $ - Text.intercalate - "|" - [ showT defaultScryptParams.rounds, - showT defaultScryptParams.blockSize, - showT defaultScryptParams.parallelism, - Text.decodeUtf8 . B64.encode $ salt, - Text.decodeUtf8 . B64.encode $ key - ] - -hashPasswordArgon2id :: (MonadIO m) => ByteString -> m Text -hashPasswordArgon2id pwd = do - salt <- newSalt 32 - pure $ hashPasswordArgon2idWithSalt salt pwd - -hashPasswordArgon2idWithSalt :: ByteString -> ByteString -> Text -hashPasswordArgon2idWithSalt = hashPasswordArgon2idWithOptions defaultOptions - -hashPasswordArgon2idWithOptions :: Argon2idOptions -> ByteString -> ByteString -> Text -hashPasswordArgon2idWithOptions opts salt pwd = do - let key = hashPasswordWithOptions opts pwd salt - optsStr = + let params = defaultScryptParams + let hashedKey = hashPasswordWithParams params password salt + pure $! ScryptHashedPassword {..} + +encodeScryptPassword :: ScryptHashedPassword -> Text +encodeScryptPassword ScryptHashedPassword {..} = + Text.intercalate + "|" + [ showT defaultScryptParams.rounds, + showT defaultScryptParams.blockSize, + showT defaultScryptParams.parallelism, + Text.decodeUtf8 . B64.encode $ salt, + Text.decodeUtf8 . B64.encode $ hashedKey + ] + +hashPasswordArgon2id :: (MonadIO m) => Argon2.Options -> ByteString -> m Argon2HashedPassword +hashPasswordArgon2id opts pwd = do + salt <- newSalt 16 + pure $! hashPasswordArgon2idWithSalt opts salt pwd + +hashPasswordArgon2idWithSalt :: Argon2.Options -> ByteString -> ByteString -> Argon2HashedPassword +hashPasswordArgon2idWithSalt opts salt pwd = do + let hashedKey = hashPasswordWithOptions opts pwd salt + in Argon2HashedPassword {..} + +encodeArgon2HashedPassword :: Argon2HashedPassword -> Text +encodeArgon2HashedPassword Argon2HashedPassword {..} = + let optsStr = Text.intercalate "," [ "m=" <> showT opts.memory, @@ -187,96 +214,100 @@ hashPasswordArgon2idWithOptions opts salt pwd = do "v=" <> versionToNum opts.version, optsStr, encodeWithoutPadding salt, - encodeWithoutPadding key + encodeWithoutPadding hashedKey ] where encodeWithoutPadding = Text.dropWhileEnd (== '=') . Text.decodeUtf8 . B64.encode -checkPassword :: Text -> Text -> (Bool, PasswordStatus) -checkPassword actual expected = +parsePassword :: Text -> Either String Password +parsePassword expected = case parseArgon2idPasswordHashOptions expected of - Just (opts, salt, hashedKey) -> - let producedKey = hashPasswordWithOptions opts (Text.encodeUtf8 actual) salt - in (hashedKey `constEq` producedKey, PasswordStatusOk) - Nothing -> + Right hashedPassword -> Right $ Argon2Password hashedPassword + Left argon2ParseError -> case parseScryptPasswordHashParams $ Text.encodeUtf8 expected of - Just (sparams, saltS, hashedKeyS) -> - let producedKeyS = hashPasswordWithParams sparams (Text.encodeUtf8 actual) saltS - in (hashedKeyS `constEq` producedKeyS, PasswordStatusNeedsUpdate) - Nothing -> (False, PasswordStatusNeedsUpdate) + Right hashedPassword -> Right $ ScryptPassword hashedPassword + Left scryptParseError -> + Left $ + "Failed to parse Argon2 or Scrypt. Argon2 parse error: " + <> argon2ParseError + <> ", Scrypt parse error: " + <> scryptParseError newSalt :: (MonadIO m) => Int -> m ByteString newSalt i = liftIO $ getRandomBytes i {-# INLINE newSalt #-} -parseArgon2idPasswordHashOptions :: Text -> Maybe (Argon2idOptions, ByteString, ByteString) +parseArgon2idPasswordHashOptions :: Text -> Either String Argon2HashedPassword parseArgon2idPasswordHashOptions passwordHash = do - let paramList = Text.split (== '$') passwordHash - guard (length paramList >= 5) - let (_ : variantT : vp : ps : sh : rest) = paramList - variant <- parseVariant variantT - case rest of - [hashedKey64] -> do - version <- parseVersion vp - parseAll variant version ps sh hashedKey64 - [] -> parseAll variant Argon2.Version10 vp ps sh - _ -> Nothing - where - parseVariant = splitMaybe "argon2" letterToVariant - parseVersion = splitMaybe "v=" numToVersion - -parseAll :: Argon2.Variant -> Argon2.Version -> Text -> Text -> Text -> Maybe (Argon2idOptions, ByteString, ByteString) -parseAll variant version parametersT salt64 hashedKey64 = do - (memory, iterations, parallelism) <- parseParameters parametersT - salt <- from64 $ unsafePad64 salt64 - hashedKey <- from64 $ unsafePad64 hashedKey64 - pure (Argon2.Options {..}, salt, hashedKey) + let paramsList = Text.split (== '$') passwordHash + -- The first param is empty string b/c the string begins with a separator `$`. + case paramsList of + ["", variantStr, verStr, opts, salt, hashedKey64] -> do + version <- parseVersion verStr + parseAll variantStr version opts salt hashedKey64 + ["", variantStr, opts, salt, hashedKey64] -> do + parseAll variantStr Argon2.Version10 opts salt hashedKey64 + _ -> Left $ "failed to parse argon2id hashed password, expected 5 or 6 params, got: " <> show (length paramsList) where - parseParameters paramsT = do - let paramsL = Text.split (== ',') paramsT - guard $ Imports.length paramsL == 3 - go paramsL (Nothing, Nothing, Nothing) + parseVersion = + maybe (Left "failed to parse argon2 version") Right + . splitMaybe "v=" numToVersion + + parseAll :: Text -> Argon2.Version -> Text -> Text -> Text -> Either String Argon2HashedPassword + parseAll variantStr version parametersStr salt64 hashedKey64 = do + variant <- parseVariant variantStr + (memory, iterations, parallelism) <- parseParameters parametersStr + -- We pad the Base64 with '=' chars because we drop them while encoding this. + -- At the time of implementation we've opted to be consistent with how the + -- CLI of the reference implementation of Argon2id outputs this. + salt <- from64 $ unsafePad64 salt64 + hashedKey <- from64 $ unsafePad64 hashedKey64 + pure $ Argon2HashedPassword {opts = (Argon2.Options {..}), ..} where - go [] (Just m, Just t, Just p) = Just (m, t, p) - go [] _ = Nothing - go (x : xs) (m, t, p) = - case Text.splitAt 2 x of - ("m=", i) -> go xs (readT i, t, p) - ("t=", i) -> go xs (m, readT i, p) - ("p=", i) -> go xs (m, t, readT i) - _ -> Nothing - -parseScryptPasswordHashParams :: ByteString -> Maybe (ScryptParameters, ByteString, ByteString) + parseVariant = + maybe (Left "failed to parse argon2 variant") Right + . splitMaybe "argon2" letterToVariant + parseParameters paramsT = + let paramsList = Text.split (== ',') paramsT + in go paramsList (Nothing, Nothing, Nothing) + where + go [] (Just m, Just t, Just p) = Right (m, t, p) + go [] (Nothing, _, _) = Left "failed to parse Argon2Options: failed to read parameter 'm'" + go [] (_, Nothing, _) = Left "failed to parse Argon2Options: failed to read parameter 't'" + go [] (_, _, Nothing) = Left "failed to parse Argon2Options: failed to read parameter 'p'" + go (x : xs) (m, t, p) = + case Text.splitAt 2 x of + ("m=", i) -> go xs (readT i, t, p) + ("t=", i) -> go xs (m, readT i, p) + ("p=", i) -> go xs (m, t, readT i) + (unknownParam, _) -> Left $ "failed to parse Argon2Options: Unknown param: " <> Text.unpack unknownParam + +parseScryptPasswordHashParams :: ByteString -> Either String ScryptHashedPassword parseScryptPasswordHashParams passwordHash = do let paramList = Text.split (== '|') . Text.decodeUtf8 $ passwordHash - guard (length paramList == 5) - let [ scryptRoundsT, - scryptBlockSizeT, - scryptParallelismT, - salt64, - hashedKey64 - ] = paramList - rounds <- readT scryptRoundsT - blockSize <- readT scryptBlockSizeT - parallelism <- readT scryptParallelismT - salt <- from64 salt64 - hashedKey <- from64 hashedKey64 - let outputLength = fromIntegral $ C8.length hashedKey - saltLength = fromIntegral $ C8.length salt - pure - ( ScryptParameters {..}, - salt, - hashedKey - ) + case paramList of + [roundsStr, blockSizeStr, parallelismStr, salt64, hashedKey64] -> do + rounds <- eitherFromMaybe "rounds" $ readT roundsStr + blockSize <- eitherFromMaybe "blockSize" $ readT blockSizeStr + parallelism <- eitherFromMaybe "parellelism" $ readT parallelismStr + salt <- from64 salt64 + hashedKey <- from64 hashedKey64 + let outputLength = fromIntegral $ C8.length hashedKey + saltLength = fromIntegral $ C8.length salt + pure $ ScryptHashedPassword {params = ScryptParameters {..}, ..} + _ -> Left $ "failed to parse ScryptHashedPassword: expected exactly 5 params" + where + eitherFromMaybe :: String -> Maybe a -> Either String a + eitherFromMaybe paramName = maybe (Left $ "failed to parse scrypt parameter: " <> paramName) Right ------------------------------------------------------------------------------- -hashPasswordWithOptions :: Argon2idOptions -> ByteString -> ByteString -> ByteString -hashPasswordWithOptions opts password salt = - case (Argon2.hash opts password salt 64) of +hashPasswordWithOptions :: Argon2.Options -> ByteString -> ByteString -> ByteString +hashPasswordWithOptions opts password salt = do + let tagSize = 16 + case (Argon2.hash opts password salt tagSize) of -- CryptoFailed occurs when salt, output or input are too small/big. -- since we control those values ourselves, it should never have a runtime error - -- unless we've caused it ourselves. CryptoFailed cErr -> error $ "Impossible error: " <> show cErr CryptoPassed hash -> hash @@ -334,3 +365,17 @@ unsafePad64 t where remains = Text.length t `rem` 4 pad = Text.replicate (4 - remains) "=" + +-------------------------------------------------------------------------------- +-- Type that can be used to pass a plaintext password as a request body + +newtype PasswordReqBody = PasswordReqBody + {fromPasswordReqBody :: Maybe PlainTextPassword6} + deriving stock (Eq, Show) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema PasswordReqBody + +instance ToSchema PasswordReqBody where + schema = + object "PasswordReqBody" $ + PasswordReqBody + <$> fromPasswordReqBody .= maybe_ (optField "password" schema) diff --git a/libs/wire-api/src/Wire/API/Presence.hs b/libs/wire-api/src/Wire/API/Presence.hs new file mode 100644 index 00000000000..d2e8cb6a3d0 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Presence.hs @@ -0,0 +1,98 @@ +module Wire.API.Presence (Presence (..), URI (..), parse) where + +import Control.Lens ((?~)) +import Data.Aeson qualified as A +import Data.Aeson.Types qualified as A +import Data.Attoparsec.ByteString (takeByteString) +import Data.ByteString.Char8 qualified as Bytes +import Data.ByteString.Conversion +import Data.ByteString.Lazy qualified as Lazy +import Data.Id +import Data.Misc (Milliseconds) +import Data.OpenApi qualified as S +import Data.Proxy +import Data.Schema +import Data.Text qualified as Text +import Data.Text.Encoding (decodeUtf8) +import Imports +import Network.URI qualified as Net +import Servant.API (ToHttpApiData (toUrlPiece)) + +-- FUTUREWORK: use Network.URI and toss this newtype. servant should have all these instances for us these days. +newtype URI = URI + { fromURI :: Net.URI + } + deriving (Eq, Ord, Show) + +instance A.FromJSON URI where + parseJSON = A.withText "URI" (parse . Text.unpack) + +instance A.ToJSON URI where + toJSON uri = A.String $ Text.pack (show (fromURI uri)) + +instance ToByteString URI where + builder = builder . show . fromURI + +instance FromByteString URI where + parser = takeByteString >>= parse . Bytes.unpack + +instance ToHttpApiData URI where + toUrlPiece = decodeUtf8 . toByteString' + +instance S.ToParamSchema URI where + toParamSchema _ = + S.toParamSchema (Proxy @Text) + & S.type_ ?~ S.OpenApiString + & S.description ?~ "Valid URI" + +parse :: (MonadFail m) => String -> m URI +parse = maybe (fail "Invalid URI") (pure . URI) . Net.parseURI + +-- | This is created in gundeck by cannon every time the client opens a new websocket connection. +-- (That's why we always have a 'ConnId' from the most recent connection by that client.) +data Presence = Presence + { userId :: !UserId, + connId :: !ConnId, + -- | cannon instance hosting the presence + resource :: !URI, + -- | This is 'Nothing' if either (a) the presence is older + -- than mandatory end-to-end encryption, or (b) the client is + -- operating the team settings pages without the need for + -- end-to-end crypto. + clientId :: !(Maybe ClientId), + createdAt :: !Milliseconds, + -- | REFACTOR: temp. addition to ease migration + __field :: !Lazy.ByteString + } + deriving (Eq, Ord, Show) + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via (Schema Presence) + +instance ToSchema Presence where + schema = + object "Presence" $ + ( Presence + <$> userId .= field "user_id" schema + <*> connId .= field "device_id" schema + <*> resource .= field "resource" uriSchema + <*> clientId .= optField "client_id" (maybeWithDefault A.Null schema) -- keep null for backwards compat + <*> createdAt .= (fromMaybe 0 <$> (optField "created_at" schema)) + ) + <&> ($ ("" :: Lazy.ByteString)) + +uriSchema :: ValueSchema NamedSwaggerDoc URI +uriSchema = mkSchema desc uriFromJSON (Just . uriToJSON) + where + desc :: NamedSwaggerDoc + desc = + swaggerDoc @Text + & (S.schema . S.type_ ?~ S.OpenApiString) + & (S.schema . S.description ?~ "Valid URI.") + +uriFromJSON :: A.Value -> A.Parser URI +uriFromJSON = A.withText "URI" (p . Text.unpack) + where + p :: (MonadFail m) => String -> m URI + p = maybe (fail "Invalid URI") pure . parse + +uriToJSON :: URI -> A.Value +uriToJSON (URI uri) = A.String . Text.pack $ Net.uriToString id uri mempty diff --git a/libs/wire-api/src/Wire/API/Properties.hs b/libs/wire-api/src/Wire/API/Properties.hs index 83c8ee1aa50..67a3b5b554c 100644 --- a/libs/wire-api/src/Wire/API/Properties.hs +++ b/libs/wire-api/src/Wire/API/Properties.hs @@ -21,7 +21,6 @@ module Wire.API.Properties ( PropertyKeysAndValues (..), PropertyKey (..), RawPropertyValue (..), - PropertyValue (..), ) where @@ -35,9 +34,10 @@ import Data.OpenApi qualified as S import Data.Text.Ascii import Imports import Servant -import Wire.Arbitrary (Arbitrary) +import Test.QuickCheck -newtype PropertyKeysAndValues = PropertyKeysAndValues (Map PropertyKey PropertyValue) +newtype PropertyKeysAndValues = PropertyKeysAndValues (Map PropertyKey Value) + deriving stock (Eq, Show) deriving newtype (ToJSON) instance S.ToSchema PropertyKeysAndValues where @@ -72,6 +72,7 @@ deriving instance C.Cql PropertyKey -- | A raw, unparsed property value. newtype RawPropertyValue = RawPropertyValue {rawPropertyBytes :: LByteString} + deriving (Eq, Show) instance C.Cql RawPropertyValue where ctype = C.Tagged C.BlobColumn @@ -89,15 +90,3 @@ instance S.ToSchema RawPropertyValue where declareNamedSchema _ = pure . S.NamedSchema (Just "PropertyValue") $ mempty & S.description ?~ "An arbitrary JSON value for a property" - --- | A property value together with its original serialisation. -data PropertyValue = PropertyValue - { propertyRaw :: RawPropertyValue, - propertyValue :: Value - } - -instance ToJSON PropertyValue where - toJSON = propertyValue - -instance Show PropertyValue where - show = show . propertyValue diff --git a/libs/wire-api/src/Wire/API/Provider.hs b/libs/wire-api/src/Wire/API/Provider.hs index 1fc5c34c114..fbde37da2ce 100644 --- a/libs/wire-api/src/Wire/API/Provider.hs +++ b/libs/wire-api/src/Wire/API/Provider.hs @@ -61,7 +61,7 @@ import Imports import Wire.API.Conversation.Code as Code import Wire.API.Provider.Service (ServiceToken (..)) import Wire.API.Provider.Service.Tag (ServiceTag (..)) -import Wire.API.User.Identity (Email) +import Wire.API.User.EmailAddress import Wire.API.User.Profile (Name) import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -72,7 +72,7 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) data Provider = Provider { providerId :: ProviderId, providerName :: Name, - providerEmail :: Email, + providerEmail :: EmailAddress, providerUrl :: HttpsUrl, providerDescr :: Text } @@ -103,7 +103,7 @@ newtype ProviderProfile = ProviderProfile Provider -- | Input data for registering a new provider. data NewProvider = NewProvider { newProviderName :: Name, - newProviderEmail :: Email, + newProviderEmail :: EmailAddress, newProviderUrl :: HttpsUrl, newProviderDescr :: Range 1 1024 Text, -- | If none provided, a password is generated. @@ -168,7 +168,7 @@ instance ToSchema UpdateProvider where -- | Successful response upon activating an email address (or possibly phone -- number in the future) of a provider. newtype ProviderActivationResponse = ProviderActivationResponse - {activatedProviderIdentity :: Email} + {activatedProviderIdentity :: EmailAddress} deriving stock (Eq, Show) deriving newtype (Arbitrary) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema ProviderActivationResponse @@ -184,7 +184,7 @@ instance ToSchema ProviderActivationResponse where -- | Input data for a provider login request. data ProviderLogin = ProviderLogin - { providerLoginEmail :: Email, + { providerLoginEmail :: EmailAddress, providerLoginPassword :: PlainTextPassword6 } deriving stock (Eq, Show, Generic) @@ -202,6 +202,7 @@ instance ToSchema ProviderLogin where -- DeleteProvider -- | Input data for a provider deletion request. +-- | FUTUREWORK: look into a phase out of PlainTextPassword6 newtype DeleteProvider = DeleteProvider {deleteProviderPassword :: PlainTextPassword6} deriving stock (Eq, Show) @@ -218,7 +219,7 @@ instance ToSchema DeleteProvider where -- Password Change/Reset -- | The payload for initiating a password reset. -newtype PasswordReset = PasswordReset {email :: Email} +newtype PasswordReset = PasswordReset {email :: EmailAddress} deriving stock (Eq, Show) deriving newtype (Arbitrary) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema PasswordReset @@ -264,7 +265,7 @@ instance ToSchema PasswordChange where <*> newPassword .= field "new_password" schema -- | The payload for updating an email address -newtype EmailUpdate = EmailUpdate {email :: Email} +newtype EmailUpdate = EmailUpdate {email :: EmailAddress} deriving stock (Eq, Show, Generic) deriving newtype (Arbitrary) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema EmailUpdate diff --git a/libs/wire-api/src/Wire/API/Provider/Service.hs b/libs/wire-api/src/Wire/API/Provider/Service.hs index 7b181183e1e..4589dc8dc69 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service.hs @@ -282,7 +282,7 @@ data ServiceProfilePage = ServiceProfilePage instance ToSchema ServiceProfilePage where schema = - object "ServiceProfile" $ + object "ServiceProfilePage" $ ServiceProfilePage <$> serviceProfilePageHasMore .= field "has_more" schema <*> serviceProfilePageResults .= field "services" (array schema) diff --git a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs index ec44311dece..95aaaebab1d 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs @@ -31,8 +31,6 @@ module Wire.API.Provider.Service.Tag -- * ServiceTag Matchers MatchAny (..), MatchAll (..), - (.||.), - (.&&.), matchAll, match1, match, @@ -182,7 +180,7 @@ instance ToByteString ServiceTag where builder WeatherTag = "weather" instance ToSchema ServiceTag where - schema = enum @Text "" . mconcat $ (\a -> element (decodeUtf8With lenientDecode $ toStrict $ toByteString a) a) <$> [minBound ..] + schema = enum @Text "ServiceTag" . mconcat $ (\a -> element (decodeUtf8With lenientDecode $ toStrict $ toByteString a) a) <$> [minBound ..] instance S.ToParamSchema ServiceTag where toParamSchema _ = @@ -305,12 +303,6 @@ newtype MatchAll = MatchAll {matchAllSet :: Set ServiceTag} deriving stock (Eq, Show, Ord) -(.||.) :: MatchAny -> MatchAny -> MatchAny -(.||.) (MatchAny a) (MatchAny b) = MatchAny (Set.union a b) - -(.&&.) :: MatchAll -> MatchAll -> MatchAll -(.&&.) (MatchAll a) (MatchAll b) = MatchAll (Set.union a b) - matchAll :: MatchAll -> MatchAny matchAll = MatchAny . Set.singleton diff --git a/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs b/libs/wire-api/src/Wire/API/Push/V2.hs similarity index 59% rename from libs/gundeck-types/src/Gundeck/Types/Push/V2.hs rename to libs/wire-api/src/Wire/API/Push/V2.hs index 6c0df12d8a5..1d24c9099d1 100644 --- a/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs +++ b/libs/wire-api/src/Wire/API/Push/V2.hs @@ -1,28 +1,10 @@ -{-# LANGUAGE DataKinds #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} --- 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 Gundeck.Types.Push.V2 +module Wire.API.Push.V2 ( Push (..), newPush, pushRecipients, @@ -35,7 +17,6 @@ module Gundeck.Types.Push.V2 pushNativeAps, pushNativePriority, pushPayload, - singletonRecipient, singletonPayload, Recipient (..), RecipientClients (..), @@ -72,14 +53,17 @@ module Gundeck.Types.Push.V2 ) where -import Control.Lens (makeLenses) -import Data.Aeson +import Control.Lens (makeLenses, (?~)) +import Data.Aeson (FromJSON (..), Object, ToJSON (..)) +import Data.Aeson qualified as A +import Data.Aeson.Types qualified as A import Data.Id import Data.Json.Util import Data.List1 import Data.List1 qualified as List1 +import Data.OpenApi qualified as S import Data.Range -import Data.Range qualified as Range +import Data.Schema import Data.Set qualified as Set import Imports import Wire.API.Message (Priority (..)) @@ -98,25 +82,28 @@ data Route RouteDirect deriving (Eq, Ord, Enum, Bounded, Show, Generic) deriving (Arbitrary) via GenericUniform Route + deriving (FromJSON, ToJSON, S.ToSchema) via (Schema Route) -instance FromJSON Route where - parseJSON (String "any") = pure RouteAny - parseJSON (String "direct") = pure RouteDirect - parseJSON x = fail $ "Invalid routing: " ++ show (encode x) - -instance ToJSON Route where - toJSON RouteAny = String "any" - toJSON RouteDirect = String "direct" +instance ToSchema Route where + schema = + enum @Text "Route" $ + mconcat + [ element "any" RouteAny, + element "direct" RouteDirect + ] ----------------------------------------------------------------------------- -- Recipient +-- FUTUREWORK: this is a duplicate of the type in "Wire.NotificationSubsystem" (even though +-- the latter lacks a few possibly deprecated fields). consolidate! data Recipient = Recipient { _recipientId :: !UserId, _recipientRoute :: !Route, _recipientClients :: !RecipientClients } deriving (Show, Eq, Ord, Generic) + deriving (FromJSON, ToJSON, S.ToSchema) via (Schema Recipient) data RecipientClients = -- | All clients of some user @@ -125,6 +112,7 @@ data RecipientClients RecipientClientsSome (List1 ClientId) deriving (Eq, Show, Ord, Generic) deriving (Arbitrary) via GenericUniform RecipientClients + deriving (FromJSON, ToJSON, S.ToSchema) via (Schema RecipientClients) instance Semigroup RecipientClients where RecipientClientsAll <> _ = RecipientClientsAll @@ -132,51 +120,73 @@ instance Semigroup RecipientClients where RecipientClientsSome cs1 <> RecipientClientsSome cs2 = RecipientClientsSome (cs1 <> cs2) +instance ToSchema Recipient where + schema = + object "Recipient" $ + Recipient + <$> _recipientId .= field "user_id" schema + <*> _recipientRoute .= field "route" schema + <*> _recipientClients .= field "clients" schema + +instance ToSchema RecipientClients where + schema = mkSchema d i o + where + d :: NamedSwaggerDoc + d = + swaggerDoc @[ClientId] + & (S.schema . S.type_ ?~ S.OpenApiArray) + & (S.schema . S.description ?~ "List of clientIds. Empty means `all clients`.") + + i :: A.Value -> A.Parser RecipientClients + i v = + parseJSON @[ClientId] v >>= \case + [] -> pure RecipientClientsAll + c : cs -> pure (RecipientClientsSome (list1 c cs)) + + o :: RecipientClients -> Maybe A.Value + o = + pure . toJSON . \case + RecipientClientsSome cs -> toList cs + RecipientClientsAll -> [] + makeLenses ''Recipient recipient :: UserId -> Route -> Recipient recipient u r = Recipient u r RecipientClientsAll -instance FromJSON Recipient where - parseJSON = withObject "Recipient" $ \p -> - Recipient - <$> p .: "user_id" - <*> p .: "route" - <*> p .:? "clients" .!= RecipientClientsAll - -instance ToJSON Recipient where - toJSON (Recipient u r c) = - object $ - "user_id" - .= u - # "route" - .= r - # "clients" - .= c - # [] - --- "All clients" is encoded in the API as an empty list. -instance FromJSON RecipientClients where - parseJSON x = - parseJSON @[ClientId] x >>= \case - [] -> pure RecipientClientsAll - c : cs -> pure (RecipientClientsSome (list1 c cs)) - -instance ToJSON RecipientClients where - toJSON = - toJSON . \case - RecipientClientsAll -> [] - RecipientClientsSome cs -> toList cs - ----------------------------------------------------------------------------- -- ApsData newtype ApsSound = ApsSound {fromSound :: Text} deriving (Eq, Show, ToJSON, FromJSON, Arbitrary) +instance ToSchema ApsSound where + schema = + mkSchema d i o + where + d = + swaggerDoc @Text + & (S.schema . S.type_ ?~ S.OpenApiString) + & (S.schema . S.description ?~ "ApsSound") + + i = A.withText "ApsSound" (pure . ApsSound) + o = pure . A.String . fromSound + newtype ApsLocKey = ApsLocKey {fromLocKey :: Text} deriving (Eq, Show, ToJSON, FromJSON, Arbitrary) +instance ToSchema ApsLocKey where + schema = + mkSchema d i o + where + d = + swaggerDoc @Text + & (S.schema . S.type_ ?~ S.OpenApiString) + & (S.schema . S.description ?~ "ApsLocKey") + + i = A.withText "ApsLocKey" (pure . ApsLocKey) + o = pure . A.String . fromLocKey + data ApsData = ApsData { _apsLocKey :: !ApsLocKey, _apsLocArgs :: [Text], @@ -185,36 +195,29 @@ data ApsData = ApsData } deriving (Eq, Show, Generic) deriving (Arbitrary) via GenericUniform ApsData - -makeLenses ''ApsData + deriving (FromJSON, ToJSON, S.ToSchema) via (Schema ApsData) apsData :: ApsLocKey -> [Text] -> ApsData apsData lk la = ApsData lk la Nothing True -instance ToJSON ApsData where - toJSON (ApsData k a s b) = - object $ - "loc_key" - .= k - # "loc_args" - .= a - # "sound" - .= s - # "badge" - .= b - # [] - -instance FromJSON ApsData where - parseJSON = withObject "ApsData" $ \o -> - ApsData - <$> o .: "loc_key" - <*> o .:? "loc_args" .!= [] - <*> o .:? "sound" - <*> o .:? "badge" .!= True +instance ToSchema ApsData where + schema = + object "ApsData" $ + ApsData + <$> _apsLocKey .= field "loc_key" schema + <*> withDefault "loc_args" _apsLocArgs (array schema) [] + <*> _apsSound .= optField "sound" (maybeWithDefault A.Null schema) -- keep null for backwards compat + <*> withDefault "badge" _apsBadge schema True + where + withDefault fn f s def = ((Just . f) .= maybe_ (optField fn s)) <&> fromMaybe def + +makeLenses ''ApsData ----------------------------------------------------------------------------- -- Push +-- FUTUREWORK: this is a duplicate of the type in "Wire.NotificationSubsystem" (even though +-- the latter lacks a few possibly deprecated fields). consolidate! data Push = Push { -- | Recipients -- @@ -255,8 +258,7 @@ data Push = Push _pushPayload :: !(List1 Object) } deriving (Eq, Show) - -makeLenses ''Push + deriving (FromJSON, ToJSON, S.ToSchema) via (Schema Push) newPush :: Maybe UserId -> Range 1 1024 (Set Recipient) -> List1 Object -> Push newPush from to pload = @@ -273,49 +275,30 @@ newPush from to pload = _pushPayload = pload } -singletonRecipient :: Recipient -> Range 1 1024 (Set Recipient) -singletonRecipient = Range.unsafeRange . Set.singleton - singletonPayload :: (ToJSONObject a) => a -> List1 Object singletonPayload = List1.singleton . toJSONObject -instance FromJSON Push where - parseJSON = withObject "Push" $ \p -> - Push - <$> p .: "recipients" - <*> p .:? "origin" - <*> p .:? "connections" .!= Set.empty - <*> p .:? "origin_connection" - <*> p .:? "transient" .!= False - <*> p .:? "native_include_origin" .!= True - <*> p .:? "native_encrypt" .!= True - <*> p .:? "native_aps" - <*> p .:? "native_priority" .!= HighPriority - <*> p .: "payload" - -instance ToJSON Push where - toJSON p = - object $ - "recipients" - .= _pushRecipients p - # "origin" - .= _pushOrigin p - # "connections" - .= ifNot Set.null (_pushConnections p) - # "origin_connection" - .= _pushOriginConnection p - # "transient" - .= ifNot not (_pushTransient p) - # "native_include_origin" - .= ifNot id (_pushNativeIncludeOrigin p) - # "native_encrypt" - .= ifNot id (_pushNativeEncrypt p) - # "native_aps" - .= _pushNativeAps p - # "native_priority" - .= ifNot (== HighPriority) (_pushNativePriority p) - # "payload" - .= _pushPayload p - # [] +instance ToSchema Push where + schema = + object "Push" $ + Push + <$> (fromRange . _pushRecipients) .= field "recipients" (rangedSchema (set schema)) + <*> _pushOrigin .= maybe_ (optField "origin" schema) + <*> (ifNot Set.null . _pushConnections) + .= maybe_ (fmap (fromMaybe mempty) (optField "connections" (set schema))) + <*> _pushOriginConnection .= maybe_ (optField "origin_connection" schema) + <*> (ifNot not . _pushTransient) + .= maybe_ + (fmap (fromMaybe False) (optField "transient" schema)) + <*> (ifNot id . _pushNativeIncludeOrigin) + .= maybe_ (fmap (fromMaybe True) (optField "native_include_origin" schema)) + <*> (ifNot id . _pushNativeEncrypt) + .= maybe_ (fmap (fromMaybe True) (optField "native_encrypt" schema)) + <*> _pushNativeAps .= maybe_ (optField "native_aps" schema) + <*> (ifNot (== HighPriority) . _pushNativePriority) + .= maybe_ (fromMaybe HighPriority <$> optField "native_priority" schema) + <*> _pushPayload .= field "payload" schema where ifNot f a = if f a then Nothing else Just a + +makeLenses ''Push diff --git a/libs/wire-api/src/Wire/API/Routes/Features.hs b/libs/wire-api/src/Wire/API/Routes/Features.hs new file mode 100644 index 00000000000..d61ab5546aa --- /dev/null +++ b/libs/wire-api/src/Wire/API/Routes/Features.hs @@ -0,0 +1,22 @@ +module Wire.API.Routes.Features where + +import Wire.API.Conversation.Role +import Wire.API.Error.Galley +import Wire.API.Team.Feature + +type family FeatureErrors cfg where + FeatureErrors LegalholdConfig = + '[ 'ActionDenied 'RemoveConversationMember, + 'CannotEnableLegalHoldServiceLargeTeam, + 'LegalHoldNotEnabled, + 'LegalHoldDisableUnimplemented, + 'LegalHoldServiceNotRegistered, + 'UserLegalHoldIllegalOperation, + 'LegalHoldCouldNotBlockConnections + ] + FeatureErrors _ = '[] + +type family FeatureAPIDesc cfg where + FeatureAPIDesc EnforceFileDownloadLocationConfig = + "

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

" + FeatureAPIDesc _ = "" diff --git a/libs/wire-api/src/Wire/API/Routes/FederationDomainConfig.hs b/libs/wire-api/src/Wire/API/Routes/FederationDomainConfig.hs index 74257a99e66..6b5463bfdb9 100644 --- a/libs/wire-api/src/Wire/API/Routes/FederationDomainConfig.hs +++ b/libs/wire-api/src/Wire/API/Routes/FederationDomainConfig.hs @@ -48,8 +48,6 @@ makePrisms ''FederationRestriction data FederationRestrictionTag = FederationRestrictionAllowAllTag | FederationRestrictionByTeamTag deriving (Eq, Enum, Bounded) -makePrisms ''FederationRestrictionTag - deriving via Schema FederationRestriction instance (S.ToSchema FederationRestriction) deriving via Schema FederationRestriction instance (FromJSON FederationRestriction) diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs index 7d31bedba95..0c294f7a00d 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -34,13 +34,14 @@ module Wire.API.Routes.Internal.Brig GetAccountConferenceCallingConfig, PutAccountConferenceCallingConfig, DeleteAccountConferenceCallingConfig, + GetRichInfoMultiResponse (..), swaggerDoc, module Wire.API.Routes.Internal.Brig.EJPD, FoundInvitationCode (..), ) where -import Control.Lens ((.~)) +import Control.Lens ((.~), (?~)) import Data.Aeson (FromJSON, ToJSON) import Data.Code qualified as Code import Data.CommaSeparatedList @@ -55,7 +56,7 @@ import Data.Text qualified as Text import GHC.TypeLits import Imports hiding (head) import Network.HTTP.Client qualified as HTTP -import Servant hiding (Handler, WithStatus, addHeader, respond) +import Servant hiding (Handler, addHeader, respond) import Servant.Client qualified as Servant import Servant.Client.Core qualified as Servant import Servant.OpenApi (HasOpenApi (toOpenApi)) @@ -65,7 +66,6 @@ import Wire.API.Connection import Wire.API.Error import Wire.API.Error.Brig import Wire.API.MLS.CipherSuite -import Wire.API.MakesFederatedCall import Wire.API.Routes.FederationDomainConfig import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Internal.Brig.EJPD @@ -75,6 +75,7 @@ import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Mul import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public (ZUser) +import Wire.API.Team.Export (TeamExportUser) import Wire.API.Team.Feature import Wire.API.Team.Invitation (Invitation) import Wire.API.Team.LegalHold.Internal @@ -89,22 +90,25 @@ import Wire.API.User.Client import Wire.API.User.RichInfo type EJPDRequest = - Summary - "Identify users for law enforcement. Wire has legal requirements to cooperate \ - \with the authorities. The wire backend operations team uses this to answer \ - \identification requests manually. It is our best-effort representation of the \ - \minimum required information we need to hand over about targets and (in some \ - \cases) their communication peers. For more information, consult ejpd.admin.ch." - :> "ejpd-request" - :> QueryParam' - [ Optional, - Strict, - Description "Also provide information about all contacts of the identified users" - ] - "include_contacts" - Bool - :> Servant.ReqBody '[Servant.JSON] EJPDRequestBody - :> Post '[Servant.JSON] EJPDResponseBody + Named + "ejpd-request" + ( Summary + "Identify users for law enforcement. Wire has legal requirements to cooperate \ + \with the authorities. The wire backend operations team uses this to answer \ + \identification requests manually. It is our best-effort representation of the \ + \minimum required information we need to hand over about targets and (in some \ + \cases) their communication peers. For more information, consult ejpd.admin.ch." + :> "ejpd-request" + :> QueryParam' + [ Optional, + Strict, + Description "Also provide information about all contacts of the identified users" + ] + "include_contacts" + Bool + :> Servant.ReqBody '[Servant.JSON] EJPDRequestBody + :> Post '[Servant.JSON] EJPDResponseBody + ) type GetAccountConferenceCallingConfig = Summary @@ -113,7 +117,7 @@ type GetAccountConferenceCallingConfig = :> Capture "uid" UserId :> "features" :> "conferenceCalling" - :> Get '[Servant.JSON] (WithStatusNoLock ConferenceCallingConfig) + :> Get '[Servant.JSON] (Feature ConferenceCallingConfig) type PutAccountConferenceCallingConfig = Summary @@ -122,7 +126,7 @@ type PutAccountConferenceCallingConfig = :> Capture "uid" UserId :> "features" :> "conferenceCalling" - :> Servant.ReqBody '[Servant.JSON] (WithStatusNoLock ConferenceCallingConfig) + :> Servant.ReqBody '[Servant.JSON] (Feature ConferenceCallingConfig) :> Put '[Servant.JSON] NoContent type DeleteAccountConferenceCallingConfig = @@ -158,18 +162,16 @@ type GetAllConnections = type AccountAPI = Named "get-account-conference-calling-config" GetAccountConferenceCallingConfig - :<|> PutAccountConferenceCallingConfig - :<|> DeleteAccountConferenceCallingConfig - :<|> GetAllConnectionsUnqualified - :<|> GetAllConnections + :<|> Named "i-put-account-conference-calling-config" PutAccountConferenceCallingConfig + :<|> Named "i-delete-account-conference-calling-config" DeleteAccountConferenceCallingConfig + :<|> Named "i-get-all-connections-unqualified" GetAllConnectionsUnqualified + :<|> Named "i-get-all-connections" GetAllConnections :<|> Named "createUserNoVerify" -- This endpoint can lead to the following events being sent: -- - UserActivated event to created user, if it is a team invitation or user has an SSO ID -- - UserIdentityUpdated event to created user, if email or phone get activated ( "users" - :> MakesFederatedCall 'Brig "on-user-deleted-connections" - :> MakesFederatedCall 'Brig "send-connection-action" :> ReqBody '[Servant.JSON] NewUser :> MultiVerb 'POST '[Servant.JSON] RegisterInternalResponses (Either RegisterError SelfProfile) ) @@ -177,18 +179,15 @@ type AccountAPI = "createUserNoVerifySpar" ( "users" :> "spar" - :> MakesFederatedCall 'Brig "on-user-deleted-connections" - :> MakesFederatedCall 'Brig "send-connection-action" :> ReqBody '[Servant.JSON] NewUserSpar :> MultiVerb 'POST '[Servant.JSON] CreateUserSparInternalResponses (Either CreateUserSparError SelfProfile) ) :<|> Named "putSelfEmail" ( Summary - "internal email activation (used in tests and in spar for validating emails obtained as \ - \SAML user identifiers). if the validate query parameter is false or missing, only set \ - \the activation timeout, but do not send an email, and do not do anything about \ - \activating the email." + "Internal email update and activation. Used in tests and in spar for validating emails \ + \obtained via scim or saml implicit user creation. If the `validate` query parameter is \ + \false or missing, only update the email and do not activate." :> ZUser :> "self" :> "email" @@ -240,7 +239,7 @@ type AccountAPI = ( "users" :> QueryParam' [Optional, Strict] "ids" (CommaSeparatedList UserId) :> QueryParam' [Optional, Strict] "handles" (CommaSeparatedList Handle) - :> QueryParam' [Optional, Strict] "email" (CommaSeparatedList Email) -- don't rename to `emails`, for backwards compat! + :> QueryParam' [Optional, Strict] "email" (CommaSeparatedList EmailAddress) -- don't rename to `emails`, for backwards compat! :> QueryParam' [ Optional, Strict, @@ -248,7 +247,7 @@ type AccountAPI = ] "includePendingInvitations" Bool - :> Get '[Servant.JSON] [UserAccount] + :> Get '[Servant.JSON] [User] ) :<|> Named "iGetUserContacts" @@ -261,14 +260,14 @@ type AccountAPI = "iGetUserActivationCode" ( "users" :> "activation-code" - :> QueryParam' [Required, Strict] "email" Email + :> QueryParam' [Required, Strict] "email" EmailAddress :> Get '[Servant.JSON] GetActivationCodeResp ) :<|> Named "iGetUserPasswordResetCode" ( "users" :> "password-reset-code" - :> QueryParam' [Required, Strict] "email" Email + :> QueryParam' [Required, Strict] "email" EmailAddress :> Get '[Servant.JSON] GetPasswordResetCodeResp ) :<|> Named @@ -276,14 +275,14 @@ type AccountAPI = ( Summary "This endpoint can lead to the following events being sent: UserIdentityRemoved event to target user" :> "users" :> "revoke-identity" - :> QueryParam' [Required, Strict] "email" Email + :> QueryParam' [Required, Strict] "email" EmailAddress :> Post '[Servant.JSON] NoContent ) :<|> Named "iHeadBlacklist" ( "users" :> "blacklist" - :> QueryParam' [Required, Strict] "email" Email + :> QueryParam' [Required, Strict] "email" EmailAddress :> MultiVerb 'GET '[Servant.JSON] @@ -296,14 +295,14 @@ type AccountAPI = "iDeleteBlacklist" ( "users" :> "blacklist" - :> QueryParam' [Required, Strict] "email" Email + :> QueryParam' [Required, Strict] "email" EmailAddress :> Delete '[Servant.JSON] NoContent ) :<|> Named "iPostBlacklist" ( "users" :> "blacklist" - :> QueryParam' [Required, Strict] "email" Email + :> QueryParam' [Required, Strict] "email" EmailAddress :> Post '[Servant.JSON] NoContent ) :<|> Named @@ -377,12 +376,11 @@ type AccountAPI = ( "users" :> "rich-info" :> QueryParam' '[Optional, Strict] "ids" (CommaSeparatedList UserId) - :> Get '[Servant.JSON] [(UserId, RichInfo)] + :> Get '[Servant.JSON] GetRichInfoMultiResponse ) :<|> Named "iHeadHandle" ( CanThrow 'InvalidHandle - :> "users" :> "handles" :> Capture "handle" Handle :> MultiVerb @@ -470,23 +468,29 @@ instance ToSchema NewKeyPackageRef where type MLSAPI = "mls" :> GetMLSClients type GetMLSClients = - Summary "Return all clients and all MLS-capable clients of a user" - :> "clients" - :> CanThrow 'UserNotFound - :> Capture "user" UserId - :> QueryParam' '[Required, Strict] "ciphersuite" CipherSuite - :> MultiVerb1 - 'GET - '[Servant.JSON] - (Respond 200 "MLS clients" (Set ClientInfo)) + Named + "get-mls-clients" + ( Summary "Return all clients and all MLS-capable clients of a user" + :> "clients" + :> CanThrow 'UserNotFound + :> Capture "user" UserId + :> QueryParam' '[Required, Strict] "ciphersuite" CipherSuite + :> MultiVerb1 + 'GET + '[Servant.JSON] + (Respond 200 "MLS clients" (Set ClientInfo)) + ) type GetVerificationCode = - Summary "Get verification code for a given email and action" - :> "users" - :> Capture "uid" UserId - :> "verification-code" - :> Capture "action" VerificationAction - :> Get '[Servant.JSON] (Maybe Code.Value) + Named + "get-verification-code" + ( Summary "Get verification code for a given email and action" + :> "users" + :> Capture "uid" UserId + :> "verification-code" + :> Capture "action" VerificationAction + :> Get '[Servant.JSON] (Maybe Code.Value) + ) type API = "i" @@ -533,7 +537,7 @@ type InvitationByEmail = ( "teams" :> "invitations" :> "by-email" - :> QueryParam' [Required, Strict] "email" Email + :> QueryParam' [Required, Strict] "email" EmailAddress :> Get '[Servant.JSON] Invitation ) @@ -594,13 +598,21 @@ type TeamInvitations = :> Capture "tid" TeamId :> "invitations" :> Servant.ReqBody '[JSON] NewUserScimInvitation - :> Post '[JSON] UserAccount + :> Post '[JSON] User ) type UserAPI = - UpdateUserLocale - :<|> DeleteUserLocale - :<|> GetDefaultLocale + Named "i-update-user-locale" UpdateUserLocale + :<|> Named "i-delete-user-locale" DeleteUserLocale + :<|> Named "i-get-default-locale" GetDefaultLocale + :<|> Named + "get-user-export-data" + ( Summary "Get user export data" + :> "users" + :> Capture "uid" UserId + :> "export-data" + :> MultiVerb1 'GET '[JSON] (Respond 200 "User export data" (Maybe TeamExportUser)) + ) type UpdateUserLocale = Summary @@ -640,16 +652,12 @@ type AuthAPI = Named "legalhold-login" ( "legalhold-login" - :> MakesFederatedCall 'Brig "on-user-deleted-connections" - :> MakesFederatedCall 'Brig "send-connection-action" :> ReqBody '[JSON] LegalHoldLogin :> MultiVerb1 'POST '[JSON] TokenResponse ) :<|> Named "sso-login" ( "sso-login" - :> MakesFederatedCall 'Brig "on-user-deleted-connections" - :> MakesFederatedCall 'Brig "send-connection-action" :> ReqBody '[JSON] SsoLogin :> QueryParam' [Optional, Strict] "persist" Bool :> MultiVerb1 'POST '[JSON] TokenResponse @@ -737,7 +745,7 @@ type ProviderAPI = ( Summary "Retrieve activation code via api instead of email (for testing only)" :> "provider" :> "activation-code" - :> QueryParam' '[Required, Strict] "email" Email + :> QueryParam' '[Required, Strict] "email" EmailAddress :> MultiVerb1 'GET '[JSON] (Respond 200 "" Code.KeyValuePair) ) ) @@ -745,10 +753,25 @@ type ProviderAPI = type FederationRemotesAPIDescription = "See https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections for background. " +newtype GetRichInfoMultiResponse + = GetRichInfoMultiResponse + [(UserId, RichInfo)] + deriving newtype (FromJSON, ToJSON) + +instance S.ToSchema GetRichInfoMultiResponse where + declareNamedSchema _ = + pure $ + S.NamedSchema (Just $ "GetRichInfoMultiResponse") $ + mempty & S.description ?~ "List of pairs of UserId and RichInfo" + swaggerDoc :: OpenApi -swaggerDoc = - toOpenApi (Proxy @API) - & info . title .~ "Wire-Server internal brig API" +swaggerDoc = brigSwaggerDoc + +brigSwaggerDoc :: OpenApi +brigSwaggerDoc = + ( toOpenApi (Proxy @API) + & info . title .~ "Wire-Server internal brig API" + ) newtype BrigInternalClient a = BrigInternalClient (Servant.ClientM a) deriving newtype (Functor, Applicative, Monad, Servant.RunClient) diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/EJPD.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/EJPD.hs index 38bb517cb58..fca44780100 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/EJPD.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/EJPD.hs @@ -43,7 +43,7 @@ import Imports hiding (head) import Test.QuickCheck (Arbitrary) import Wire.API.Connection (Relation) import Wire.API.Team.Member (NewListType) -import Wire.API.User.Identity (Email, Phone) +import Wire.API.User.Identity (EmailAddress, Phone) import Wire.API.User.Profile (Name) import Wire.Arbitrary (GenericUniform (..)) @@ -62,7 +62,7 @@ data EJPDResponseItemRoot = EJPDResponseItemRoot ejpdResponseRootTeamId :: Maybe TeamId, ejpdResponseRootName :: Name, ejpdResponseRootHandle :: Maybe Handle, - ejpdResponseRootEmail :: Maybe Email, + ejpdResponseRootEmail :: Maybe EmailAddress, ejpdResponseRootPhone :: Maybe Phone, ejpdResponseRootPushTokens :: Set Text, -- 'Wire.API.Push.V2.Token.Token', but that would produce an orphan instance. ejpdResponseRootContacts :: Maybe (Set EJPDContact), @@ -78,7 +78,7 @@ data EJPDResponseItemLeaf = EJPDResponseItemLeaf ejpdResponseLeafTeamId :: Maybe TeamId, ejpdResponseLeafName :: Name, ejpdResponseLeafHandle :: Maybe Handle, - ejpdResponseLeafEmail :: Maybe Email, + ejpdResponseLeafEmail :: Maybe EmailAddress, ejpdResponseLeafPhone :: Maybe Phone, ejpdResponseLeafPushTokens :: Set Text, -- 'Wire.API.Push.V2.Token.Token', but that would produce an orphan instance. ejpdResponseLeafConversations :: Maybe (Set EJPDConvInfo), diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs index 70d478643a0..78d4ddcbf0a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs @@ -39,7 +39,7 @@ type OAuthAPI = :> Post '[JSON] OAuthClientCredentials ) :<|> Named - "get-oauth-client" + "i-get-oauth-client" ( Summary "Get OAuth client by id" :> CanThrow 'OAuthFeatureDisabled :> CanThrow 'OAuthClientNotFound diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/SearchIndex.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/SearchIndex.hs index 0b90fd43524..9e0fdabd7dc 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/SearchIndex.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/SearchIndex.hs @@ -30,21 +30,3 @@ type ISearchIndexAPI = :> "refresh" :> Post '[JSON] NoContent ) - :<|> Named - "indexReindex" - ( Summary - "reindex from Cassandra (NB: e.g. integration testing prefer the `brig-index` \ - \executable for actual operations!)" - :> "index" - :> "reindex" - :> Post '[JSON] NoContent - ) - :<|> Named - "indexReindexIfSameOrNewer" - ( Summary - "forcefully reindex from Cassandra, even if nothing has changed (NB: e.g. \ - \integration testing prefer the `brig-index` executable for actual operations!)" - :> "index" - :> "reindex-if-same-or-newer" - :> Post '[JSON] NoContent - ) diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs index 592e72dc61a..75cc7caa714 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs @@ -28,8 +28,8 @@ import Wire.API.Routes.Named type InternalAPI = "i" - :> ( "status" :> MultiVerb 'GET '() '[RespondEmpty 200 "OK"] () - :<|> Named "iGetAsset" ("assets" :> Capture "key" AssetKey :> Get '[Servant.JSON] Text) + :> ( Named "i_status" ("status" :> MultiVerb 'GET '() '[RespondEmpty 200 "OK"] ()) + :<|> Named "i_get_asset" ("assets" :> Capture "key" AssetKey :> Get '[Servant.JSON] Text) ) swaggerDoc :: OpenApi diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs index 8e7a8991d31..bdae90491ca 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -24,9 +24,8 @@ import Data.OpenApi (OpenApi, info, title) import Data.Range import GHC.TypeLits (AppendSymbol) import Imports hiding (head) -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi -import Wire.API.ApplyMods import Wire.API.Bot import Wire.API.Bot.Service import Wire.API.Conversation @@ -36,8 +35,8 @@ import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Event.Conversation import Wire.API.FederationStatus -import Wire.API.MakesFederatedCall import Wire.API.Provider.Service (ServiceRef) +import Wire.API.Routes.Features import Wire.API.Routes.Internal.Brig.EJPD import Wire.API.Routes.Internal.Galley.ConversationsIntra import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti @@ -56,127 +55,35 @@ import Wire.API.Team.Member import Wire.API.Team.SearchVisibility import Wire.API.User.Client -type LegalHoldFeatureStatusChangeErrors = - '( 'ActionDenied 'RemoveConversationMember, - '( AuthenticationError, - '( 'CannotEnableLegalHoldServiceLargeTeam, - '( 'LegalHoldNotEnabled, - '( 'LegalHoldDisableUnimplemented, - '( 'LegalHoldServiceNotRegistered, - '( 'UserLegalHoldIllegalOperation, - '( 'LegalHoldCouldNotBlockConnections, '()) - ) - ) - ) - ) - ) - ) - ) +type family IFeatureAPI1 cfg where + -- special case for classified domains, since it cannot be set + IFeatureAPI1 ClassifiedDomainsConfig = IFeatureStatusGet ClassifiedDomainsConfig + IFeatureAPI1 cfg = IFeatureAPI1Full cfg + +type IFeatureAPI1Full cfg = + IFeatureStatusGet cfg + :<|> IFeatureStatusPut cfg + :<|> IFeatureStatusPatch cfg -type LegalHoldFeaturesStatusChangeFederatedCalls = - '[ MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-mls-message-sent" - ] +type family IAllFeaturesAPI cfgs where + IAllFeaturesAPI '[cfg] = IFeatureAPI1 cfg + IAllFeaturesAPI (cfg : cfgs) = IFeatureAPI1 cfg :<|> IAllFeaturesAPI cfgs type IFeatureAPI = - -- SSOConfig - IFeatureStatusGet SSOConfig - :<|> IFeatureStatusPut '[] '() SSOConfig - :<|> IFeatureStatusPatch '[] '() SSOConfig - -- LegalholdConfig - :<|> IFeatureStatusGet LegalholdConfig - :<|> IFeatureStatusPut - LegalHoldFeaturesStatusChangeFederatedCalls - LegalHoldFeatureStatusChangeErrors - LegalholdConfig - :<|> IFeatureStatusPatch - LegalHoldFeaturesStatusChangeFederatedCalls - LegalHoldFeatureStatusChangeErrors - LegalholdConfig - -- SearchVisibilityAvailableConfig - :<|> IFeatureStatusGet SearchVisibilityAvailableConfig - :<|> IFeatureStatusPut '[] '() SearchVisibilityAvailableConfig - :<|> IFeatureStatusPatch '[] '() SearchVisibilityAvailableConfig - -- ValidateSAMLEmailsConfig - :<|> IFeatureStatusGet ValidateSAMLEmailsConfig - :<|> IFeatureStatusPut '[] '() ValidateSAMLEmailsConfig - :<|> IFeatureStatusPatch '[] '() ValidateSAMLEmailsConfig - -- DigitalSignaturesConfig - :<|> IFeatureStatusGet DigitalSignaturesConfig - :<|> IFeatureStatusPut '[] '() DigitalSignaturesConfig - :<|> IFeatureStatusPatch '[] '() DigitalSignaturesConfig - -- AppLockConfig - :<|> IFeatureStatusGet AppLockConfig - :<|> IFeatureStatusPut '[] '() AppLockConfig - :<|> IFeatureStatusPatch '[] '() AppLockConfig - -- FileSharingConfig - :<|> IFeatureStatusGet FileSharingConfig - :<|> IFeatureStatusPut '[] '() FileSharingConfig + IAllFeaturesAPI Features + -- legacy lock status put endpoints :<|> IFeatureStatusLockStatusPut FileSharingConfig - :<|> IFeatureStatusPatch '[] '() FileSharingConfig - -- ConferenceCallingConfig - :<|> IFeatureStatusGet ConferenceCallingConfig - :<|> IFeatureStatusPut '[] '() ConferenceCallingConfig - :<|> IFeatureStatusPatch '[] '() ConferenceCallingConfig - -- SelfDeletingMessagesConfig - :<|> IFeatureStatusGet SelfDeletingMessagesConfig - :<|> IFeatureStatusPut '[] '() SelfDeletingMessagesConfig + :<|> IFeatureStatusLockStatusPut ConferenceCallingConfig :<|> IFeatureStatusLockStatusPut SelfDeletingMessagesConfig - :<|> IFeatureStatusPatch '[] '() SelfDeletingMessagesConfig - -- GuestLinksConfig - :<|> IFeatureStatusGet GuestLinksConfig - :<|> IFeatureStatusPut '[] '() GuestLinksConfig :<|> IFeatureStatusLockStatusPut GuestLinksConfig - :<|> IFeatureStatusPatch '[] '() GuestLinksConfig - -- SndFactorPasswordChallengeConfig - :<|> IFeatureStatusGet SndFactorPasswordChallengeConfig - :<|> IFeatureStatusPut '[] '() SndFactorPasswordChallengeConfig :<|> IFeatureStatusLockStatusPut SndFactorPasswordChallengeConfig - :<|> IFeatureStatusPatch '[] '() SndFactorPasswordChallengeConfig - -- SearchVisibilityInboundConfig - :<|> IFeatureStatusGet SearchVisibilityInboundConfig - :<|> IFeatureStatusPut '[] '() SearchVisibilityInboundConfig - :<|> IFeatureStatusPatch '[] '() SearchVisibilityInboundConfig - :<|> IFeatureNoConfigMultiGet SearchVisibilityInboundConfig - -- ClassifiedDomainsConfig - :<|> IFeatureStatusGet ClassifiedDomainsConfig - -- MLSConfig - :<|> IFeatureStatusGet MLSConfig - :<|> IFeatureStatusPut '[] '() MLSConfig - :<|> IFeatureStatusPatch '[] '() MLSConfig :<|> IFeatureStatusLockStatusPut MLSConfig - -- ExposeInvitationURLsToTeamAdminConfig - :<|> IFeatureStatusGet ExposeInvitationURLsToTeamAdminConfig - :<|> IFeatureStatusPut '[] '() ExposeInvitationURLsToTeamAdminConfig - :<|> IFeatureStatusPatch '[] '() ExposeInvitationURLsToTeamAdminConfig - -- SearchVisibilityInboundConfig - :<|> IFeatureStatusGet SearchVisibilityInboundConfig - :<|> IFeatureStatusPut '[] '() SearchVisibilityInboundConfig - :<|> IFeatureStatusPatch '[] '() SearchVisibilityInboundConfig - -- OutlookCalIntegrationConfig - :<|> IFeatureStatusGet OutlookCalIntegrationConfig - :<|> IFeatureStatusPut '[] '() OutlookCalIntegrationConfig - :<|> IFeatureStatusPatch '[] '() OutlookCalIntegrationConfig :<|> IFeatureStatusLockStatusPut OutlookCalIntegrationConfig - -- MlsE2EIdConfig - :<|> IFeatureStatusGet MlsE2EIdConfig - :<|> IFeatureStatusPut '[] '() MlsE2EIdConfig - :<|> IFeatureStatusPatch '[] '() MlsE2EIdConfig :<|> IFeatureStatusLockStatusPut MlsE2EIdConfig - -- MlsMigrationConfig - :<|> IFeatureStatusGet MlsMigrationConfig - :<|> IFeatureStatusPut '[] '() MlsMigrationConfig - :<|> IFeatureStatusPatch '[] '() MlsMigrationConfig :<|> IFeatureStatusLockStatusPut MlsMigrationConfig - -- EnforceFileDownloadLocationConfig - :<|> IFeatureStatusGetWithDesc EnforceFileDownloadLocationConfig "

Custom feature: only supported for some decidated on-prem systems.

" - :<|> IFeatureStatusPutWithDesc '[] '() EnforceFileDownloadLocationConfig "

Custom feature: only supported for some decidated on-prem systems.

" - :<|> IFeatureStatusPatchWithDesc '[] '() EnforceFileDownloadLocationConfig "

Custom feature: only supported for some decidated on-prem systems.

" - :<|> IFeatureStatusLockStatusPutWithDesc EnforceFileDownloadLocationConfig "

Custom feature: only supported for some decidated on-prem systems.

" - -- LimitedEventFanoutConfig - :<|> IFeatureStatusGet LimitedEventFanoutConfig - :<|> IFeatureStatusPut '[] '() LimitedEventFanoutConfig - :<|> IFeatureStatusPatch '[] '() LimitedEventFanoutConfig + :<|> IFeatureStatusLockStatusPut EnforceFileDownloadLocationConfig + -- special endpoints + :<|> IFeatureNoConfigMultiGet SearchVisibilityInboundConfig -- all feature configs :<|> Named "feature-configs-internal" @@ -192,7 +99,7 @@ type IFeatureAPI = ] "user_id" UserId - :> Get '[JSON] AllFeatureConfigs + :> Get '[JSON] AllTeamFeatures ) type InternalAPI = "i" :> InternalAPIBase @@ -208,8 +115,6 @@ type InternalAPIBase = "delete-user" ( Summary "Remove a user from their teams and conversations and erase their clients" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" :> ZLocalUser :> ZOptConn :> "user" @@ -221,9 +126,6 @@ type InternalAPIBase = :<|> Named "connect" ( Summary "Create a connect conversation (deprecated)" - :> MakesFederatedCall 'Brig "api-version" - :> MakesFederatedCall 'Galley "on-conversation-created" - :> MakesFederatedCall 'Galley "on-conversation-updated" :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation :> CanThrow 'NotConnected @@ -303,7 +205,7 @@ type ITeamsAPIBase = :<|> Named "create-binding-team" ( ZUser - :> ReqBody '[JSON] BindingNewTeam + :> ReqBody '[JSON] NewTeam :> MultiVerb1 'PUT '[JSON] @@ -392,62 +294,67 @@ type ITeamsAPIBase = ) ) -type IFeatureStatusGet f = IFeatureStatusGetWithDesc f "" - -type IFeatureStatusGetWithDesc f desc = Named '("iget", f) (Description desc :> FeatureStatusBaseGet f) - -type IFeatureStatusPut calls errs f = IFeatureStatusPutWithDesc calls errs f "" - -type IFeatureStatusPutWithDesc calls errs f desc = Named '("iput", f) (ApplyMods calls (Description desc :> FeatureStatusBasePutInternal errs f)) +type IFeatureStatusGet cfg = + Named + '("iget", cfg) + ( Description (FeatureAPIDesc cfg) + :> FeatureStatusBaseGet cfg + ) -type IFeatureStatusPatch calls errs f = IFeatureStatusPatchWithDesc calls errs f "" +type IFeatureStatusPut cfg = + Named + '("iput", cfg) + ( Description (FeatureAPIDesc cfg) + :> FeatureStatusBasePutInternal cfg + ) -type IFeatureStatusPatchWithDesc calls errs f desc = Named '("ipatch", f) (ApplyMods calls (Description desc :> FeatureStatusBasePatchInternal errs f)) +type IFeatureStatusPatch cfg = + Named + '("ipatch", cfg) + ( Description (FeatureAPIDesc cfg) + :> FeatureStatusBasePatchInternal cfg + ) -type FeatureStatusBasePutInternal errs featureConfig = +type FeatureStatusBasePutInternal cfg = FeatureStatusBaseInternal - (AppendSymbol "Put config for " (FeatureSymbol featureConfig)) - errs - featureConfig - ( ReqBody '[JSON] (WithStatusNoLock featureConfig) - :> Put '[JSON] (WithStatus featureConfig) + (AppendSymbol "Put config for " (FeatureSymbol cfg)) + cfg + ( ReqBody '[JSON] (Feature cfg) + :> Put '[JSON] (LockableFeature cfg) ) -type FeatureStatusBasePatchInternal errs featureConfig = +type FeatureStatusBasePatchInternal cfg = FeatureStatusBaseInternal - (AppendSymbol "Patch config for " (FeatureSymbol featureConfig)) - errs - featureConfig - ( ReqBody '[JSON] (WithStatusPatch featureConfig) - :> Patch '[JSON] (WithStatus featureConfig) + (AppendSymbol "Patch config for " (FeatureSymbol cfg)) + cfg + ( ReqBody '[JSON] (LockableFeaturePatch cfg) + :> Patch '[JSON] (LockableFeature cfg) ) -type FeatureStatusBaseInternal desc errs featureConfig a = +type FeatureStatusBaseInternal desc cfg a = Summary desc :> CanThrow OperationDenied :> CanThrow 'NotATeamMember :> CanThrow 'TeamNotFound :> CanThrow TeamFeatureError - :> CanThrowMany errs + :> CanThrowMany (FeatureErrors cfg) :> "teams" :> Capture "tid" TeamId :> "features" - :> FeatureSymbol featureConfig + :> FeatureSymbol cfg :> a -type IFeatureStatusLockStatusPut featureName = IFeatureStatusLockStatusPutWithDesc featureName "" - -type IFeatureStatusLockStatusPutWithDesc featureName desc = +type IFeatureStatusLockStatusPut cfg = Named - '("ilock", featureName) - ( Summary (AppendSymbol "(Un-)lock " (FeatureSymbol featureName)) - :> Description desc + '("ilock", cfg) + ( Summary (AppendSymbol "(Un-)lock " (FeatureSymbol cfg)) + :> Description (FeatureAPIDesc cfg) :> CanThrow 'NotATeamMember :> CanThrow 'TeamNotFound :> "teams" :> Capture "tid" TeamId :> "features" - :> FeatureSymbol featureName + :> FeatureSymbol cfg :> Capture "lockStatus" LockStatus :> Put '[JSON] LockStatusResponse ) @@ -480,7 +387,7 @@ type IConversationAPI = Named "conversation-get-member" ( "conversations" - :> Capture "cnv" ConvId + :> QualifiedCapture "cnv" ConvId :> "members" :> Capture "usr" UserId :> Get '[JSON] (Maybe Member) @@ -501,16 +408,6 @@ type IConversationAPI = :> "v2" :> Put '[JSON] Conversation ) - :<|> Named - "conversation-block-unqualified" - ( CanThrow 'InvalidOperation - :> CanThrow 'ConvNotFound - :> ZUser - :> "conversations" - :> Capture "cnv" ConvId - :> "block" - :> Put '[JSON] () - ) :<|> Named "conversation-block" ( CanThrow 'InvalidOperation @@ -525,21 +422,6 @@ type IConversationAPI = -- - MemberJoin event to you, if the conversation existed and had < 2 members before -- - MemberJoin event to other, if the conversation existed and only the other was member -- before - :<|> Named - "conversation-unblock-unqualified" - ( CanThrow 'InvalidOperation - :> CanThrow 'ConvNotFound - :> ZLocalUser - :> ZOptConn - :> "conversations" - :> Capture "cnv" ConvId - :> "unblock" - :> Put '[JSON] Conversation - ) - -- This endpoint can lead to the following events being sent: - -- - MemberJoin event to you, if the conversation existed and had < 2 members before - -- - MemberJoin event to other, if the conversation existed and only the other was member - -- before :<|> Named "conversation-unblock" ( CanThrow 'InvalidOperation @@ -563,8 +445,7 @@ type IConversationAPI = "conversation-mls-one-to-one" ( CanThrow 'NotConnected :> CanThrow 'MLSNotEnabled - :> "conversations" - :> "mls-one2one" + :> "mls-one2one-conversations" :> ZLocalUser :> QualifiedCapture "user" UserId :> Get '[JSON] Conversation @@ -574,8 +455,7 @@ type IConversationAPI = ( CanThrow 'NotConnected :> CanThrow 'MLSNotEnabled :> ZLocalUser - :> "conversations" - :> "mls-one2one" + :> "mls-one2one-conversations" :> QualifiedCapture "user" UserId :> "established" :> Get '[JSON] Bool @@ -648,7 +528,7 @@ type IMiscAPI = (RespondEmpty 200 "OK") ) :<|> Named - "add-bot" + "i-add-bot" ( -- This endpoint can lead to the following events being sent: -- - MemberJoin event to members CanThrow ('ActionDenied 'AddConversationMember) diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Gundeck.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Gundeck.hs new file mode 100644 index 00000000000..c786d1c3020 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Gundeck.hs @@ -0,0 +1,103 @@ +module Wire.API.Routes.Internal.Gundeck where + +import Control.Lens ((%~), (.~), (?~)) +import Data.Aeson +import Data.CommaSeparatedList +import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap +import Data.Id +import Data.Metrics.Servant +import Data.OpenApi qualified as S hiding (HasServer, Header) +import Data.OpenApi.Declare qualified as S +import Data.Text qualified as Text +import Data.Typeable +import Imports +import Network.Wai +import Servant hiding (URI (..)) +import Servant.API.Description +import Servant.OpenApi +import Servant.OpenApi.Internal +import Servant.Server.Internal.Delayed +import Servant.Server.Internal.DelayedIO +import Servant.Server.Internal.ErrorFormatter +import Wire.API.CannonId +import Wire.API.Presence +import Wire.API.Push.V2 +import Wire.API.Routes.Named +import Wire.API.Routes.Public + +-- | this can be replaced by `ReqBody '[JSON] Presence` once the fix in cannon from +-- https://github.com/wireapp/wire-server/pull/4246 has been deployed everywhere. +-- +-- Background: Cannon.WS.regInfo called gundeck without setting the content-type header here. +-- wai-routes and wai-predicates were able to work with that; servant is less lenient. +data ReqBodyHack + +-- | cloned from instance for ReqBody'. +instance + ( HasServer api context, + HasContextEntry (MkContextWithErrorFormatter context) ErrorFormatters + ) => + HasServer (ReqBodyHack :> api) context + where + type ServerT (ReqBodyHack :> api) m = Presence -> ServerT api m + + hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s + + route Proxy context subserver = + route (Proxy :: Proxy api) context $ + addBodyCheck subserver ctCheck bodyCheck + where + rep = typeRep (Proxy :: Proxy ReqBodyHack) + formatError = bodyParserErrorFormatter $ getContextEntry (mkContextWithErrorFormatter context) + + ctCheck = pure eitherDecode + + -- Body check, we get a body parsing functions as the first argument. + bodyCheck f = withRequest $ \request -> do + mrqbody <- f <$> liftIO (lazyRequestBody request) + case mrqbody of + Left e -> delayedFailFatal $ formatError rep request e + Right v -> pure v + +-- | cloned from instance for ReqBody'. +instance (RoutesToPaths route) => RoutesToPaths (ReqBodyHack :> route) where + getRoutes = getRoutes @route + +-- | cloned from instance for ReqBody'. +instance (HasOpenApi sub) => HasOpenApi (ReqBodyHack :> sub) where + toOpenApi _ = + toOpenApi (Proxy @sub) + & addRequestBody reqBody + & addDefaultResponse400 tname + & S.components . S.schemas %~ (<> defs) + where + tname = "body" + transDesc "" = Nothing + transDesc desc = Just (Text.pack desc) + (defs, ref) = S.runDeclare (S.declareSchemaRef (Proxy @Presence)) mempty + reqBody = + (mempty :: S.RequestBody) + & S.description .~ transDesc (reflectDescription (Proxy :: Proxy '[])) + & S.required ?~ True + & S.content .~ InsOrdHashMap.fromList [(t, mempty & S.schema ?~ ref) | t <- allContentType (Proxy :: Proxy '[JSON])] + +type InternalAPI = + "i" + :> ( Named "i-status" ("status" :> Get '[JSON] NoContent) + :<|> Named "i-push" ("push" :> "v2" :> ReqBody '[JSON] [Push] :> Post '[JSON] NoContent) + :<|> ( "presences" + :> ( Named "i-presences-get-for-users" (QueryParam' [Required, Strict] "ids" (CommaSeparatedList UserId) :> Get '[JSON] [Presence]) + :<|> Named "i-presences-get-for-user" (Capture "uid" UserId :> Get '[JSON] [Presence]) + :<|> Named "i-presences-post" (ReqBodyHack :> Verb 'POST 201 '[JSON] (Headers '[Header "Location" URI] NoContent)) + :<|> Named "i-presences-delete" (Capture "uid" UserId :> "devices" :> Capture "did" ConnId :> "cannons" :> Capture "cannon" CannonId :> Delete '[JSON] NoContent) + ) + ) + :<|> Named "i-clients-delete" (ZUser :> "clients" :> Capture "cid" ClientId :> Delete '[JSON] NoContent) + :<|> Named "i-user-delete" (ZUser :> "user" :> Delete '[JSON] NoContent) + :<|> Named "i-push-tokens-get" ("push-tokens" :> Capture "uid" UserId :> Get '[JSON] PushTokenList) + ) + +swaggerDoc :: S.OpenApi +swaggerDoc = + toOpenApi (Proxy @InternalAPI) + & S.info . S.title .~ "Wire-Server internal gundeck API" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs b/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs deleted file mode 100644 index ffde2e561c3..00000000000 --- a/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs +++ /dev/null @@ -1,45 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Wire.API.Routes.Internal.LegalHold where - -import Control.Lens -import Data.Id -import Data.OpenApi (OpenApi) -import Data.OpenApi.Lens -import Data.Proxy -import Imports -import Servant.API hiding (Header, WithStatus) -import Servant.OpenApi -import Wire.API.Team.Feature - -type InternalLegalHoldAPI = - "i" - :> "teams" - :> ( Capture "tid" TeamId - :> "legalhold" - :> Get '[JSON] (WithStatus LegalholdConfig) - :<|> Capture "tid" TeamId - :> "legalhold" - :> ReqBody '[JSON] (WithStatusNoLock LegalholdConfig) - :> Put '[] NoContent - ) - -swaggerDoc :: OpenApi -swaggerDoc = - toOpenApi (Proxy @InternalLegalHoldAPI) - & info . title .~ "Wire-Server internal legalhold API" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs index 8cc2207031c..0cd59f6d22b 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs @@ -23,15 +23,16 @@ import Data.OpenApi import Imports import Servant import Servant.OpenApi +import Wire.API.Routes.Named import Wire.API.User import Wire.API.User.Saml type InternalAPI = "i" - :> ( "status" :> Get '[JSON] NoContent - :<|> "teams" :> Capture "team" TeamId :> DeleteNoContent - :<|> "sso" :> "settings" :> ReqBody '[JSON] SsoSettings :> Put '[JSON] NoContent - :<|> "scim" :> "userinfos" :> ReqBody '[JSON] UserSet :> Post '[JSON] ScimUserInfos + :> ( Named "i_status" ("status" :> Get '[JSON] NoContent) + :<|> Named "i_delete_team" ("teams" :> Capture "team" TeamId :> DeleteNoContent) + :<|> Named "i_put_sso_settings" ("sso" :> "settings" :> ReqBody '[JSON] SsoSettings :> Put '[JSON] NoContent) + :<|> Named "i_post_scim_user_info" ("scim" :> "userinfo" :> Capture "user" UserId :> Post '[JSON] ScimUserInfo) ) swaggerDoc :: OpenApi diff --git a/libs/wire-api/src/Wire/API/Routes/LowLevelStream.hs b/libs/wire-api/src/Wire/API/Routes/LowLevelStream.hs index f39080b54f7..0313c04ced8 100644 --- a/libs/wire-api/src/Wire/API/Routes/LowLevelStream.hs +++ b/libs/wire-api/src/Wire/API/Routes/LowLevelStream.hs @@ -18,6 +18,8 @@ module Wire.API.Routes.LowLevelStream where import Control.Lens (at, (.~), (?~), _Just) +import Control.Monad.Codensity +import Control.Monad.Trans.Resource import Data.ByteString.Char8 as B8 import Data.CaseInsensitive qualified as CI import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap @@ -39,6 +41,10 @@ import Servant.Server hiding (respond) import Servant.Server.Internal import Wire.API.Routes.Version +-- | Used as the return type of a streaming handler. The 'Codensity' wrapper +-- makes it possible to add finalisation logic to the streaming action. +type LowLevelStreamingBody = Codensity IO StreamingBody + -- FUTUREWORK: make it possible to generate headers at runtime data LowLevelStream method status (headers :: [(Symbol, Symbol)]) desc ctype @@ -63,7 +69,9 @@ instance (ReflectMethod method, KnownNat status, RenderHeaders headers, Accept ctype) => HasServer (LowLevelStream method status headers desc ctype) context where - type ServerT (LowLevelStream method status headers desc ctype) m = m StreamingBody + type + ServerT (LowLevelStream method status headers desc ctype) m = + m LowLevelStreamingBody hoistServerWithContext _ _ nt s = nt s route Proxy _ action = leafRouter $ \env request respond -> @@ -71,15 +79,25 @@ instance cmediatype = HTTP.matchAccept [contentType (Proxy @ctype)] accH accCheck = when (isNothing cmediatype) $ delayedFail err406 contentHeader = (hContentType, HTTP.renderHeader . maybeToList $ cmediatype) - in runAction - ( action - `addMethodCheck` methodCheck method request - `addAcceptCheck` accCheck - ) - env - request - respond - $ Route . responseStream status (contentHeader : extraHeaders) + in runResourceT $ do + r <- + runDelayed + ( action + `addMethodCheck` methodCheck method request + `addAcceptCheck` accCheck + ) + env + request + liftIO $ case r of + Route h -> + runHandler h >>= \case + Left e -> respond $ FailFatal e + Right getStreamingBody -> lowerCodensity $ do + body <- getStreamingBody + let resp = responseStream status (contentHeader : extraHeaders) body + lift $ respond $ Route resp + Fail e -> respond $ Fail e + FailFatal e -> respond $ FailFatal e where method = reflectMethod (Proxy :: Proxy method) status = statusFromNat (Proxy :: Proxy status) diff --git a/libs/wire-api/src/Wire/API/Routes/MultiTablePaging/State.hs b/libs/wire-api/src/Wire/API/Routes/MultiTablePaging/State.hs index e14c63c1332..8188aa20a1f 100644 --- a/libs/wire-api/src/Wire/API/Routes/MultiTablePaging/State.hs +++ b/libs/wire-api/src/Wire/API/Routes/MultiTablePaging/State.hs @@ -25,7 +25,6 @@ import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Attoparsec.ByteString qualified as AB import Data.ByteString qualified as BS import Data.ByteString.Base64.URL qualified as Base64Url -import Data.Either.Combinators (mapLeft) import Data.OpenApi qualified as S import Data.Proxy import Data.Schema diff --git a/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs b/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs index 0ee626b9d98..ebda942c2cd 100644 --- a/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs +++ b/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs @@ -55,7 +55,6 @@ import Control.Lens hiding (Context, (<|)) import Data.ByteString.Builder import Data.ByteString.Lazy qualified as LBS import Data.CaseInsensitive qualified as CI -import Data.Either.Combinators (leftToMaybe) import Data.HashMap.Strict.InsOrd (InsOrdHashMap, unionWith) import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap import Data.Kind @@ -111,9 +110,9 @@ type RespondEmpty s desc = RespondAs '() s desc () -- | A type to describe a streaming 'MultiVerb' response. -- --- Includes status code, description, framing strategy and content type. Note --- that the handler return type is hardcoded to be 'SourceIO ByteString'. -data RespondStreaming (s :: Nat) (desc :: Symbol) (framing :: Type) (ct :: Type) +-- Includes status code, description and content type. Note that the handler +-- return type is hardcoded to be 'SourceIO ByteString'. +data RespondStreaming (s :: Nat) (desc :: Symbol) (ct :: Type) -- | The result of parsing a response as a union alternative of type 'a'. -- @@ -269,14 +268,14 @@ instance mempty & S.description .~ Text.pack (symbolVal (Proxy @desc)) -type instance ResponseType (RespondStreaming s desc framing ct) = SourceIO ByteString +type instance ResponseType (RespondStreaming s desc ct) = SourceIO ByteString instance (Accept ct, KnownStatus s) => - IsResponse cs (RespondStreaming s desc framing ct) + IsResponse cs (RespondStreaming s desc ct) where - type ResponseStatus (RespondStreaming s desc framing ct) = s - type ResponseBody (RespondStreaming s desc framing ct) = SourceIO ByteString + type ResponseStatus (RespondStreaming s desc ct) = s + type ResponseBody (RespondStreaming s desc ct) = SourceIO ByteString responseRender _ x = pure . addContentType @ct $ Response @@ -290,7 +289,7 @@ instance guard (responseStatusCode resp == statusVal (Proxy @s)) pure $ responseBody resp -instance (KnownSymbol desc) => IsSwaggerResponse (RespondStreaming s desc framing ct) where +instance (KnownSymbol desc) => IsSwaggerResponse (RespondStreaming s desc ct) where responseSwagger = pure $ mempty @@ -544,9 +543,6 @@ instance (ResponseType r ~ a) => AsUnion '[r] a where toUnion = Z . I fromUnion = unI . unZ -_foo :: Union '[Int] -_foo = toUnion @'[Respond 200 "test" Int] @Int 3 - class InjectAfter as bs where injectAfter :: Union bs -> Union (as .++ bs) diff --git a/libs/wire-api/src/Wire/API/Routes/Named.hs b/libs/wire-api/src/Wire/API/Routes/Named.hs index d7aad87521a..5978774da2a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Named.hs +++ b/libs/wire-api/src/Wire/API/Routes/Named.hs @@ -19,13 +19,14 @@ module Wire.API.Routes.Named where -import Control.Lens ((%~)) +import Control.Lens ((%~), (?~)) import Data.Kind import Data.Metrics.Servant import Data.OpenApi.Lens hiding (HasServer) import Data.OpenApi.Operation import Data.Proxy import Data.Text qualified as T +import GHC.Generics import GHC.TypeLits import Imports import Servant @@ -41,17 +42,34 @@ newtype Named name x = Named {unnamed :: x} -- types other than string literals in some places. class RenderableSymbol a where renderSymbol :: Text + renderOperationId :: Text + renderOperationId = renderSymbol @a -instance {-# OVERLAPPABLE #-} (KnownSymbol a) => RenderableSymbol a where +instance (KnownSymbol a) => RenderableSymbol a where renderSymbol = T.pack . show $ symbolVal (Proxy @a) + renderOperationId = T.pack $ symbolVal (Proxy @a) -instance {-# OVERLAPPING #-} (RenderableSymbol a, RenderableSymbol b) => RenderableSymbol '(a, b) where +instance (RenderableSymbol a, RenderableSymbol b) => RenderableSymbol '(a, b) where renderSymbol = "(" <> (renderSymbol @a) <> ", " <> (renderSymbol @b) <> ")" + renderOperationId = renderOperationId @a <> "_" <> renderOperationId @b + +newtype RenderableTypeName a = RenderableTypeName a + +instance (GRenderableSymbol (Rep a)) => RenderableSymbol (RenderableTypeName a) where + renderSymbol = grenderSymbol @(Rep a) + renderOperationId = grenderSymbol @(Rep a) + +class GRenderableSymbol f where + grenderSymbol :: Text + +instance (KnownSymbol tyName) => GRenderableSymbol (D1 (MetaData tyName modName pkg b) k) where + grenderSymbol = T.pack $ symbolVal (Proxy @tyName) instance (HasOpenApi api, RenderableSymbol name) => HasOpenApi (Named name api) where toOpenApi _ = toOpenApi (Proxy @api) & allOperations . description %~ (Just (dscr <> "\n\n") <>) + & allOperations . operationId ?~ renderOperationId @name where dscr :: Text dscr = @@ -137,9 +155,14 @@ namedClient :: Client m endpoint namedClient = clientIn (Proxy @endpoint) (Proxy @m) ---------------------------------------------- --- Utility to add a combinator to a Named API - +-- | Utility to push a Servant combinator inside Named APIs. +-- +-- For example: +-- @@ +-- From 'V5 ::> (Named "foo" (Get '[JSON] Foo) :<|> Named "bar" (Post '[JSON] Bar)) +-- == +-- Named "foo" (From 'V5 :> Get '[JSON] Foo) :<|> Named "bar" (From 'V5 :> Post '[JSON] Bar) +-- @@ type family x ::> api infixr 4 ::> @@ -147,3 +170,7 @@ infixr 4 ::> type instance x ::> (Named name api) = Named name (x :> api) + +type instance + x ::> (api1 :<|> api2) = + (x ::> api1) :<|> (x ::> api2) 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 8a9cbfc0842..d377fc79835 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -49,7 +49,6 @@ import Wire.API.Error.Empty import Wire.API.MLS.CipherSuite import Wire.API.MLS.KeyPackage import Wire.API.MLS.Servant -import Wire.API.MakesFederatedCall import Wire.API.OAuth import Wire.API.Properties (PropertyKey, PropertyKeysAndValues, RawPropertyValue) import Wire.API.Routes.API @@ -153,7 +152,6 @@ type UserAPI = Named "get-user-unqualified" ( Summary "Get a user by UserId" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> Until 'V2 :> ZLocalUser :> "users" @@ -165,7 +163,6 @@ type UserAPI = Named "get-user-qualified" ( Summary "Get a user by Domain and UserId" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> "users" :> QualifiedCaptureUserId "uid" @@ -186,8 +183,6 @@ type UserAPI = "get-handle-info-unqualified" ( Summary "(deprecated, use /search/contacts) Get information on a user handle" :> Until 'V2 - :> MakesFederatedCall 'Brig "get-user-by-handle" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZUser :> "users" :> "handles" @@ -204,8 +199,6 @@ type UserAPI = "get-user-by-handle-qualified" ( Summary "(deprecated, use /search/contacts) Get information on a user handle" :> Until 'V2 - :> MakesFederatedCall 'Brig "get-user-by-handle" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZUser :> "users" :> "by-handle" @@ -225,7 +218,6 @@ type UserAPI = ( Summary "List users (deprecated)" :> Until 'V2 :> Description "The 'ids' and 'handles' parameters are mutually exclusive." - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZUser :> "users" :> QueryParam' [Optional, Strict, Description "User IDs of users to fetch"] "ids" (CommaSeparatedList UserId) @@ -236,7 +228,6 @@ type UserAPI = "list-users-by-ids-or-handles" ( Summary "List users" :> Description "The 'qualified_ids' and 'qualified_handles' parameters are mutually exclusive." - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZUser :> From 'V4 :> "list-users" @@ -249,7 +240,6 @@ type UserAPI = "list-users-by-ids-or-handles@V3" ( Summary "List users" :> Description "The 'qualified_ids' and 'qualified_handles' parameters are mutually exclusive." - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZUser :> Until 'V4 :> "list-users" @@ -268,7 +258,7 @@ type UserAPI = "get-rich-info" ( Summary "Get a user's rich info" :> CanThrow 'InsufficientTeamPermissions - :> ZUser + :> ZLocalUser :> "users" :> CaptureUserId "uid" :> "rich-info" @@ -315,14 +305,13 @@ type SelfAPI = \password, it must be provided. if password is correct, or if neither \ \a verified identity nor a password exists, account deletion \ \is scheduled immediately." - :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'InvalidUser :> CanThrow 'InvalidCode :> CanThrow 'BadCredentials :> CanThrow 'MissingAuth :> CanThrow 'DeleteCodePending :> CanThrow 'OwnerDeletingSelf - :> ZUser + :> ZLocalUser :> "self" :> ReqBody '[JSON] DeleteUser :> MultiVerb 'DELETE '[JSON] DeleteSelfResponses (Maybe Timeout) @@ -333,7 +322,6 @@ type SelfAPI = Named "put-self" ( Summary "Update your profile." - :> MakesFederatedCall 'Brig "send-connection-action" :> ZLocalUser :> ZConn :> "self" @@ -343,6 +331,7 @@ type SelfAPI = :<|> Named "change-phone" ( Summary "Change your phone number." + :> Until 'V6 :> ZUser :> ZConn :> "self" @@ -356,10 +345,10 @@ type SelfAPI = Named "remove-phone" ( Summary "Remove your phone number." + :> Until 'V6 :> Description "Your phone number can only be removed if you also have an \ \email address and a password." - :> MakesFederatedCall 'Brig "send-connection-action" :> ZUser :> "self" :> "phone" @@ -374,7 +363,6 @@ type SelfAPI = :> Description "Your email address can only be removed if you also have a \ \phone number." - :> MakesFederatedCall 'Brig "send-connection-action" :> ZUser :> "self" :> "email" @@ -406,7 +394,6 @@ type SelfAPI = :<|> Named "change-locale" ( Summary "Change your locale." - :> MakesFederatedCall 'Brig "send-connection-action" :> ZLocalUser :> ZConn :> "self" @@ -417,8 +404,6 @@ type SelfAPI = :<|> Named "change-handle" ( Summary "Change your handle." - :> MakesFederatedCall 'Brig "send-connection-action" - :> MakesFederatedCall 'Brig "send-connection-action" :> ZLocalUser :> ZConn :> "self" @@ -440,8 +425,9 @@ type SelfAPI = type UserHandleAPI = Named - "check-user-handles" + "check-user-handles@v6" ( Summary "Check availability of user handles" + :> Until V7 :> ZUser :> "users" :> "handles" @@ -453,8 +439,22 @@ type UserHandleAPI = [Handle] ) :<|> Named - "check-user-handle" + "check-user-handles" + ( Summary "Check availability of user handles" + :> From V7 + :> ZUser + :> "handles" + :> ReqBody '[JSON] CheckHandles + :> MultiVerb + 'POST + '[JSON] + '[Respond 200 "List of free handles" [Handle]] + [Handle] + ) + :<|> Named + "check-user-handle@v6" ( Summary "Check whether a user handle can be taken" + :> Until V7 :> CanThrow 'InvalidHandle :> CanThrow 'HandleNotFound :> ZUser @@ -467,32 +467,58 @@ type UserHandleAPI = '[Respond 200 "Handle is taken" ()] () ) + :<|> Named + "check-user-handle" + ( Summary "Check whether a user handle can be taken" + :> From V7 + :> CanThrow 'InvalidHandle + :> CanThrow 'HandleNotFound + :> ZUser + :> "handles" + :> Capture "handle" Text + :> MultiVerb + 'HEAD + '[JSON] + '[Respond 200 "Handle is taken" ()] + () + ) type AccountAPI = - -- docs/reference/user/registration.md {#RefRegistration} - -- - -- This endpoint can lead to the following events being sent: - -- - UserActivated event to created user, if it is a team invitation or user has an SSO ID - -- - UserIdentityUpdated event to created user, if email code or phone code is provided Named - "register" - ( Summary "Register a new user." - :> Description - "If the environment where the registration takes \ - \place is private and a registered email address \ - \is not whitelisted, a 403 error is returned." - :> MakesFederatedCall 'Brig "send-connection-action" - :> "register" - :> ReqBody '[JSON] NewUserPublic - :> MultiVerb 'POST '[JSON] RegisterResponses (Either RegisterError RegisterSuccess) + "upgrade-personal-to-team" + ( Summary "Upgrade personal user to team owner" + :> "upgrade-personal-to-team" + :> ZLocalUser + :> ReqBody '[JSON] BindingNewTeamUser + :> MultiVerb + 'POST + '[JSON] + UpgradePersonalToTeamResponses + (Either UpgradePersonalToTeamError CreateUserTeam) ) + :<|> + -- docs/reference/user/registration.md {#RefRegistration} + -- + -- This endpoint can lead to the following events being sent: + -- - UserActivated event to created user, if it is a team invitation or user has an SSO ID + -- - UserIdentityUpdated event to created user, if email code or phone code is provided + Named + "register" + ( Summary "Register a new user." + :> Description + "If the environment where the registration takes \ + \place is private and a registered email address \ + \is not whitelisted, a 403 error is returned." + :> "register" + :> ReqBody '[JSON] NewUserPublic + :> MultiVerb 'POST '[JSON] RegisterResponses (Either RegisterError RegisterSuccess) + ) -- This endpoint can lead to the following events being sent: -- UserDeleted event to contacts of deleted user -- MemberLeave event to members for all conversations the user was in (via galley) :<|> Named "verify-delete" ( Summary "Verify account deletion with a code." - :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'InvalidCode :> "delete" :> ReqBody '[JSON] VerifyDeleteUser @@ -503,8 +529,7 @@ type AccountAPI = -- - UserIdentityUpdated event to the user, if email or phone get activated :<|> Named "get-activate" - ( Summary "Activate (i.e. confirm) an email address or phone number." - :> MakesFederatedCall 'Brig "send-connection-action" + ( Summary "Activate (i.e. confirm) an email address." :> Description "See also 'POST /activate' which has a larger feature set." :> CanThrow 'UserKeyExists :> CanThrow 'InvalidActivationCodeWrongUser @@ -527,11 +552,10 @@ type AccountAPI = -- - UserIdentityUpdated event to the user, if email or phone get activated :<|> Named "post-activate" - ( Summary "Activate (i.e. confirm) an email address or phone number." + ( Summary "Activate (i.e. confirm) an email address." :> Description "Activation only succeeds once and the number of \ \failed attempts for a valid key is limited." - :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'UserKeyExists :> CanThrow 'InvalidActivationCodeWrongUser :> CanThrow 'InvalidActivationCodeWrongCode @@ -551,7 +575,6 @@ type AccountAPI = ( Summary "Send (or resend) an email activation code." :> CanThrow 'UserKeyExists :> CanThrow 'InvalidEmail - :> CanThrow 'InvalidPhone :> CanThrow 'BlacklistedEmail :> CanThrow 'CustomerExtensionBlockedDomain :> "activate" @@ -645,7 +668,6 @@ type PrekeyAPI = "get-users-prekeys-client-unqualified" ( Summary "(deprecated) Get a prekey for a specific client of a user." :> Until 'V2 - :> MakesFederatedCall 'Brig "claim-prekey" :> ZUser :> "users" :> CaptureUserId "uid" @@ -656,7 +678,6 @@ type PrekeyAPI = :<|> Named "get-users-prekeys-client-qualified" ( Summary "Get a prekey for a specific client of a user." - :> MakesFederatedCall 'Brig "claim-prekey" :> ZUser :> "users" :> QualifiedCaptureUserId "uid" @@ -668,7 +689,6 @@ type PrekeyAPI = "get-users-prekey-bundle-unqualified" ( Summary "(deprecated) Get a prekey for each client of a user." :> Until 'V2 - :> MakesFederatedCall 'Brig "claim-prekey-bundle" :> ZUser :> "users" :> CaptureUserId "uid" @@ -678,7 +698,6 @@ type PrekeyAPI = :<|> Named "get-users-prekey-bundle-qualified" ( Summary "Get a prekey for each client of a user." - :> MakesFederatedCall 'Brig "claim-prekey-bundle" :> ZUser :> "users" :> QualifiedCaptureUserId "uid" @@ -702,7 +721,6 @@ type PrekeyAPI = ( Summary "(deprecated) Given a map of user IDs to client IDs return a prekey for each one." :> Description "You can't request information for more users than maximum conversation size." - :> MakesFederatedCall 'Brig "claim-multi-prekey-bundle" :> ZUser :> Until 'V4 :> "users" @@ -715,7 +733,6 @@ type PrekeyAPI = ( Summary "(deprecated) Given a map of user IDs to client IDs return a prekey for each one." :> Description "You can't request information for more users than maximum conversation size." - :> MakesFederatedCall 'Brig "claim-multi-prekey-bundle" :> ZUser :> From 'V4 :> "users" @@ -733,16 +750,15 @@ type UserClientAPI = -- - ClientAdded event to self -- - ClientRemoved event to self, if removing old clients due to max number Named - "add-client-v5" + "add-client-v6" ( Summary "Register a new client" - :> Until 'V6 - :> MakesFederatedCall 'Brig "send-connection-action" + :> Until 'V7 :> CanThrow 'TooManyClients :> CanThrow 'MissingAuth :> CanThrow 'MalformedPrekeys :> CanThrow 'CodeAuthenticationFailed :> CanThrow 'CodeAuthenticationRequired - :> ZUser + :> ZLocalUser :> ZConn :> "clients" :> ReqBody '[JSON] NewClient @@ -752,20 +768,19 @@ type UserClientAPI = ( WithHeaders ClientHeaders Client - (VersionedRespond 'V5 201 "Client registered" Client) + (VersionedRespond 'V6 201 "Client registered" Client) ) ) :<|> Named "add-client" ( Summary "Register a new client" :> From 'V6 - :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'TooManyClients :> CanThrow 'MissingAuth :> CanThrow 'MalformedPrekeys :> CanThrow 'CodeAuthenticationFailed :> CanThrow 'CodeAuthenticationRequired - :> ZUser + :> ZLocalUser :> ZConn :> "clients" :> ReqBody '[JSON] NewClient @@ -802,21 +817,21 @@ type UserClientAPI = :> MultiVerb 'DELETE '[JSON] '[RespondEmpty 200 "Client deleted"] () ) :<|> Named - "list-clients-v5" + "list-clients-v6" ( Summary "List the registered clients" - :> Until 'V6 + :> Until 'V7 :> ZUser :> "clients" :> MultiVerb1 'GET '[JSON] - ( VersionedRespond 'V5 200 "List of clients" [Client] + ( VersionedRespond 'V6 200 "List of clients" [Client] ) ) :<|> Named "list-clients" ( Summary "List the registered clients" - :> From 'V6 + :> From 'V7 :> ZUser :> "clients" :> MultiVerb1 @@ -826,9 +841,9 @@ type UserClientAPI = ) ) :<|> Named - "get-client-v5" + "get-client-v6" ( Summary "Get a registered client by ID" - :> Until 'V6 + :> Until 'V7 :> ZUser :> "clients" :> CaptureClientId "client" @@ -836,14 +851,14 @@ type UserClientAPI = 'GET '[JSON] '[ EmptyErrorForLegacyReasons 404 "Client not found", - VersionedRespond 'V5 200 "Client found" Client + VersionedRespond 'V6 200 "Client found" Client ] (Maybe Client) ) :<|> Named "get-client" ( Summary "Get a registered client by ID" - :> From 'V6 + :> From 'V7 :> ZUser :> "clients" :> CaptureClientId "client" @@ -933,7 +948,6 @@ type ClientAPI = "get-user-clients-unqualified" ( Summary "Get all of a user's clients" :> Until 'V2 - :> MakesFederatedCall 'Brig "get-user-clients" :> "users" :> CaptureUserId "uid" :> "clients" @@ -942,7 +956,6 @@ type ClientAPI = :<|> Named "get-user-clients-qualified" ( Summary "Get all of a user's clients" - :> MakesFederatedCall 'Brig "get-user-clients" :> "users" :> QualifiedCaptureUserId "uid" :> "clients" @@ -952,7 +965,6 @@ type ClientAPI = "get-user-client-unqualified" ( Summary "Get a specific client of a user" :> Until 'V2 - :> MakesFederatedCall 'Brig "get-user-clients" :> "users" :> CaptureUserId "uid" :> "clients" @@ -962,7 +974,6 @@ type ClientAPI = :<|> Named "get-user-client-qualified" ( Summary "Get a specific client of a user" - :> MakesFederatedCall 'Brig "get-user-clients" :> "users" :> QualifiedCaptureUserId "uid" :> "clients" @@ -973,7 +984,6 @@ type ClientAPI = "list-clients-bulk" ( Summary "List all clients for a set of user ids" :> Until 'V2 - :> MakesFederatedCall 'Brig "get-user-clients" :> ZUser :> "users" :> "list-clients" @@ -984,7 +994,6 @@ type ClientAPI = "list-clients-bulk-v2" ( Summary "List all clients for a set of user ids" :> Until 'V2 - :> MakesFederatedCall 'Brig "get-user-clients" :> ZUser :> "users" :> "list-clients" @@ -997,7 +1006,6 @@ type ClientAPI = ( Summary "List all clients for a set of user ids" :> Description "If a backend is unreachable, the clients from that backend will be omitted from the response" :> From 'V2 - :> MakesFederatedCall 'Brig "get-user-clients" :> ZUser :> "users" :> "list-clients" @@ -1018,7 +1026,6 @@ type ConnectionAPI = "create-connection-unqualified" ( Summary "Create a connection to another user" :> Until 'V2 - :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'MissingLegalholdConsentOldClients :> CanThrow 'MissingLegalholdConsent :> CanThrow 'InvalidUser @@ -1042,8 +1049,6 @@ type ConnectionAPI = :<|> Named "create-connection" ( Summary "Create a connection to another user" - :> MakesFederatedCall 'Brig "get-users-by-ids" - :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'MissingLegalholdConsentOldClients :> CanThrow 'MissingLegalholdConsent :> CanThrow 'InvalidUser @@ -1123,7 +1128,6 @@ type ConnectionAPI = "update-connection-unqualified" ( Summary "Update a connection to another user" :> Until 'V2 - :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'MissingLegalholdConsentOldClients :> CanThrow 'MissingLegalholdConsent :> CanThrow 'InvalidUser @@ -1152,8 +1156,6 @@ type ConnectionAPI = Named "update-connection" ( Summary "Update a connection to another user" - :> MakesFederatedCall 'Brig "get-users-by-ids" - :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'MissingLegalholdConsentOldClients :> CanThrow 'MissingLegalholdConsent :> CanThrow 'InvalidUser @@ -1175,9 +1177,7 @@ type ConnectionAPI = :<|> Named "search-contacts" ( Summary "Search for users" - :> MakesFederatedCall 'Brig "get-users-by-ids" - :> MakesFederatedCall 'Brig "search-users" - :> ZUser + :> ZLocalUser :> "search" :> "contacts" :> QueryParam' '[Required, Strict, Description "Search query"] "q" Text @@ -1379,7 +1379,7 @@ type SearchAPI = Description "Number of results to return (min: 1, max: 500, default: 15)" ] "size" - (Range 1 500 Int32) + (Range 1 500 Int) :> QueryParam' [ Optional, Strict, @@ -1406,7 +1406,6 @@ type AuthAPI = \ Every other combination is invalid.\ \ Access tokens can be given as query parameter or authorisation\ \ header, with the latter being preferred." - :> MakesFederatedCall 'Brig "send-connection-action" :> QueryParam "client_id" ClientId :> Cookies '["zuid" ::: SomeUserToken] :> Bearer SomeAccessToken @@ -1417,6 +1416,7 @@ type AuthAPI = "send-login-code" ( "login" :> "send" + :> Until 'V6 :> Summary "Send a login code to a verified phone number" :> Description "This operation generates and sends a login code via sms for phone login.\ @@ -1437,7 +1437,6 @@ type AuthAPI = ( "login" :> Summary "Authenticate a user to obtain a cookie and first access token" :> Description "Logins are throttled at the server's discretion" - :> MakesFederatedCall 'Brig "send-connection-action" :> ReqBody '[JSON] Login :> QueryParam' [ Optional, @@ -1558,7 +1557,8 @@ type TeamsAPI = :> CanThrow 'BlacklistedEmail :> CanThrow 'TooManyTeamInvitations :> CanThrow 'InsufficientTeamPermissions - :> ZUser + :> CanThrow 'InvalidInvitationCode + :> ZLocalUser :> "teams" :> Capture "tid" TeamId :> "invitations" @@ -1635,7 +1635,7 @@ type TeamsAPI = :> "teams" :> "invitations" :> "by-email" - :> QueryParam' '[Required, Strict, Description "Email address"] "email" Email + :> QueryParam' '[Required, Strict, Description "Email address"] "email" EmailAddress :> MultiVerb 'HEAD '[JSON] @@ -1658,6 +1658,23 @@ type TeamsAPI = '[JSON] (Respond 200 "Number of team members" TeamSize) ) + :<|> Named + "accept-team-invitation" + ( Summary "Accept a team invitation, changing a personal account into a team member account." + :> CanThrow 'PendingInvitationNotFound + :> CanThrow 'TooManyTeamMembers + :> CanThrow 'MissingIdentity + :> CanThrow 'InvalidActivationCodeWrongUser + :> CanThrow 'InvalidActivationCodeWrongCode + :> CanThrow 'BadCredentials + :> CanThrow 'MissingAuth + :> ZLocalUser + :> "teams" + :> "invitations" + :> "accept" + :> ReqBody '[JSON] AcceptTeamInvitation + :> MultiVerb 'POST '[JSON] '[RespondEmpty 200 "Team invitation accepted."] () + ) type SystemSettingsAPI = Named diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs index 635e6711c4a..4c072cf0210 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs @@ -43,8 +43,9 @@ type DeleteResponses = type BotAPI = Named - "add-bot" + "add-bot@v6" ( Summary "Add bot" + :> Until V7 :> CanThrow 'AccessDenied :> CanThrow 'InvalidConversation :> CanThrow 'TooManyConversationMembers @@ -52,22 +53,53 @@ type BotAPI = :> ZAccess :> ZConn :> "conversations" - :> Capture "Conversation ID" ConvId + :> Capture "conv" ConvId :> "bots" :> ReqBody '[JSON] AddBot :> MultiVerb1 'POST '[JSON] (Respond 201 "" AddBotResponse) ) :<|> Named - "remove-bot" + "add-bot" + ( Summary "Add bot" + :> From V7 + :> CanThrow 'AccessDenied + :> CanThrow 'InvalidConversation + :> CanThrow 'TooManyConversationMembers + :> CanThrow 'ServiceDisabled + :> ZAccess + :> ZConn + :> "bot" + :> "conversations" + :> Capture "conv" ConvId + :> ReqBody '[JSON] AddBot + :> MultiVerb1 'POST '[JSON] (Respond 201 "" AddBotResponse) + ) + :<|> Named + "remove-bot@v6" ( Summary "Remove bot" + :> Until V7 :> CanThrow 'AccessDenied :> CanThrow 'InvalidConversation :> ZAccess :> ZConn :> "conversations" - :> Capture "Conversation ID" ConvId + :> Capture "conv" ConvId :> "bots" - :> Capture "Bot ID" BotId + :> Capture "bot" BotId + :> MultiVerb 'DELETE '[JSON] DeleteResponses (Maybe RemoveBotResponse) + ) + :<|> Named + "remove-bot" + ( Summary "Remove bot" + :> From V7 + :> CanThrow 'AccessDenied + :> CanThrow 'InvalidConversation + :> ZAccess + :> ZConn + :> "bot" + :> "conversations" + :> Capture "conv" ConvId + :> Capture "bot" BotId :> MultiVerb 'DELETE '[JSON] DeleteResponses (Maybe RemoveBotResponse) ) :<|> Named @@ -114,9 +146,9 @@ type BotAPI = :> MultiVerb1 'POST '[JSON] (RespondEmpty 200 "") ) :<|> Named - "bot-get-client-v5" + "bot-get-client-v6" ( Summary "Get client for bot" - :> Until 'V6 + :> Until 'V7 :> CanThrow 'AccessDenied :> CanThrow 'ClientNotFound :> ZBot @@ -126,14 +158,14 @@ type BotAPI = 'GET '[JSON] '[ ErrorResponse 'ClientNotFound, - VersionedRespond 'V5 200 "Client found" Client + VersionedRespond 'V6 200 "Client found" Client ] (Maybe Client) ) :<|> Named "bot-get-client" ( Summary "Get client for bot" - :> From 'V6 + :> From 'V7 :> CanThrow 'AccessDenied :> CanThrow 'ClientNotFound :> ZBot @@ -178,7 +210,7 @@ type BotAPI = :> ZBot :> "bot" :> "users" - :> Capture "User ID" UserId + :> Capture "user" UserId :> "clients" :> Get '[JSON] [PubClient] ) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs index a3173c0700b..19d20cd30f6 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs @@ -24,11 +24,14 @@ import Servant (JSON) import Servant hiding (Handler, JSON, Tagged, addHeader, respond) import Servant.OpenApi.Internal.Orphans () import Wire.API.Error +import Wire.API.Error.Brig import Wire.API.OAuth +import Wire.API.Password import Wire.API.Routes.API import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named (Named) import Wire.API.Routes.Public +import Wire.API.Routes.Version type OAuthAPI = Named @@ -36,7 +39,7 @@ type OAuthAPI = ( Summary "Get OAuth client information" :> CanThrow 'OAuthFeatureDisabled :> CanThrow 'OAuthClientNotFound - :> ZUser + :> ZLocalUser :> "oauth" :> "clients" :> Capture' '[Description "The ID of the OAuth client"] "OAuthClientId" OAuthClientId @@ -52,7 +55,7 @@ type OAuthAPI = "create-oauth-auth-code" ( Summary "Create an OAuth authorization code" :> Description "Currently only supports the 'code' response type, which corresponds to the authorization code flow." - :> ZUser + :> ZLocalUser :> "oauth" :> "authorization" :> "codes" @@ -96,7 +99,7 @@ type OAuthAPI = "get-oauth-applications" ( Summary "Get OAuth applications with account access" :> Description "Get all OAuth applications with active account access for a user." - :> ZUser + :> ZLocalUser :> "oauth" :> "applications" :> MultiVerb1 @@ -104,19 +107,52 @@ type OAuthAPI = '[JSON] (Respond 200 "OAuth applications found" [OAuthApplication]) ) + :<|> Named + "revoke-oauth-account-access-v6" + ( Summary "Revoke account access from an OAuth application" + :> ZLocalUser + :> Until 'V7 + :> "oauth" + :> "applications" + :> Capture' '[Description "The ID of the OAuth client"] "OAuthClientId" OAuthClientId + :> MultiVerb + 'DELETE + '[JSON] + '[RespondEmpty 204 "OAuth application access revoked"] + () + ) :<|> Named "revoke-oauth-account-access" ( Summary "Revoke account access from an OAuth application" - :> ZUser + :> CanThrow 'AccessDenied + :> ZLocalUser + :> From 'V7 :> "oauth" :> "applications" :> Capture' '[Description "The ID of the OAuth client"] "OAuthClientId" OAuthClientId + :> "sessions" + :> ReqBody '[JSON] PasswordReqBody :> MultiVerb 'DELETE '[JSON] '[RespondEmpty 204 "OAuth application access revoked"] () ) + :<|> Named + "delete-oauth-refresh-token" + ( Summary "Revoke an active OAuth session" + :> Description "Revoke an active OAuth session by providing the refresh token ID." + :> ZLocalUser + :> CanThrow 'AccessDenied + :> CanThrow 'OAuthClientNotFound + :> "oauth" + :> "applications" + :> Capture' '[Description "The ID of the OAuth client"] "OAuthClientId" OAuthClientId + :> "sessions" + :> Capture' '[Description "The ID of the refresh token"] "RefreshTokenId" OAuthRefreshTokenId + :> ReqBody '[JSON] PasswordReqBody + :> Delete '[JSON] () + ) type CreateOAuthAuthorizationCodeHeaders = '[Header "Location" RedirectUrl] diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Services.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Services.hs index 0fff51c6f9a..df62901e3ee 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Services.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Services.hs @@ -129,6 +129,8 @@ type ServicesAPI = ( Summary "Get services tags" :> CanThrow 'AccessDenied :> ZUser + :> "services" + :> "tags" :> Get '[JSON] ServiceTagList ) :<|> Named diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs b/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs index d9c7ca0ed3e..22cd1ddf5f0 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs @@ -29,10 +29,10 @@ import URI.ByteString import Wire.API.Asset import Wire.API.Error import Wire.API.Error.Cargohold -import Wire.API.MakesFederatedCall import Wire.API.Routes.API import Wire.API.Routes.AssetBody import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named import Wire.API.Routes.Public import Wire.API.Routes.QualifiedCapture import Wire.API.Routes.Version @@ -40,6 +40,15 @@ import Wire.API.Routes.Version data PrincipalTag = UserPrincipalTag | BotPrincipalTag | ProviderPrincipalTag deriving (Eq, Show) +instance RenderableSymbol UserPrincipalTag where + renderSymbol = "user" + +instance RenderableSymbol BotPrincipalTag where + renderSymbol = "bot" + +instance RenderableSymbol ProviderPrincipalTag where + renderSymbol = "provider" + type family PrincipalId (tag :: PrincipalTag) = (id :: Type) | id -> tag where PrincipalId 'UserPrincipalTag = Local UserId PrincipalId 'BotPrincipalTag = BotId @@ -80,7 +89,6 @@ type AssetStreaming = RespondStreaming 200 "Asset returned directly with content type `application/octet-stream`" - NoFraming OctetStream type GetAsset = @@ -128,192 +136,214 @@ type CargoholdAPI = -- This was introduced before API versioning, and the user endpoints contain a -- v3 suffix, which is removed starting from API V2. type BaseAPIv3 (tag :: PrincipalTag) = - ( Summary "Upload an asset" - :> CanThrow 'AssetTooLarge - :> CanThrow 'InvalidLength - :> tag - :> AssetBody - :> MultiVerb - 'POST - '[JSON] - '[ WithHeaders - (AssetLocationHeader Relative) - (Asset, AssetLocation Relative) - (Respond 201 "Asset posted" Asset) - ] - (Asset, AssetLocation Relative) - ) - :<|> ( Summary "Download an asset" - :> tag - :> Capture "key" AssetKey - :> Header "Asset-Token" AssetToken - :> QueryParam "asset_token" AssetToken - :> ZHostOpt - :> GetAsset - ) - :<|> ( Summary "Delete an asset" - :> CanThrow 'AssetNotFound - :> CanThrow 'Unauthorised - :> tag - :> Capture "key" AssetKey - :> MultiVerb - 'DELETE - '[JSON] - '[RespondEmpty 200 "Asset deleted"] - () - ) + Named + '("assets-upload-v3", tag) + ( Summary "Upload an asset" + :> CanThrow 'AssetTooLarge + :> CanThrow 'InvalidLength + :> tag + :> AssetBody + :> MultiVerb + 'POST + '[JSON] + '[ WithHeaders + (AssetLocationHeader Relative) + (Asset, AssetLocation Relative) + (Respond 201 "Asset posted" Asset) + ] + (Asset, AssetLocation Relative) + ) + :<|> Named + '("assets-download-v3", tag) + ( Summary "Download an asset" + :> tag + :> Capture "key" AssetKey + :> Header "Asset-Token" AssetToken + :> QueryParam "asset_token" AssetToken + :> ZHostOpt + :> GetAsset + ) + :<|> Named + '("assets-delete-v3", tag) + ( Summary "Delete an asset" + :> CanThrow 'AssetNotFound + :> CanThrow 'Unauthorised + :> tag + :> Capture "key" AssetKey + :> MultiVerb + 'DELETE + '[JSON] + '[RespondEmpty 200 "Asset deleted"] + () + ) -- | Qualified asset API. Only download and delete endpoints are supported, as -- upload has stayed unqualified. These endpoints also predate API versioning, -- and contain a v4 suffix. type QualifiedAPI = - ( Summary "Download an asset" - :> Until 'V2 - :> Description - "**Note**: local assets result in a redirect, \ - \while remote assets are streamed directly." - :> MakesFederatedCall 'Cargohold "get-asset" - :> MakesFederatedCall 'Cargohold "stream-asset" - :> ZLocalUser - :> "assets" - :> "v4" - :> QualifiedCapture "key" AssetKey - :> Header "Asset-Token" AssetToken - :> QueryParam "asset_token" AssetToken - :> ZHostOpt - :> MultiVerb - 'GET - '() - '[ ErrorResponse 'AssetNotFound, - AssetRedirect, - AssetStreaming - ] - (Maybe LocalOrRemoteAsset) - ) - :<|> ( Summary "Delete an asset" - :> Until 'V2 - :> Description "**Note**: only local assets can be deleted." - :> CanThrow 'AssetNotFound - :> CanThrow 'Unauthorised - :> ZLocalUser - :> "assets" - :> "v4" - :> QualifiedCapture "key" AssetKey - :> MultiVerb - 'DELETE - '[JSON] - '[RespondEmpty 200 "Asset deleted"] - () - ) + Named + "assets-download-v4" + ( Summary "Download an asset" + :> Until 'V2 + :> Description + "**Note**: local assets result in a redirect, \ + \while remote assets are streamed directly." + :> ZLocalUser + :> "assets" + :> "v4" + :> QualifiedCapture "key" AssetKey + :> Header "Asset-Token" AssetToken + :> QueryParam "asset_token" AssetToken + :> ZHostOpt + :> MultiVerb + 'GET + '() + '[ ErrorResponse 'AssetNotFound, + AssetRedirect, + AssetStreaming + ] + (Maybe LocalOrRemoteAsset) + ) + :<|> Named + "assets-delete-v4" + ( Summary "Delete an asset" + :> Until 'V2 + :> Description "**Note**: only local assets can be deleted." + :> CanThrow 'AssetNotFound + :> CanThrow 'Unauthorised + :> ZLocalUser + :> "assets" + :> "v4" + :> QualifiedCapture "key" AssetKey + :> MultiVerb + 'DELETE + '[JSON] + '[RespondEmpty 200 "Asset deleted"] + () + ) -- Old endpoints, predating BaseAPIv3, and therefore API versioning. type LegacyAPI = - ( ZLocalUser - :> Until 'V2 - :> "assets" - :> QueryParam' [Required, Strict] "conv_id" ConvId - :> Capture "id" AssetId - :> GetAsset - ) - :<|> ( ZLocalUser - :> Until 'V2 - :> "conversations" - :> Capture "cnv" ConvId - :> "assets" - :> Capture "id" AssetId - :> GetAsset - ) - :<|> ( ZLocalUser - :> Until 'V2 - :> "conversations" - :> Capture "cnv" ConvId - :> "otr" - :> "assets" - :> Capture "id" AssetId - :> GetAsset - ) + Named + "assets-download-legacy" + ( ZLocalUser + :> Until 'V2 + :> "assets" + :> QueryParam' [Required, Strict] "conv_id" ConvId + :> Capture "id" AssetId + :> GetAsset + ) + :<|> Named + "assets-conv-download-legacy" + ( ZLocalUser + :> Until 'V2 + :> "conversations" + :> Capture "cnv" ConvId + :> "assets" + :> Capture "id" AssetId + :> GetAsset + ) + :<|> Named + "assets-conv-otr-download-legacy" + ( ZLocalUser + :> Until 'V2 + :> "conversations" + :> Capture "cnv" ConvId + :> "otr" + :> "assets" + :> Capture "id" AssetId + :> GetAsset + ) -- | With API versioning, the previous ad-hoc v3/v4 versioning is abandoned, and -- asset endpoints are versioned normally as part of the public API, without any -- explicit prefix. type MainAPI = - ( Summary "Renew an asset token" - :> From 'V2 - :> CanThrow 'AssetNotFound - :> CanThrow 'Unauthorised - :> ZLocalUser - :> "assets" - :> Capture "key" AssetKey - :> "token" - :> Post '[JSON] NewAssetToken - ) - :<|> ( Summary "Delete an asset token" - :> From 'V2 - :> Description "**Note**: deleting the token makes the asset public." - :> ZLocalUser - :> "assets" - :> Capture "key" AssetKey - :> "token" - :> MultiVerb - 'DELETE - '[JSON] - '[RespondEmpty 200 "Asset token deleted"] - () - ) - :<|> ( Summary "Upload an asset" - :> From 'V2 - :> CanThrow 'AssetTooLarge - :> CanThrow 'InvalidLength - :> ZLocalUser - :> "assets" - :> AssetBody - :> MultiVerb - 'POST - '[JSON] - '[ WithHeaders - (AssetLocationHeader Relative) - (Asset, AssetLocation Relative) - (Respond 201 "Asset posted" Asset) - ] - (Asset, AssetLocation Relative) - ) - :<|> ( Summary "Download an asset" - :> From 'V2 - :> Description - "**Note**: local assets result in a redirect, \ - \while remote assets are streamed directly." - :> MakesFederatedCall 'Cargohold "get-asset" - :> MakesFederatedCall 'Cargohold "stream-asset" - :> CanThrow 'NoMatchingAssetEndpoint - :> ZLocalUser - :> "assets" - :> QualifiedCapture "key" AssetKey - :> Header "Asset-Token" AssetToken - :> QueryParam "asset_token" AssetToken - :> ZHostOpt - :> MultiVerb - 'GET - '() - '[ ErrorResponse 'AssetNotFound, - AssetRedirect, - AssetStreaming - ] - (Maybe LocalOrRemoteAsset) - ) - :<|> ( Summary "Delete an asset" - :> From 'V2 - :> Description "**Note**: only local assets can be deleted." - :> CanThrow 'AssetNotFound - :> CanThrow 'Unauthorised - :> ZLocalUser - :> "assets" - :> QualifiedCapture "key" AssetKey - :> MultiVerb - 'DELETE - '[JSON] - '[RespondEmpty 200 "Asset deleted"] - () - ) + Named + "tokens-renew" + ( Summary "Renew an asset token" + :> From 'V2 + :> CanThrow 'AssetNotFound + :> CanThrow 'Unauthorised + :> ZLocalUser + :> "assets" + :> Capture "key" AssetKey + :> "token" + :> Post '[JSON] NewAssetToken + ) + :<|> Named + "tokens-delete" + ( Summary "Delete an asset token" + :> From 'V2 + :> Description "**Note**: deleting the token makes the asset public." + :> ZLocalUser + :> "assets" + :> Capture "key" AssetKey + :> "token" + :> MultiVerb + 'DELETE + '[JSON] + '[RespondEmpty 200 "Asset token deleted"] + () + ) + :<|> Named + "assets-upload" + ( Summary "Upload an asset" + :> From 'V2 + :> CanThrow 'AssetTooLarge + :> CanThrow 'InvalidLength + :> ZLocalUser + :> "assets" + :> AssetBody + :> MultiVerb + 'POST + '[JSON] + '[ WithHeaders + (AssetLocationHeader Relative) + (Asset, AssetLocation Relative) + (Respond 201 "Asset posted" Asset) + ] + (Asset, AssetLocation Relative) + ) + :<|> Named + "assets-download" + ( Summary "Download an asset" + :> From 'V2 + :> Description + "**Note**: local assets result in a redirect, \ + \while remote assets are streamed directly." + :> CanThrow 'NoMatchingAssetEndpoint + :> ZLocalUser + :> "assets" + :> QualifiedCapture "key" AssetKey + :> Header "Asset-Token" AssetToken + :> QueryParam "asset_token" AssetToken + :> ZHostOpt + :> MultiVerb + 'GET + '() + '[ ErrorResponse 'AssetNotFound, + AssetRedirect, + AssetStreaming + ] + (Maybe LocalOrRemoteAsset) + ) + :<|> Named + "assets-delete" + ( Summary "Delete an asset" + :> From 'V2 + :> Description "**Note**: only local assets can be deleted." + :> CanThrow 'AssetNotFound + :> CanThrow 'Unauthorised + :> ZLocalUser + :> "assets" + :> QualifiedCapture "key" AssetKey + :> MultiVerb + 'DELETE + '[JSON] + '[RespondEmpty 200 "Asset deleted"] + () + ) data CargoholdAPITag diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs index 52ec0ee5022..e7610068772 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs @@ -20,7 +20,7 @@ module Wire.API.Routes.Public.Galley where -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.Bot diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs index 6d4359b545c..46c89d9c530 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs @@ -17,11 +17,10 @@ module Wire.API.Routes.Public.Galley.Bot where -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.MakesFederatedCall import Wire.API.Message import Wire.API.Provider.Bot import Wire.API.Routes.MultiVerb @@ -32,9 +31,7 @@ import Wire.API.Routes.Public.Galley.Messaging type BotAPI = Named "post-bot-message-unqualified" - ( MakesFederatedCall 'Galley "on-message-sent" - :> MakesFederatedCall 'Brig "get-user-clients" - :> ZBot + ( ZBot :> ZConversation :> CanThrow 'ConvNotFound :> "bot" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index 064dd35f673..60802b1c5bc 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -23,7 +23,7 @@ import Data.Id import Data.Range import Data.SOP (I (..), NS (..)) import Imports hiding (head) -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Conversation import Wire.API.Conversation.Code @@ -35,9 +35,9 @@ import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Event.Conversation import Wire.API.MLS.GroupInfo +import Wire.API.MLS.Keys import Wire.API.MLS.Servant import Wire.API.MLS.SubConversation -import Wire.API.MakesFederatedCall import Wire.API.OAuth import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named @@ -142,7 +142,6 @@ type ConversationAPI = "get-conversation@v2" ( Summary "Get a conversation by ID" :> Until 'V3 - :> MakesFederatedCall 'Galley "get-conversations" :> CanThrow 'ConvNotFound :> CanThrow 'ConvAccessDenied :> ZLocalUser @@ -155,7 +154,6 @@ type ConversationAPI = ( Summary "Get a conversation by ID" :> From 'V3 :> Until 'V6 - :> MakesFederatedCall 'Galley "get-conversations" :> CanThrow 'ConvNotFound :> CanThrow 'ConvAccessDenied :> ZLocalUser @@ -167,7 +165,6 @@ type ConversationAPI = "get-conversation" ( Summary "Get a conversation by ID" :> From 'V6 - :> MakesFederatedCall 'Galley "get-conversations" :> CanThrow 'ConvNotFound :> CanThrow 'ConvAccessDenied :> ZLocalUser @@ -190,7 +187,6 @@ type ConversationAPI = "get-group-info" ( Summary "Get MLS group information" :> From 'V5 - :> MakesFederatedCall 'Galley "query-group-info" :> CanThrow 'ConvNotFound :> CanThrow 'MLSMissingGroupInfo :> CanThrow 'MLSNotEnabled @@ -297,7 +293,6 @@ type ConversationAPI = :<|> Named "list-conversations@v1" ( Summary "Get conversation metadata for a list of conversation ids" - :> MakesFederatedCall 'Galley "get-conversations" :> Until 'V2 :> ZLocalUser :> "conversations" @@ -309,7 +304,6 @@ type ConversationAPI = :<|> Named "list-conversations@v2" ( Summary "Get conversation metadata for a list of conversation ids" - :> MakesFederatedCall 'Galley "get-conversations" :> From 'V2 :> Until 'V3 :> ZLocalUser @@ -329,7 +323,6 @@ type ConversationAPI = :<|> Named "list-conversations@v5" ( Summary "Get conversation metadata for a list of conversation ids" - :> MakesFederatedCall 'Galley "get-conversations" :> From 'V3 :> Until 'V6 :> ZLocalUser @@ -349,7 +342,6 @@ type ConversationAPI = :<|> Named "list-conversations" ( Summary "Get conversation metadata for a list of conversation ids" - :> MakesFederatedCall 'Galley "get-conversations" :> From 'V6 :> ZLocalUser :> "conversations" @@ -379,9 +371,6 @@ type ConversationAPI = "create-group-conversation@v2" ( Summary "Create a new conversation" :> DescriptionOAuthScope 'WriteConversations - :> MakesFederatedCall 'Brig "api-version" - :> MakesFederatedCall 'Galley "on-conversation-created" - :> MakesFederatedCall 'Galley "on-conversation-updated" :> Until 'V3 :> CanThrow 'ConvAccessDenied :> CanThrow 'MLSNonEmptyMemberList @@ -402,9 +391,6 @@ type ConversationAPI = "create-group-conversation@v3" ( Summary "Create a new conversation" :> DescriptionOAuthScope 'WriteConversations - :> MakesFederatedCall 'Brig "api-version" - :> MakesFederatedCall 'Galley "on-conversation-created" - :> MakesFederatedCall 'Galley "on-conversation-updated" :> From 'V3 :> Until 'V4 :> CanThrow 'ConvAccessDenied @@ -425,10 +411,6 @@ type ConversationAPI = :<|> Named "create-group-conversation@v5" ( Summary "Create a new conversation" - :> MakesFederatedCall 'Brig "api-version" - :> MakesFederatedCall 'Brig "get-not-fully-connected-backends" - :> MakesFederatedCall 'Galley "on-conversation-created" - :> MakesFederatedCall 'Galley "on-conversation-updated" :> From 'V4 :> Until 'V6 :> CanThrow 'ConvAccessDenied @@ -450,10 +432,6 @@ type ConversationAPI = :<|> Named "create-group-conversation" ( Summary "Create a new conversation" - :> MakesFederatedCall 'Brig "api-version" - :> MakesFederatedCall 'Brig "get-not-fully-connected-backends" - :> MakesFederatedCall 'Galley "on-conversation-created" - :> MakesFederatedCall 'Galley "on-conversation-updated" :> From 'V6 :> CanThrow 'ConvAccessDenied :> CanThrow 'MLSNonEmptyMemberList @@ -539,7 +517,6 @@ type ConversationAPI = "get-subconversation" ( Summary "Get information about an MLS subconversation" :> From 'V5 - :> MakesFederatedCall 'Galley "get-sub-conversation" :> CanThrow 'ConvNotFound :> CanThrow 'ConvAccessDenied :> CanThrow 'MLSSubConvUnsupportedConvType @@ -561,8 +538,6 @@ type ConversationAPI = "leave-subconversation" ( Summary "Leave an MLS subconversation" :> From 'V5 - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "leave-sub-conversation" :> CanThrow 'ConvNotFound :> CanThrow 'ConvAccessDenied :> CanThrow 'MLSProtocolErrorTag @@ -584,7 +559,6 @@ type ConversationAPI = "delete-subconversation" ( Summary "Delete an MLS subconversation" :> From 'V5 - :> MakesFederatedCall 'Galley "delete-sub-conversation" :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvNotFound :> CanThrow 'MLSNotEnabled @@ -604,7 +578,6 @@ type ConversationAPI = "get-subconversation-group-info" ( Summary "Get MLS group information of subconversation" :> From 'V5 - :> MakesFederatedCall 'Galley "query-group-info" :> CanThrow 'ConvNotFound :> CanThrow 'MLSMissingGroupInfo :> CanThrow 'MLSNotEnabled @@ -629,8 +602,6 @@ type ConversationAPI = :<|> Named "create-one-to-one-conversation@v2" ( Summary "Create a 1:1 conversation" - :> MakesFederatedCall 'Brig "api-version" - :> MakesFederatedCall 'Galley "on-conversation-created" :> Until 'V3 :> CanThrow 'ConvAccessDenied :> CanThrow 'InvalidOperation @@ -650,10 +621,10 @@ type ConversationAPI = :> ConversationVerb 'V2 Conversation ) :<|> Named - "create-one-to-one-conversation" + "create-one-to-one-conversation@v6" ( Summary "Create a 1:1 conversation" - :> MakesFederatedCall 'Galley "on-conversation-created" :> From 'V3 + :> Until 'V7 :> CanThrow 'ConvAccessDenied :> CanThrow 'InvalidOperation :> CanThrow 'NoBindingTeamMembers @@ -671,6 +642,26 @@ type ConversationAPI = :> ReqBody '[JSON] NewConv :> ConversationVerb 'V3 Conversation ) + :<|> Named + "create-one-to-one-conversation" + ( Summary "Create a 1:1 conversation" + :> From 'V7 + :> CanThrow 'ConvAccessDenied + :> CanThrow 'InvalidOperation + :> CanThrow 'NoBindingTeamMembers + :> CanThrow 'NonBindingTeam + :> CanThrow 'NotATeamMember + :> CanThrow 'NotConnected + :> CanThrow OperationDenied + :> CanThrow 'TeamNotFound + :> CanThrow 'MissingLegalholdConsent + :> CanThrow UnreachableBackendsLegacy + :> ZLocalUser + :> ZConn + :> "one2one-conversations" + :> ReqBody '[JSON] NewConv + :> ConversationVerb 'V3 Conversation + ) :<|> Named "get-one-to-one-mls-conversation@v5" ( Summary "Get an MLS 1:1 conversation" @@ -679,30 +670,42 @@ type ConversationAPI = :> ZLocalUser :> CanThrow 'MLSNotEnabled :> CanThrow 'NotConnected + :> CanThrow 'MLSFederatedOne2OneNotSupported :> "conversations" :> "one2one" :> QualifiedCapture "usr" UserId :> MultiVerb1 'GET '[JSON] (VersionedRespond 'V5 200 "MLS 1-1 conversation" Conversation) ) :<|> Named - "get-one-to-one-mls-conversation" + "get-one-to-one-mls-conversation@v6" ( Summary "Get an MLS 1:1 conversation" - :> From 'V5 + :> From 'V6 + :> Until 'V7 :> ZLocalUser :> CanThrow 'MLSNotEnabled :> CanThrow 'NotConnected :> "conversations" :> "one2one" :> QualifiedCapture "usr" UserId - :> MultiVerb1 'GET '[JSON] (Respond 200 "MLS 1-1 conversation" Conversation) + :> MultiVerb1 'GET '[JSON] (Respond 200 "MLS 1-1 conversation" (MLSOne2OneConversation MLSPublicKey)) + ) + :<|> Named + "get-one-to-one-mls-conversation" + ( Summary "Get an MLS 1:1 conversation" + :> From 'V7 + :> ZLocalUser + :> CanThrow 'MLSNotEnabled + :> CanThrow 'NotConnected + :> "one2one-conversations" + :> QualifiedCapture "usr" UserId + :> QueryParam "format" MLSPublicKeyFormat + :> MultiVerb1 'GET '[JSON] (Respond 200 "MLS 1-1 conversation" (MLSOne2OneConversation SomeKey)) ) -- This endpoint can lead to the following events being sent: -- - MemberJoin event to members :<|> Named "add-members-to-conversation-unqualified" ( Summary "Add members to an existing conversation (deprecated)" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" :> Until 'V2 :> CanThrow ('ActionDenied 'AddConversationMember) :> CanThrow ('ActionDenied 'LeaveConversation) @@ -726,8 +729,6 @@ type ConversationAPI = :<|> Named "add-members-to-conversation-unqualified2" ( Summary "Add qualified members to an existing conversation." - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" :> Until 'V2 :> CanThrow ('ActionDenied 'AddConversationMember) :> CanThrow ('ActionDenied 'LeaveConversation) @@ -752,8 +753,6 @@ type ConversationAPI = :<|> Named "add-members-to-conversation" ( Summary "Add qualified members to an existing conversation." - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" :> From 'V2 :> CanThrow ('ActionDenied 'AddConversationMember) :> CanThrow ('ActionDenied 'LeaveConversation) @@ -780,7 +779,6 @@ type ConversationAPI = "join-conversation-by-id-unqualified" ( Summary "Join a conversation by its ID (if link access enabled) (deprecated)" :> Until 'V5 - :> MakesFederatedCall 'Galley "on-conversation-updated" :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation @@ -801,7 +799,6 @@ type ConversationAPI = :> Description "If the guest links team feature is disabled, this will fail with 409 GuestLinksDisabled.\ \Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/code-check` which responds with 404 CodeNotFound if guest links are disabled." - :> MakesFederatedCall 'Galley "on-conversation-updated" :> CanThrow 'CodeNotFound :> CanThrow 'InvalidConversationPassword :> CanThrow 'ConvAccessDenied @@ -884,7 +881,7 @@ type ConversationAPI = :> Capture' '[Description "Conversation ID"] "cnv" ConvId :> "features" :> FeatureSymbol GuestLinksConfig - :> Get '[Servant.JSON] (WithStatus GuestLinksConfig) + :> Get '[Servant.JSON] (LockableFeature GuestLinksConfig) ) -- This endpoint can lead to the following events being sent: -- - ConvCodeDelete event to members @@ -928,8 +925,6 @@ type ConversationAPI = "member-typing-unqualified" ( Summary "Sending typing notifications" :> Until 'V3 - :> MakesFederatedCall 'Galley "update-typing-indicator" - :> MakesFederatedCall 'Galley "on-typing-indicator-updated" :> CanThrow 'ConvNotFound :> ZLocalUser :> ZConn @@ -942,8 +937,6 @@ type ConversationAPI = :<|> Named "member-typing-qualified" ( Summary "Sending typing notifications" - :> MakesFederatedCall 'Galley "update-typing-indicator" - :> MakesFederatedCall 'Galley "on-typing-indicator-updated" :> CanThrow 'ConvNotFound :> ZLocalUser :> ZConn @@ -958,10 +951,6 @@ type ConversationAPI = :<|> Named "remove-member-unqualified" ( Summary "Remove a member from a conversation (deprecated)" - :> MakesFederatedCall 'Galley "leave-conversation" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> Until 'V2 :> ZLocalUser :> ZConn @@ -979,10 +968,6 @@ type ConversationAPI = :<|> Named "remove-member" ( Summary "Remove a member from a conversation" - :> MakesFederatedCall 'Galley "leave-conversation" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'RemoveConversationMember) @@ -999,11 +984,9 @@ type ConversationAPI = :<|> Named "update-other-member-unqualified" ( Summary "Update membership of the specified user (deprecated)" + :> Until V7 :> Deprecated :> Description "Use `PUT /conversations/:cnv_domain/:cnv/members/:usr_domain/:usr` instead" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow 'ConvNotFound @@ -1026,9 +1009,6 @@ type ConversationAPI = "update-other-member" ( Summary "Update membership of the specified user" :> Description "**Note**: at least one field has to be provided." - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow 'ConvNotFound @@ -1054,9 +1034,6 @@ type ConversationAPI = ( Summary "Update conversation name (deprecated)" :> Deprecated :> Description "Use `/conversations/:domain/:conv/name` instead." - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow ('ActionDenied 'ModifyConversationName) :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation @@ -1076,9 +1053,6 @@ type ConversationAPI = ( Summary "Update conversation name (deprecated)" :> Deprecated :> Description "Use `/conversations/:domain/:conv/name` instead." - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow ('ActionDenied 'ModifyConversationName) :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation @@ -1097,9 +1071,6 @@ type ConversationAPI = :<|> Named "update-conversation-name" ( Summary "Update conversation name" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow ('ActionDenied 'ModifyConversationName) :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation @@ -1122,9 +1093,6 @@ type ConversationAPI = ( Summary "Update the message timer for a conversation (deprecated)" :> Deprecated :> Description "Use `/conversations/:domain/:cnv/message-timer` instead." - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'ModifyConversationMessageTimer) @@ -1144,9 +1112,6 @@ type ConversationAPI = :<|> Named "update-conversation-message-timer" ( Summary "Update the message timer for a conversation" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'ModifyConversationMessageTimer) @@ -1170,10 +1135,6 @@ type ConversationAPI = ( Summary "Update receipt mode for a conversation (deprecated)" :> Deprecated :> Description "Use `PUT /conversations/:domain/:cnv/receipt-mode` instead." - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "update-conversation" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'ModifyConversationReceiptMode) @@ -1193,10 +1154,6 @@ type ConversationAPI = :<|> Named "update-conversation-receipt-mode" ( Summary "Update receipt mode for a conversation" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "update-conversation" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'ModifyConversationReceiptMode) @@ -1219,9 +1176,6 @@ type ConversationAPI = :<|> Named "update-conversation-access-unqualified" ( Summary "Update access modes for a conversation (deprecated)" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> Until 'V3 :> Description "Use PUT `/conversations/:domain/:cnv/access` instead." :> ZLocalUser @@ -1245,9 +1199,6 @@ type ConversationAPI = :<|> Named "update-conversation-access@v2" ( Summary "Update access modes for a conversation" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> Until 'V3 :> ZLocalUser :> ZConn @@ -1270,9 +1221,6 @@ type ConversationAPI = :<|> Named "update-conversation-access" ( Summary "Update access modes for a conversation" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> From 'V3 :> ZLocalUser :> ZConn diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/CustomBackend.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/CustomBackend.hs index 607a6e62573..c91dd758fdd 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/CustomBackend.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/CustomBackend.hs @@ -18,7 +18,7 @@ module Wire.API.Routes.Public.Galley.CustomBackend where import Data.Domain -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.CustomBackend import Wire.API.Error 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 654f79657a2..55c6bf0c810 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 @@ -19,14 +19,12 @@ module Wire.API.Routes.Public.Galley.Feature where import Data.Id import GHC.TypeLits -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () -import Wire.API.ApplyMods -import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.MakesFederatedCall import Wire.API.OAuth +import Wire.API.Routes.Features import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public @@ -34,107 +32,84 @@ import Wire.API.Routes.Version import Wire.API.Team.Feature import Wire.API.Team.SearchVisibility (TeamSearchVisibilityView) +type FeatureAPIGetPut cfg = + FeatureAPIGet cfg :<|> FeatureAPIPut cfg + type FeatureAPI = - FeatureStatusGet SSOConfig - :<|> FeatureStatusGet LegalholdConfig - :<|> FeatureStatusPut - '[ MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-mls-message-sent" - ] - '( 'ActionDenied 'RemoveConversationMember, - '( AuthenticationError, - '( 'CannotEnableLegalHoldServiceLargeTeam, - '( 'LegalHoldNotEnabled, - '( 'LegalHoldDisableUnimplemented, - '( 'LegalHoldServiceNotRegistered, - '( 'UserLegalHoldIllegalOperation, - '( 'LegalHoldCouldNotBlockConnections, '()) - ) - ) - ) - ) - ) - ) - ) - LegalholdConfig - :<|> FeatureStatusGet SearchVisibilityAvailableConfig - :<|> FeatureStatusPut '[] '() SearchVisibilityAvailableConfig - :<|> FeatureStatusDeprecatedGet "This endpoint is potentially used by the old Android client. It is not used by iOS, team management, or webapp as of June 2022" SearchVisibilityAvailableConfig - :<|> FeatureStatusDeprecatedPut "This endpoint is potentially used by the old Android client. It is not used by iOS, team management, or webapp as of June 2022" SearchVisibilityAvailableConfig + FeatureAPIGet SSOConfig + :<|> FeatureAPIGetPut LegalholdConfig + :<|> FeatureAPIGetPut SearchVisibilityAvailableConfig :<|> SearchVisibilityGet :<|> SearchVisibilitySet - :<|> FeatureStatusGet ValidateSAMLEmailsConfig - :<|> FeatureStatusDeprecatedGet "This endpoint is potentially used by the old Android client. It is not used by iOS, team management, or webapp as of June 2022" ValidateSAMLEmailsConfig - :<|> FeatureStatusGet DigitalSignaturesConfig - :<|> FeatureStatusDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is potentially used by the old Android client. It is not used by team management, or webapp as of June 2022" DigitalSignaturesConfig - :<|> FeatureStatusGet AppLockConfig - :<|> FeatureStatusPut '[] '() AppLockConfig - :<|> FeatureStatusGet FileSharingConfig - :<|> FeatureStatusPut '[] '() FileSharingConfig - :<|> FeatureStatusGet ClassifiedDomainsConfig - :<|> FeatureStatusGet ConferenceCallingConfig - :<|> FeatureStatusGet SelfDeletingMessagesConfig - :<|> FeatureStatusPut '[] '() SelfDeletingMessagesConfig - :<|> FeatureStatusGet GuestLinksConfig - :<|> FeatureStatusPut '[] '() GuestLinksConfig - :<|> FeatureStatusGet SndFactorPasswordChallengeConfig - :<|> FeatureStatusPut '[] '() SndFactorPasswordChallengeConfig - :<|> From 'V5 ::> FeatureStatusGet MLSConfig - :<|> From 'V5 ::> FeatureStatusPut '[] '() MLSConfig - :<|> FeatureStatusGet ExposeInvitationURLsToTeamAdminConfig - :<|> FeatureStatusPut '[] '() ExposeInvitationURLsToTeamAdminConfig - :<|> FeatureStatusGet SearchVisibilityInboundConfig - :<|> FeatureStatusPut '[] '() SearchVisibilityInboundConfig - :<|> FeatureStatusGet OutlookCalIntegrationConfig - :<|> FeatureStatusPut '[] '() OutlookCalIntegrationConfig - :<|> From 'V5 ::> FeatureStatusGet MlsE2EIdConfig - :<|> From 'V5 ::> Until 'V6 ::> Named "put-MlsE2EIdConfig@v5" (ZUser :> FeatureStatusBasePutPublic '() MlsE2EIdConfig) - :<|> From 'V6 ::> FeatureStatusPut '[] '() MlsE2EIdConfig - :<|> From 'V5 ::> FeatureStatusGet MlsMigrationConfig - :<|> From 'V5 ::> FeatureStatusPut '[] '() MlsMigrationConfig - :<|> From 'V5 - ::> FeatureStatusGetWithDesc - EnforceFileDownloadLocationConfig - "

Custom feature: only supported for some decidated on-prem systems.

" - :<|> From 'V5 - ::> FeatureStatusPutWithDesc - '[] - '() - EnforceFileDownloadLocationConfig - "

Custom feature: only supported for some decidated on-prem systems.

" - :<|> From 'V5 ::> FeatureStatusGet LimitedEventFanoutConfig - :<|> AllFeatureConfigsUserGet - :<|> AllFeatureConfigsTeamGet - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" LegalholdConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" SSOConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" SearchVisibilityAvailableConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" ValidateSAMLEmailsConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" DigitalSignaturesConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" AppLockConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" FileSharingConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" ClassifiedDomainsConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" ConferenceCallingConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" SelfDeletingMessagesConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" GuestLinksConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" SndFactorPasswordChallengeConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" MLSConfig + :<|> FeatureAPIGet ValidateSAMLEmailsConfig + :<|> FeatureAPIGet DigitalSignaturesConfig + :<|> FeatureAPIGetPut AppLockConfig + :<|> FeatureAPIGetPut FileSharingConfig + :<|> FeatureAPIGet ClassifiedDomainsConfig + :<|> FeatureAPIGetPut ConferenceCallingConfig + :<|> FeatureAPIGetPut SelfDeletingMessagesConfig + :<|> FeatureAPIGetPut GuestLinksConfig + :<|> FeatureAPIGetPut SndFactorPasswordChallengeConfig + :<|> From 'V5 ::> FeatureAPIGetPut MLSConfig + :<|> FeatureAPIGetPut ExposeInvitationURLsToTeamAdminConfig + :<|> FeatureAPIGetPut SearchVisibilityInboundConfig + :<|> FeatureAPIGetPut OutlookCalIntegrationConfig + :<|> From 'V5 ::> FeatureAPIGet MlsE2EIdConfig + :<|> From 'V5 ::> Until 'V6 ::> Named "put-MlsE2EIdConfig@v5" (ZUser :> FeatureStatusBasePutPublic MlsE2EIdConfig) + :<|> From 'V6 ::> FeatureAPIPut MlsE2EIdConfig + :<|> From 'V5 ::> FeatureAPIGetPut MlsMigrationConfig + :<|> From 'V5 ::> FeatureAPIGetPut EnforceFileDownloadLocationConfig + :<|> From 'V5 ::> FeatureAPIGet LimitedEventFanoutConfig + :<|> AllTeamFeaturesUserGet + :<|> AllTeamFeaturesTeamGet + :<|> DeprecatedFeatureAPI + :<|> AllDeprecatedFeatureConfigAPI DeprecatedFeatureConfigs + +type DeprecationNotice1 = "This endpoint is potentially used by the old Android client. It is not used by iOS, team management, or webapp as of June 2022" + +type DeprecationNotice2 = "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" + +type DeprecatedFeatureConfigs = + [ LegalholdConfig, + SSOConfig, + SearchVisibilityAvailableConfig, + ValidateSAMLEmailsConfig, + DigitalSignaturesConfig, + AppLockConfig, + FileSharingConfig, + ClassifiedDomainsConfig, + ConferenceCallingConfig, + SelfDeletingMessagesConfig, + GuestLinksConfig, + SndFactorPasswordChallengeConfig, + MLSConfig + ] -type FeatureStatusGet f = FeatureStatusGetWithDesc f "" +type family AllDeprecatedFeatureConfigAPI cfgs where + AllDeprecatedFeatureConfigAPI '[cfg] = FeatureConfigDeprecatedGet DeprecationNotice2 cfg + AllDeprecatedFeatureConfigAPI (cfg : cfgs) = + FeatureConfigDeprecatedGet DeprecationNotice2 cfg + :<|> AllDeprecatedFeatureConfigAPI cfgs -type FeatureStatusGetWithDesc f desc = +type DeprecatedFeatureAPI = + FeatureStatusDeprecatedGet DeprecationNotice1 SearchVisibilityAvailableConfig + :<|> FeatureStatusDeprecatedPut DeprecationNotice1 SearchVisibilityAvailableConfig + :<|> FeatureStatusDeprecatedGet DeprecationNotice1 ValidateSAMLEmailsConfig + :<|> FeatureStatusDeprecatedGet DeprecationNotice2 DigitalSignaturesConfig + +type FeatureAPIGet cfg = Named - '("get", f) - ( Description desc - :> (ZUser :> FeatureStatusBaseGet f) + '("get", cfg) + ( Description (FeatureAPIDesc cfg) + :> (ZUser :> FeatureStatusBaseGet cfg) ) -type FeatureStatusPut segs errs f = FeatureStatusPutWithDesc segs errs f "" - -type FeatureStatusPutWithDesc segs errs f desc = +type FeatureAPIPut cfg = Named - '("put", f) - ( Description desc - :> (ApplyMods segs (ZUser :> FeatureStatusBasePutPublic errs f)) + '("put", cfg) + ( Description (FeatureAPIDesc cfg) + :> ZUser + :> FeatureStatusBasePutPublic cfg ) type FeatureStatusDeprecatedGet d f = @@ -156,21 +131,21 @@ type FeatureStatusBaseGet featureConfig = :> Capture "tid" TeamId :> "features" :> FeatureSymbol featureConfig - :> Get '[Servant.JSON] (WithStatus featureConfig) + :> Get '[Servant.JSON] (LockableFeature featureConfig) -type FeatureStatusBasePutPublic errs featureConfig = +type FeatureStatusBasePutPublic featureConfig = Summary (AppendSymbol "Put config for " (FeatureSymbol featureConfig)) :> CanThrow OperationDenied :> CanThrow 'NotATeamMember :> CanThrow 'TeamNotFound :> CanThrow TeamFeatureError - :> CanThrowMany errs + :> CanThrowMany (FeatureErrors featureConfig) :> "teams" :> Capture "tid" TeamId :> "features" :> FeatureSymbol featureConfig - :> ReqBody '[Servant.JSON] (WithStatusNoLock featureConfig) - :> Put '[Servant.JSON] (WithStatus featureConfig) + :> ReqBody '[Servant.JSON] (Feature featureConfig) + :> Put '[Servant.JSON] (LockableFeature featureConfig) -- | A type for a GET endpoint for a feature with a deprecated path type FeatureStatusBaseDeprecatedGet desc featureConfig = @@ -190,7 +165,7 @@ type FeatureStatusBaseDeprecatedGet desc featureConfig = :> Capture "tid" TeamId :> "features" :> DeprecatedFeatureName featureConfig - :> Get '[Servant.JSON] (WithStatus featureConfig) + :> Get '[Servant.JSON] (LockableFeature featureConfig) ) -- | A type for a PUT endpoint for a feature with a deprecated path @@ -212,8 +187,8 @@ type FeatureStatusBaseDeprecatedPut desc featureConfig = :> Capture "tid" TeamId :> "features" :> DeprecatedFeatureName featureConfig - :> ReqBody '[Servant.JSON] (WithStatusNoLock featureConfig) - :> Put '[Servant.JSON] (WithStatus featureConfig) + :> ReqBody '[Servant.JSON] (Feature featureConfig) + :> Put '[Servant.JSON] (LockableFeature featureConfig) type FeatureConfigDeprecatedGet desc featureConfig = Named @@ -227,10 +202,10 @@ type FeatureConfigDeprecatedGet desc featureConfig = :> CanThrow 'TeamNotFound :> "feature-configs" :> FeatureSymbol featureConfig - :> Get '[Servant.JSON] (WithStatus featureConfig) + :> Get '[Servant.JSON] (LockableFeature featureConfig) ) -type AllFeatureConfigsUserGet = +type AllTeamFeaturesUserGet = Named "get-all-feature-configs-for-user" ( Summary @@ -244,10 +219,10 @@ type AllFeatureConfigsUserGet = :> CanThrow OperationDenied :> CanThrow 'TeamNotFound :> "feature-configs" - :> Get '[Servant.JSON] AllFeatureConfigs + :> Get '[Servant.JSON] AllTeamFeatures ) -type AllFeatureConfigsTeamGet = +type AllTeamFeaturesTeamGet = Named "get-all-feature-configs-for-team" ( Summary "Gets feature configs for a team" @@ -259,7 +234,7 @@ type AllFeatureConfigsTeamGet = :> "teams" :> Capture "tid" TeamId :> "features" - :> Get '[JSON] AllFeatureConfigs + :> Get '[JSON] AllTeamFeatures ) type SearchVisibilityGet = diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs index f04ad6c3e70..c1e2882ffe5 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs @@ -20,12 +20,11 @@ module Wire.API.Routes.Public.Galley.LegalHold where import Data.Id import GHC.Generics import Generics.SOP qualified as GSOP -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.MakesFederatedCall import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public @@ -63,9 +62,6 @@ type LegalHoldAPI = :<|> Named "delete-legal-hold-settings" ( Summary "Delete legal hold service settings" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow AuthenticationError :> CanThrow OperationDenied :> CanThrow 'NotATeamMember @@ -102,9 +98,6 @@ type LegalHoldAPI = :<|> Named "consent-to-legal-hold" ( Summary "Consent to legal hold" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow ('ActionDenied 'RemoveConversationMember) :> CanThrow 'InvalidOperation :> CanThrow 'TeamMemberNotFound @@ -120,9 +113,6 @@ type LegalHoldAPI = :<|> Named "request-legal-hold-device" ( Summary "Request legal hold device" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow ('ActionDenied 'RemoveConversationMember) :> CanThrow 'NotATeamMember :> CanThrow OperationDenied @@ -133,6 +123,7 @@ type LegalHoldAPI = :> CanThrow 'LegalHoldServiceBadResponse :> CanThrow 'LegalHoldServiceNotRegistered :> CanThrow 'LegalHoldCouldNotBlockConnections + :> CanThrow 'MLSLegalholdIncompatible :> CanThrow 'UserLegalHoldIllegalOperation :> Description "This endpoint can lead to the following events being sent:\n\ @@ -151,9 +142,6 @@ type LegalHoldAPI = :<|> Named "disable-legal-hold-for-user" ( Summary "Disable legal hold for user" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow AuthenticationError :> CanThrow ('ActionDenied 'RemoveConversationMember) :> CanThrow 'NotATeamMember @@ -180,9 +168,6 @@ type LegalHoldAPI = :<|> Named "approve-legal-hold-device" ( Summary "Approve legal hold device" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow AuthenticationError :> CanThrow 'AccessDenied :> CanThrow ('ActionDenied 'RemoveConversationMember) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs index 6c53e5e3398..8913227982f 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs @@ -17,7 +17,7 @@ module Wire.API.Routes.Public.Galley.MLS where -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Galley @@ -26,7 +26,6 @@ import Wire.API.MLS.Keys import Wire.API.MLS.Message import Wire.API.MLS.Serialisation import Wire.API.MLS.Servant -import Wire.API.MakesFederatedCall import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public @@ -37,10 +36,6 @@ type MLSMessagingAPI = "mls-message" ( Summary "Post an MLS message" :> From 'V5 - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "send-mls-message" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Brig "get-mls-clients" :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvMemberNotFound :> CanThrow 'ConvNotFound @@ -72,33 +67,27 @@ type MLSMessagingAPI = :<|> Named "mls-commit-bundle" ( Summary "Post a MLS CommitBundle" - :> From 'V5 - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "mls-welcome" - :> MakesFederatedCall 'Galley "send-mls-commit-bundle" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Brig "get-mls-clients" - :> MakesFederatedCall 'Brig "get-users-by-ids" - :> MakesFederatedCall 'Brig "api-version" - :> CanThrow 'ConvAccessDenied - :> CanThrow 'ConvMemberNotFound - :> CanThrow 'ConvNotFound - :> CanThrow 'LegalHoldNotEnabled - :> CanThrow 'MissingLegalholdConsent - :> CanThrow 'MLSClientMismatch - :> CanThrow 'MLSClientSenderUserMismatch - :> CanThrow 'MLSCommitMissingReferences - :> CanThrow 'MLSGroupConversationMismatch - :> CanThrow 'MLSInvalidLeafNodeIndex - :> CanThrow 'MLSNotEnabled - :> CanThrow 'MLSProposalNotFound - :> CanThrow 'MLSProtocolErrorTag - :> CanThrow 'MLSSelfRemovalNotAllowed - :> CanThrow 'MLSStaleMessage - :> CanThrow 'MLSSubConvClientNotInParent - :> CanThrow 'MLSUnsupportedMessage - :> CanThrow 'MLSUnsupportedProposal - :> CanThrow 'MLSWelcomeMismatch + :> From V5 + :> CanThrow ConvAccessDenied + :> CanThrow ConvMemberNotFound + :> CanThrow ConvNotFound + :> CanThrow LegalHoldNotEnabled + :> CanThrow MissingLegalholdConsent + :> CanThrow MLSClientMismatch + :> CanThrow MLSClientSenderUserMismatch + :> CanThrow MLSCommitMissingReferences + :> CanThrow MLSGroupConversationMismatch + :> CanThrow MLSInvalidLeafNodeIndex + :> CanThrow MLSNotEnabled + :> CanThrow MLSProposalNotFound + :> CanThrow MLSProtocolErrorTag + :> CanThrow MLSSelfRemovalNotAllowed + :> CanThrow MLSStaleMessage + :> CanThrow MLSSubConvClientNotInParent + :> CanThrow MLSUnsupportedMessage + :> CanThrow MLSUnsupportedProposal + :> CanThrow MLSWelcomeMismatch + :> CanThrow MLSLegalholdIncompatible :> CanThrow MLSProposalFailure :> CanThrow NonFederatingBackends :> CanThrow UnreachableBackends @@ -107,26 +96,28 @@ type MLSMessagingAPI = :> ZClient :> ZConn :> ReqBody '[MLS] (RawMLS CommitBundle) - :> MultiVerb1 'POST '[JSON] (Respond 201 "Commit accepted and forwarded" MLSMessageSendingStatus) - ) - :<|> Named - "mls-public-keys-v5" - ( Summary "Get public keys used by the backend to sign external proposals" - :> From 'V5 - :> Until 'V6 - :> CanThrow 'MLSNotEnabled - :> "public-keys" - :> ZLocalUser - :> MultiVerb1 'GET '[JSON] (Respond 200 "Public keys" (MLSKeysByPurpose MLSPublicKeys)) + :> MultiVerb1 POST '[JSON] (Respond 201 "Commit accepted and forwarded" MLSMessageSendingStatus) ) :<|> Named "mls-public-keys" ( Summary "Get public keys used by the backend to sign external proposals" - :> From 'V6 + :> Description + "The 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." + :> From 'V5 :> CanThrow 'MLSNotEnabled :> "public-keys" :> ZLocalUser - :> MultiVerb1 'GET '[JSON] (Respond 200 "Public keys" (MLSKeysByPurpose MLSPublicKeysJWK)) + :> QueryParam "format" MLSPublicKeyFormat + :> MultiVerb1 + 'GET + '[JSON] + ( Respond + 200 + "Public keys" + (MLSKeysByPurpose (MLSKeys SomeKey)) + ) ) type MLSAPI = LiftNamed ("mls" :> MLSMessagingAPI) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs index d4b81661b79..5604c3d74f7 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs @@ -23,12 +23,11 @@ import Data.OpenApi qualified as S import Data.SOP import Generics.SOP qualified as GSOP import Imports -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Brig qualified as BrigError import Wire.API.Error.Galley -import Wire.API.MakesFederatedCall import Wire.API.Message import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named @@ -41,8 +40,6 @@ type MessagingAPI = "post-otr-message-unqualified" ( Summary "Post an encrypted message to a conversation (accepts JSON or Protobuf)" :> Description PostOtrDescriptionUnqualified - :> MakesFederatedCall 'Galley "on-message-sent" - :> MakesFederatedCall 'Brig "get-user-clients" :> ZLocalUser :> ZConn :> "conversations" @@ -83,9 +80,6 @@ type MessagingAPI = "post-proteus-message" ( Summary "Post an encrypted message to a conversation (accepts only Protobuf)" :> Description PostOtrDescription - :> MakesFederatedCall 'Brig "get-user-clients" - :> MakesFederatedCall 'Galley "on-message-sent" - :> MakesFederatedCall 'Galley "send-message" :> ZLocalUser :> ZConn :> "conversations" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Team.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Team.hs index fd3fd392a4a..ae0c36aca68 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Team.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Team.hs @@ -17,9 +17,14 @@ module Wire.API.Routes.Public.Galley.Team where +import Control.Lens ((?~)) +import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Id +import Data.OpenApi.Schema qualified as S +import Data.Range +import Data.Schema import Imports -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Galley @@ -28,8 +33,37 @@ import Wire.API.Routes.Named import Wire.API.Routes.Public import Wire.API.Routes.Version import Wire.API.Team +import Wire.API.Team.Member import Wire.API.Team.Permission +-- | FUTUREWORK: remove when the create-non-binding-team endpoint is deleted +data NonBindingNewTeam = NonBindingNewTeam + { teamName :: Range 1 256 Text, + teamIcon :: Icon, + teamIconKey :: Maybe (Range 1 256 Text), + teamMembers :: Maybe (Range 1 127 [TeamMember]) + } + deriving stock (Eq, Show) + deriving (FromJSON, ToJSON, S.ToSchema) via (Schema NonBindingNewTeam) + +instance ToSchema NonBindingNewTeam where + schema = + object "NonBindingNewTeam" $ + NonBindingNewTeam + <$> (.teamName) .= fieldWithDocModifier "name" (description ?~ "team name") schema + <*> (.teamIcon) .= fieldWithDocModifier "icon" (description ?~ "team icon (asset ID)") schema + <*> (.teamIconKey) .= maybe_ (optFieldWithDocModifier "icon_key" (description ?~ "team icon asset key") schema) + <*> (.teamMembers) + .= maybe_ + ( optFieldWithDocModifier + "members" + (description ?~ "initial team member ids (between 1 and 127)") + sch + ) + where + sch :: ValueSchema SwaggerDoc (Range 1 127 [TeamMember]) + sch = fromRange .= rangedSchema (array schema) + type TeamAPI = Named "create-non-binding-team" @@ -37,8 +71,7 @@ type TeamAPI = :> Until 'V4 :> ZUser :> ZConn - :> CanThrow 'NotConnected - :> CanThrow 'UserBindingExists + :> CanThrow InvalidAction :> "teams" :> ReqBody '[Servant.JSON] NonBindingNewTeam :> MultiVerb diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs index 0f45c2ac92c..f77aab90644 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs @@ -18,12 +18,11 @@ module Wire.API.Routes.Public.Galley.TeamConversation where import Data.Id -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.MakesFederatedCall import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public @@ -68,9 +67,6 @@ type TeamConversationAPI = :<|> Named "delete-team-conversation" ( Summary "Remove a team conversation" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow ('ActionDenied 'DeleteConversation) :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs index 4c71df03e49..ef66057baa3 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs @@ -22,7 +22,7 @@ import Data.Int import Data.Range import GHC.Generics import Generics.SOP qualified as GSOP -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Galley 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 e1f92b07998..787da9d22a2 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs @@ -25,7 +25,6 @@ import SAML2.WebSSO qualified as SAML import Servant import Servant.API.Extended import Servant.Multipart -import Servant.OpenApi import URI.ByteString qualified as URI import Web.Scim.Capabilities.MetaSchema as Scim.Meta import Web.Scim.Class.Auth as Scim.Auth @@ -35,7 +34,10 @@ import Wire.API.Error import Wire.API.Error.Brig import Wire.API.Routes.API import Wire.API.Routes.Internal.Spar +import Wire.API.Routes.Named import Wire.API.Routes.Public +import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.API.SwaggerServant import Wire.API.User.IdentityProvider import Wire.API.User.Saml @@ -58,8 +60,8 @@ type DeprecateSSOAPIV1 = \Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams" type APISSO = - DeprecateSSOAPIV1 :> Deprecated :> "metadata" :> SAML.APIMeta - :<|> "metadata" :> Capture "team" TeamId :> SAML.APIMeta + Named "sso-metadata" (DeprecateSSOAPIV1 :> Deprecated :> "metadata" :> SAML.APIMeta) + :<|> Named "sso-team-metadata" ("metadata" :> Capture "team" TeamId :> SAML.APIMeta) :<|> "initiate-login" :> APIAuthReqPrecheck :<|> "initiate-login" :> APIAuthReq :<|> APIAuthRespLegacy @@ -69,40 +71,52 @@ type APISSO = type CheckOK = Verb 'HEAD 200 type APIAuthReqPrecheck = - QueryParam "success_redirect" URI.URI - :> QueryParam "error_redirect" URI.URI - :> Capture "idp" SAML.IdPId - :> CheckOK '[PlainText] NoContent + Named + "auth-req-precheck" + ( QueryParam "success_redirect" URI.URI + :> QueryParam "error_redirect" URI.URI + :> Capture "idp" SAML.IdPId + :> CheckOK '[PlainText] NoContent + ) type APIAuthReq = - QueryParam "success_redirect" URI.URI - :> QueryParam "error_redirect" URI.URI - -- (SAML.APIAuthReq from here on, except for the cookies) - :> Capture "idp" SAML.IdPId - :> Get '[SAML.HTML] (SAML.FormRedirect SAML.AuthnRequest) + Named + "auth-req" + ( QueryParam "success_redirect" URI.URI + :> QueryParam "error_redirect" URI.URI + -- (SAML.APIAuthReq from here on, except for the cookies) + :> Capture "idp" SAML.IdPId + :> Get '[SAML.HTML] (SAML.FormRedirect SAML.AuthnRequest) + ) type APIAuthRespLegacy = - DeprecateSSOAPIV1 - :> Deprecated - :> "finalize-login" - -- (SAML.APIAuthResp from here on, except for response) - :> MultipartForm Mem SAML.AuthnResponseBody - :> Post '[PlainText] Void + Named + "auth-resp-legacy" + ( DeprecateSSOAPIV1 + :> Deprecated + :> "finalize-login" + -- (SAML.APIAuthResp from here on, except for response) + :> MultipartForm Mem SAML.AuthnResponseBody + :> Post '[PlainText] Void + ) type APIAuthResp = - "finalize-login" - :> Capture "team" TeamId - -- (SAML.APIAuthResp from here on, except for response) - :> MultipartForm Mem SAML.AuthnResponseBody - :> Post '[PlainText] Void + Named + "auth-resp" + ( "finalize-login" + :> Capture "team" TeamId + -- (SAML.APIAuthResp from here on, except for response) + :> MultipartForm Mem SAML.AuthnResponseBody + :> Post '[PlainText] Void + ) type APIIDP = - ZOptUser :> IdpGet - :<|> ZOptUser :> IdpGetRaw - :<|> ZOptUser :> IdpGetAll - :<|> ZOptUser :> IdpCreate - :<|> ZOptUser :> IdpUpdate - :<|> ZOptUser :> IdpDelete + Named "idp-get" (ZOptUser :> IdpGet) + :<|> Named "idp-get-raw" (ZOptUser :> IdpGetRaw) + :<|> Named "idp-get-all" (ZOptUser :> IdpGetAll) + :<|> Named "idp-create" (ZOptUser :> IdpCreate) + :<|> Named "idp-update" (ZOptUser :> IdpUpdate) + :<|> Named "idp-delete" (ZOptUser :> IdpDelete) type IdpGetRaw = Capture "id" SAML.IdPId :> "raw" :> Get '[RawXML] RawIdPMetadata @@ -132,7 +146,10 @@ type IdpDelete = :> DeleteNoContent type SsoSettingsGet = - Get '[JSON] SsoSettings + Named + "sso-settings" + ( Get '[JSON] SsoSettings + ) sparSPIssuer :: (Functor m, SAML.HasConfig m) => Maybe TeamId -> m SAML.Issuer sparSPIssuer Nothing = @@ -151,7 +168,6 @@ sparResponseURI (Just tid) = type APIScim = OmitDocs :> "v2" :> ScimSiteAPI SparTag :<|> "auth-tokens" - :> CanThrow 'PasswordAuthenticationFailed :> CanThrow 'CodeAuthenticationFailed :> CanThrow 'CodeAuthenticationRequired :> APIScimToken @@ -173,9 +189,21 @@ data ScimSite tag route = ScimSite deriving (Generic) type APIScimToken = - ZOptUser :> APIScimTokenCreate - :<|> ZOptUser :> APIScimTokenDelete - :<|> ZOptUser :> APIScimTokenList + Named "auth-tokens-create@v6" (Until 'V7 :> ZOptUser :> APIScimTokenCreateV6) + :<|> Named "auth-tokens-create" (From 'V7 :> ZOptUser :> APIScimTokenCreate) + :<|> Named "auth-tokens-put-name" (From 'V7 :> ZUser :> APIScimTokenPutName) + :<|> Named "auth-tokens-delete" (ZOptUser :> APIScimTokenDelete) + :<|> Named "auth-tokens-list@v6" (Until 'V7 :> ZOptUser :> APIScimTokenListV6) + :<|> Named "auth-tokens-list" (From 'V7 :> ZOptUser :> APIScimTokenList) + +type APIScimTokenPutName = + Capture "id" ScimTokenId + :> ReqBody '[JSON] ScimTokenName + :> Put '[JSON] () + +type APIScimTokenCreateV6 = + VersionedReqBody 'V6 '[JSON] CreateScimToken + :> Post '[JSON] CreateScimTokenResponseV6 type APIScimTokenCreate = ReqBody '[JSON] CreateScimToken @@ -188,9 +216,10 @@ type APIScimTokenDelete = type APIScimTokenList = Get '[JSON] ScimTokenList +type APIScimTokenListV6 = + Get '[JSON] ScimTokenListV6 + data SparAPITag instance ServiceAPI SparAPITag v where type ServiceAPIRoutes SparAPITag = SparAPI - type SpecialisedAPIRoutes v SparAPITag = SparAPI - serviceSwagger = toOpenApi (Proxy @SparAPI) diff --git a/libs/wire-api/src/Wire/API/Routes/SpecialiseToVersion.hs b/libs/wire-api/src/Wire/API/Routes/SpecialiseToVersion.hs index a2f29573f43..215e5c78d29 100644 --- a/libs/wire-api/src/Wire/API/Routes/SpecialiseToVersion.hs +++ b/libs/wire-api/src/Wire/API/Routes/SpecialiseToVersion.hs @@ -21,9 +21,10 @@ module Wire.API.Routes.SpecialiseToVersion where import Data.Singletons.Base.TH import GHC.TypeLits import Servant +import Servant.API.Extended import Servant.API.Extended.RawM qualified as RawM +import Servant.Multipart.API import Wire.API.Deprecated -import Wire.API.MakesFederatedCall import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.VersionInfo @@ -46,6 +47,10 @@ type instance SpecialiseToVersion v (Named n api) = Named n (SpecialiseToVersion v api) +type instance + SpecialiseToVersion v (NoContentVerb m) = + NoContentVerb m + type instance SpecialiseToVersion v (Capture' mod sym a :> api) = Capture' mod sym a :> SpecialiseToVersion v api @@ -84,10 +89,6 @@ type instance SpecialiseToVersion v (Description desc :> api) = Description desc :> SpecialiseToVersion v api -type instance - SpecialiseToVersion v (MakesFederatedCall comp rpc :> api) = - MakesFederatedCall comp rpc :> SpecialiseToVersion v api - type instance SpecialiseToVersion v (StreamBody' opts f t x :> api) = StreamBody' opts f t x :> SpecialiseToVersion v api @@ -97,3 +98,11 @@ type instance SpecialiseToVersion v EmptyAPI = EmptyAPI type instance SpecialiseToVersion v (api1 :<|> api2) = SpecialiseToVersion v api1 :<|> SpecialiseToVersion v api2 + +type instance + SpecialiseToVersion v (ReqBodyCustomError t l x :> api) = + ReqBodyCustomError t l x :> SpecialiseToVersion v api + +type instance + SpecialiseToVersion v (MultipartForm x b :> api) = + MultipartForm x b :> SpecialiseToVersion v api diff --git a/libs/wire-api/src/Wire/API/Routes/Version.hs b/libs/wire-api/src/Wire/API/Routes/Version.hs index 2256e54ac69..92e095f360f 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version.hs @@ -44,8 +44,8 @@ module Wire.API.Routes.Version Until, From, - -- * Swagger instances - SpecialiseToVersion, + -- * Swagger + module Wire.API.Routes.SpecialiseToVersion, ) where @@ -65,13 +65,10 @@ import Data.Set qualified as Set import Data.Singletons.Base.TH import Data.Text qualified as Text import Data.Text.Encoding as Text -import GHC.TypeLits import Imports hiding ((\\)) import Servant -import Servant.API.Extended.RawM qualified as RawM -import Wire.API.Deprecated -import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named hiding (unnamed) +import Wire.API.Routes.SpecialiseToVersion import Wire.API.VersionInfo import Wire.Arbitrary (Arbitrary, GenericUniform (GenericUniform)) @@ -83,7 +80,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 +data Version = V0 | V1 | V2 | V3 | V4 | V5 | V6 | V7 deriving stock (Eq, Ord, Bounded, Enum, Show, Generic) deriving (FromJSON, ToJSON) via (Schema Version) deriving (Arbitrary) via (GenericUniform Version) @@ -102,6 +99,7 @@ versionInt V3 = 3 versionInt V4 = 4 versionInt V5 = 5 versionInt V6 = 6 +versionInt V7 = 7 supportedVersions :: [Version] supportedVersions = [minBound .. maxBound] @@ -211,6 +209,7 @@ isDevelopmentVersion V2 = False isDevelopmentVersion V3 = False isDevelopmentVersion V4 = False isDevelopmentVersion V5 = False +isDevelopmentVersion V6 = False isDevelopmentVersion _ = True developmentVersions :: [Version] @@ -249,72 +248,4 @@ expandVersionExp :: VersionExp -> Set Version expandVersionExp (VersionExpConst v) = Set.singleton v expandVersionExp VersionExpDevelopment = Set.fromList developmentVersions --- Version-aware swagger generation - $(promoteOrdInstances [''Version]) - -type family SpecialiseToVersion (v :: Version) api - -type instance - SpecialiseToVersion v (From w :> api) = - If (v < w) EmptyAPI (SpecialiseToVersion v api) - -type instance - SpecialiseToVersion v (Until w :> api) = - If (v < w) (SpecialiseToVersion v api) EmptyAPI - -type instance - SpecialiseToVersion v ((s :: Symbol) :> api) = - s :> SpecialiseToVersion v api - -type instance - SpecialiseToVersion v (Named n api) = - Named n (SpecialiseToVersion v api) - -type instance - SpecialiseToVersion v (Capture' mod sym a :> api) = - Capture' mod sym a :> SpecialiseToVersion v api - -type instance - SpecialiseToVersion v (Summary s :> api) = - Summary s :> SpecialiseToVersion v api - -type instance - SpecialiseToVersion v (Deprecated :> api) = - Deprecated :> SpecialiseToVersion v api - -type instance - SpecialiseToVersion v (Verb m s t r) = - Verb m s t r - -type instance - SpecialiseToVersion v (MultiVerb m t r x) = - MultiVerb m t r x - -type instance SpecialiseToVersion v RawM.RawM = RawM.RawM - -type instance - SpecialiseToVersion v (ReqBody t x :> api) = - ReqBody t x :> SpecialiseToVersion v api - -type instance - SpecialiseToVersion v (QueryParam' mods l x :> api) = - QueryParam' mods l x :> SpecialiseToVersion v api - -type instance - SpecialiseToVersion v (Header' opts l x :> api) = - Header' opts l x :> SpecialiseToVersion v api - -type instance - SpecialiseToVersion v (Description desc :> api) = - Description desc :> SpecialiseToVersion v api - -type instance - SpecialiseToVersion v (StreamBody' opts f t x :> api) = - StreamBody' opts f t x :> SpecialiseToVersion v api - -type instance SpecialiseToVersion v EmptyAPI = EmptyAPI - -type instance - SpecialiseToVersion v (api1 :<|> api2) = - SpecialiseToVersion v api1 :<|> SpecialiseToVersion v api2 diff --git a/libs/wire-api/src/Wire/API/Routes/Version/Wai.hs b/libs/wire-api/src/Wire/API/Routes/Version/Wai.hs index cd797101f11..6174c6e515f 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version/Wai.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version/Wai.hs @@ -44,12 +44,12 @@ versionMiddleware disabledAPIVersions app req k = case parseVersion (removeVersi where err :: Text -> IO ResponseReceived err v = - k . errorRs' . mkError HTTP.status404 "unsupported-version" $ + k . errorRs . mkError HTTP.status404 "unsupported-version" $ "Version " <> fromStrict v <> " is not supported" errint :: IO ResponseReceived errint = - k . errorRs' . mkError HTTP.status404 "unsupported-version" $ + k . errorRs . mkError HTTP.status404 "unsupported-version" $ "Internal APIs (`/i/...`) are not under version control" data ParseVersionError = NoVersion | BadVersion Text | InternalApisAreUnversioned @@ -74,8 +74,8 @@ looksLikeVersion version = case T.splitAt 1 version of (h, t) -> h == "v" && T.a -- | swagger-delivering end-points are not disableable: they should work for all versions. requestIsDisableable :: Request -> Bool requestIsDisableable (pathInfo -> path) = case path of - ["api", "swagger-ui"] -> False - ["api", "swagger.json"] -> False + ("api" : "swagger-ui" : _) -> False + ("api" : "swagger.json" : _) -> False _ -> True removeVersionHeader :: Request -> Request diff --git a/libs/wire-api/src/Wire/API/Routes/Versioned.hs b/libs/wire-api/src/Wire/API/Routes/Versioned.hs index 405ec783e00..640d91fd022 100644 --- a/libs/wire-api/src/Wire/API/Routes/Versioned.hs +++ b/libs/wire-api/src/Wire/API/Routes/Versioned.hs @@ -29,6 +29,7 @@ import Servant import Servant.API.ContentTypes import Servant.OpenApi import Servant.OpenApi.Internal +import Test.QuickCheck (Arbitrary) import Wire.API.Routes.MultiVerb import Wire.API.Routes.Version @@ -89,10 +90,10 @@ instance responseUnrender c = fmap unVersioned . responseUnrender @cs @(Respond s desc (Versioned v a)) c instance - (KnownSymbol desc, S.ToSchema a) => - IsSwaggerResponse (VersionedRespond v s desc a) + (KnownSymbol desc, S.ToSchema a, SingI v, ToSchema (Versioned v a), Typeable v) => + IsSwaggerResponse (VersionedRespond (v :: Version) s desc a) where - responseSwagger = simpleResponseSwagger @a @'[JSON] @desc + responseSwagger = simpleResponseSwagger @(Versioned v a) @'[JSON] @desc ------------------------------------------------------------------------------- -- Versioned newtype wrapper @@ -102,6 +103,7 @@ instance -- Servant. newtype Versioned (v :: Version) a = Versioned {unVersioned :: a} deriving (Eq, Show) + deriving newtype (Arbitrary) instance Functor (Versioned v) where fmap f (Versioned a) = Versioned (f a) diff --git a/libs/wire-api/src/Wire/API/SwaggerServant.hs b/libs/wire-api/src/Wire/API/SwaggerServant.hs index 8ea0729a504..f5ad2081593 100644 --- a/libs/wire-api/src/Wire/API/SwaggerServant.hs +++ b/libs/wire-api/src/Wire/API/SwaggerServant.hs @@ -23,9 +23,8 @@ where import Data.Metrics.Servant import Data.Proxy -import Imports hiding (head) import Servant -import Servant.OpenApi (HasOpenApi (toOpenApi)) +import Wire.API.Routes.Version -- | A type-level tag that lets us omit any branch from Swagger docs. -- @@ -34,9 +33,6 @@ import Servant.OpenApi (HasOpenApi (toOpenApi)) -- it's only justification is laziness. data OmitDocs -instance HasOpenApi (OmitDocs :> a) where - toOpenApi _ = mempty - instance (HasServer api ctx) => HasServer (OmitDocs :> api) ctx where type ServerT (OmitDocs :> api) m = ServerT api m @@ -46,3 +42,7 @@ instance (HasServer api ctx) => HasServer (OmitDocs :> api) ctx where instance (RoutesToPaths api) => RoutesToPaths (OmitDocs :> api) where getRoutes = getRoutes @api + +type instance + SpecialiseToVersion v (OmitDocs :> api) = + EmptyAPI diff --git a/libs/wire-api/src/Wire/API/Team.hs b/libs/wire-api/src/Wire/API/Team.hs index 13c09ab567b..a1fc3c99b8a 100644 --- a/libs/wire-api/src/Wire/API/Team.hs +++ b/libs/wire-api/src/Wire/API/Team.hs @@ -40,15 +40,9 @@ module Wire.API.Team teamListHasMore, -- * NewTeam - BindingNewTeam (..), - bindingNewTeamObjectSchema, - NonBindingNewTeam (..), NewTeam (..), + newTeamObjectSchema, newNewTeam, - newTeamName, - newTeamIcon, - newTeamIconKey, - newTeamMembers, -- * TeamUpdateData TeamUpdateData (..), @@ -84,7 +78,6 @@ import Data.Text.Encoding qualified as T import Imports import Test.QuickCheck.Gen (suchThat) import Wire.API.Asset (AssetKey) -import Wire.API.Team.Member (TeamMember) import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) -------------------------------------------------------------------------------- @@ -177,62 +170,27 @@ instance ToSchema TeamList where -------------------------------------------------------------------------------- -- NewTeam -newtype BindingNewTeam = BindingNewTeam (NewTeam ()) - deriving stock (Eq, Show, Generic) - deriving (ToJSON, FromJSON, S.ToSchema) via (Schema BindingNewTeam) - -instance ToSchema BindingNewTeam where - schema = object "BindingNewTeam" bindingNewTeamObjectSchema - -bindingNewTeamObjectSchema :: ObjectSchema SwaggerDoc BindingNewTeam -bindingNewTeamObjectSchema = - BindingNewTeam <$> unwrap .= newTeamObjectSchema null_ - where - unwrap (BindingNewTeam nt) = nt - --- FUTUREWORK: since new team members do not get serialized, we zero them here. --- it may be worth looking into how this can be solved in the types. -instance Arbitrary BindingNewTeam where - arbitrary = - BindingNewTeam . zeroTeamMembers <$> arbitrary @(NewTeam ()) - where - zeroTeamMembers tms = tms {_newTeamMembers = Nothing} - --- | FUTUREWORK: this is dead code! remove! -newtype NonBindingNewTeam = NonBindingNewTeam (NewTeam (Range 1 127 [TeamMember])) - deriving stock (Eq, Show, Generic) - deriving (FromJSON, ToJSON, S.ToSchema) via (Schema NonBindingNewTeam) - -instance ToSchema NonBindingNewTeam where - schema = - object "NonBindingNewTeam" $ - NonBindingNewTeam - <$> unwrap .= newTeamObjectSchema sch - where - unwrap (NonBindingNewTeam nt) = nt - - sch :: ValueSchema SwaggerDoc (Range 1 127 [TeamMember]) - sch = fromRange .= rangedSchema (array schema) - -data NewTeam a = NewTeam - { _newTeamName :: Range 1 256 Text, - _newTeamIcon :: Icon, - _newTeamIconKey :: Maybe (Range 1 256 Text), - _newTeamMembers :: Maybe a +data NewTeam = NewTeam + { newTeamName :: Range 1 256 Text, + newTeamIcon :: Icon, + newTeamIconKey :: Maybe (Range 1 256 Text) } deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform (NewTeam a)) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema NewTeam) + deriving (Arbitrary) via (GenericUniform NewTeam) -newNewTeam :: Range 1 256 Text -> Icon -> NewTeam a -newNewTeam nme ico = NewTeam nme ico Nothing Nothing - -newTeamObjectSchema :: ValueSchema SwaggerDoc a -> ObjectSchema SwaggerDoc (NewTeam a) -newTeamObjectSchema sch = +newTeamObjectSchema :: ObjectSchema SwaggerDoc NewTeam +newTeamObjectSchema = NewTeam - <$> _newTeamName .= fieldWithDocModifier "name" (description ?~ "team name") schema - <*> _newTeamIcon .= fieldWithDocModifier "icon" (description ?~ "team icon (asset ID)") schema - <*> _newTeamIconKey .= maybe_ (optFieldWithDocModifier "icon_key" (description ?~ "team icon asset key") schema) - <*> _newTeamMembers .= maybe_ (optFieldWithDocModifier "members" (description ?~ "initial team member ids (between 1 and 127)") sch) + <$> newTeamName .= fieldWithDocModifier "name" (description ?~ "team name") schema + <*> newTeamIcon .= fieldWithDocModifier "icon" (description ?~ "team icon (asset ID)") schema + <*> newTeamIconKey .= maybe_ (optFieldWithDocModifier "icon_key" (description ?~ "team icon asset key") schema) + +instance ToSchema NewTeam where + schema = object "NewTeam" newTeamObjectSchema + +newNewTeam :: Range 1 256 Text -> Icon -> NewTeam +newNewTeam nme ico = NewTeam nme ico Nothing -------------------------------------------------------------------------------- -- TeamUpdateData @@ -322,6 +280,5 @@ instance ToSchema TeamDeleteData where makeLenses ''Team makeLenses ''TeamList -makeLenses ''NewTeam makeLenses ''TeamUpdateData makeLenses ''TeamDeleteData diff --git a/libs/wire-api/src/Wire/API/Team/Conversation.hs b/libs/wire-api/src/Wire/API/Team/Conversation.hs index 877ca425df3..d3b240d9f54 100644 --- a/libs/wire-api/src/Wire/API/Team/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Team/Conversation.hs @@ -77,9 +77,7 @@ newTeamConversation = TeamConversation -------------------------------------------------------------------------------- -- TeamConversationList -newtype TeamConversationList = TeamConversationList - { _teamConversations :: [TeamConversation] - } +newtype TeamConversationList = TeamConversationList {teamConversations :: [TeamConversation]} deriving (Generic) deriving stock (Eq, Show) deriving newtype (Arbitrary) @@ -91,10 +89,9 @@ instance ToSchema TeamConversationList where "TeamConversationList" (description ?~ "Team conversation list") $ TeamConversationList - <$> _teamConversations .= field "conversations" (array schema) + <$> teamConversations .= field "conversations" (array schema) newTeamConversationList :: [TeamConversation] -> TeamConversationList newTeamConversationList = TeamConversationList makeLenses ''TeamConversation -makeLenses ''TeamConversationList diff --git a/libs/wire-api/src/Wire/API/Team/Export.hs b/libs/wire-api/src/Wire/API/Team/Export.hs index 636950095bf..c31040c5e42 100644 --- a/libs/wire-api/src/Wire/API/Team/Export.hs +++ b/libs/wire-api/src/Wire/API/Team/Export.hs @@ -17,6 +17,7 @@ module Wire.API.Team.Export (TeamExportUser (..), quoted, unquoted) where +import Data.Aeson qualified as A import Data.Aeson qualified as Aeson import Data.Attoparsec.ByteString.Lazy (parseOnly) import Data.ByteString.Char8 qualified as C @@ -24,22 +25,31 @@ import Data.ByteString.Conversion (FromByteString (..), ToByteString, toByteStri import Data.Csv (DefaultOrdered (..), FromNamedRecord (..), Parser, ToNamedRecord (..), namedRecord, (.:)) import Data.Handle (Handle) import Data.Id (UserId) -import Data.Json.Util (UTCTimeMillis) +import Data.Json.Util (UTCTimeMillis, utcTimeSchema) import Data.Misc (HttpsUrl) +import Data.OpenApi qualified as OpenApi +import Data.Schema +import Data.Text qualified as T +import Data.Text.Encoding qualified as T +import Data.Time.Clock +import Data.Time.Format import Data.Vector (fromList) import Imports -import Test.QuickCheck (Arbitrary) +import Test.QuickCheck import Wire.API.Team.Role (Role) -import Wire.API.User (Name) -import Wire.API.User.Identity (Email) +import Wire.API.User (AccountStatus (..), Name) +import Wire.API.User.Identity (EmailAddress) import Wire.API.User.Profile (ManagedBy) import Wire.API.User.RichInfo (RichInfo) -import Wire.Arbitrary (GenericUniform (GenericUniform)) +import Wire.Arbitrary + +timestampFormat :: String +timestampFormat = "%Y-%m-%d" data TeamExportUser = TeamExportUser { tExportDisplayName :: Name, tExportHandle :: Maybe Handle, - tExportEmail :: Maybe Email, + tExportEmail :: Maybe EmailAddress, tExportRole :: Maybe Role, tExportCreatedOn :: Maybe UTCTimeMillis, tExportInvitedBy :: Maybe Handle, @@ -49,10 +59,33 @@ data TeamExportUser = TeamExportUser tExportSCIMExternalId :: Text, tExportSCIMRichInfo :: Maybe RichInfo, tExportUserId :: UserId, - tExportNumDevices :: Int + tExportNumDevices :: Int, + tExportLastActive :: Maybe UTCTime, + tExportStatus :: Maybe AccountStatus } deriving (Show, Eq, Generic) deriving (Arbitrary) via (GenericUniform TeamExportUser) + deriving (A.ToJSON, A.FromJSON, OpenApi.ToSchema) via (Schema TeamExportUser) + +instance ToSchema TeamExportUser where + schema = + object "TeamExportUser" $ + TeamExportUser + <$> tExportDisplayName .= field "display_name" schema + <*> tExportHandle .= maybe_ (optField "handle" schema) + <*> tExportEmail .= maybe_ (optField "email" schema) + <*> tExportRole .= maybe_ (optField "role" schema) + <*> tExportCreatedOn .= maybe_ (optField "created_on" schema) + <*> tExportInvitedBy .= maybe_ (optField "invited_by" schema) + <*> tExportIdpIssuer .= maybe_ (optField "idp_issuer" schema) + <*> tExportManagedBy .= field "managed_by" schema + <*> tExportSAMLNamedId .= field "saml_name_id" schema + <*> tExportSCIMExternalId .= field "scim_external_id" schema + <*> tExportSCIMRichInfo .= maybe_ (optField "scim_rich_info" schema) + <*> tExportUserId .= field "user_id" schema + <*> tExportNumDevices .= field "num_devices" schema + <*> tExportLastActive .= maybe_ (optField "last_active" utcTimeSchema) + <*> tExportStatus .= maybe_ (optField "status" schema) instance ToNamedRecord TeamExportUser where toNamedRecord row = @@ -69,7 +102,16 @@ instance ToNamedRecord TeamExportUser where ("scim_external_id", secureCsvFieldToByteString (tExportSCIMExternalId row)), ("scim_rich_info", maybe "" (C.toStrict . Aeson.encode) (tExportSCIMRichInfo row)), ("user_id", secureCsvFieldToByteString (tExportUserId row)), - ("num_devices", secureCsvFieldToByteString (tExportNumDevices row)) + ("num_devices", secureCsvFieldToByteString (tExportNumDevices row)), + ( "last_active", + C.pack + ( maybe + "" + (formatTime defaultTimeLocale timestampFormat) + (tExportLastActive row) + ) + ), + ("status", maybe "" formatAccountStatus (tExportStatus row)) ] secureCsvFieldToByteString :: forall a. (ToByteString a) => a -> ByteString @@ -91,7 +133,9 @@ instance DefaultOrdered TeamExportUser where "scim_external_id", "scim_rich_info", "user_id", - "num_devices" + "num_devices", + "last_active", + "status" ] allowEmpty :: (ByteString -> Parser a) -> ByteString -> Parser (Maybe a) @@ -104,6 +148,26 @@ parseByteString bstr = Left err -> fail err Right thing -> pure thing +parseUTCTime :: ByteString -> Parser UTCTime +parseUTCTime b = do + s <- either (fail . displayException) pure $ T.decodeUtf8' b + parseTimeM False defaultTimeLocale timestampFormat (T.unpack s) + +parseAccountStatus :: ByteString -> Parser AccountStatus +parseAccountStatus "active" = pure Active +parseAccountStatus "suspended" = pure Suspended +parseAccountStatus "deleted" = pure Deleted +parseAccountStatus "ephemeral" = pure Ephemeral +parseAccountStatus "pending-invitation" = pure PendingInvitation +parseAccountStatus _ = fail "invalid account status" + +formatAccountStatus :: AccountStatus -> ByteString +formatAccountStatus Active = "active" +formatAccountStatus Suspended = "suspended" +formatAccountStatus Deleted = "deleted" +formatAccountStatus Ephemeral = "ephemeral" +formatAccountStatus PendingInvitation = "pending-invitation" + instance FromNamedRecord TeamExportUser where parseNamedRecord nrec = TeamExportUser @@ -126,6 +190,8 @@ instance FromNamedRecord TeamExportUser where ) <*> (nrec .: "user_id" >>= parseByteString) <*> (nrec .: "num_devices" >>= parseByteString) + <*> (nrec .: "last_active" >>= allowEmpty parseUTCTime) + <*> (nrec .: "status" >>= allowEmpty parseAccountStatus) quoted :: ByteString -> ByteString quoted bs = case C.uncons bs of diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index f2fec9ce3d6..533bbd8837f 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -1,7 +1,7 @@ +{-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE StrictData #-} -{-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} -- This file is part of the Wire Server implementation. -- @@ -25,43 +25,29 @@ module Wire.API.Team.Feature featureName, featureNameBS, LockStatus (..), - WithStatus, - withStatus, - withStatus', - wsStatus, - wsLockStatus, - wsConfig, - wsTTL, - setStatus, - setLockStatus, - setConfig, - setConfig', - setTTL, - setWsTTL, - WithStatusPatch, - wsPatch, - wspStatus, - wspLockStatus, - wspConfig, - wspTTL, - WithStatusNoLock (..), + DbFeature (..), + dbFeatureLockStatus, + dbFeatureStatus, + dbFeatureConfig, + dbFeatureModConfig, + LockableFeature (..), + defUnlockedFeature, + defLockedFeature, + LockableFeaturePatch (..), + Feature (..), forgetLock, withLockStatus, - withUnlocked, FeatureTTL, FeatureTTLDays, FeatureTTL' (..), FeatureTTLUnit (..), - convertFeatureTTLDaysToSeconds, - convertFeatureTTLSecondsToDays, EnforceAppLock (..), - defFeatureStatusNoLock, - computeFeatureConfigForTeamUser, + genericComputeFeature, IsFeatureConfig (..), FeatureSingleton (..), - FeatureTrivialConfig (..), - HasDeprecatedFeatureName (..), + DeprecatedFeatureName, LockStatusResponse (..), + One2OneCalls (..), -- Features LegalholdConfig (..), SSOConfig (..), @@ -83,14 +69,18 @@ module Wire.API.Team.Feature MlsMigrationConfig (..), EnforceFileDownloadLocationConfig (..), LimitedEventFanoutConfig (..), - AllFeatureConfigs (..), - unImplicitLockStatus, - ImplicitLockStatus (..), + Features, + AllFeatures, + NpProject (..), + npProject, + NpUpdate (..), + npUpdate, + AllTeamFeatures, ) where import Cassandra.CQL qualified as Cass -import Control.Lens (makeLenses, (?~)) +import Control.Lens ((?~)) import Data.Aeson qualified as A import Data.Aeson.Types qualified as A import Data.Attoparsec.ByteString qualified as Parser @@ -104,8 +94,10 @@ import Data.Id import Data.Json.Util import Data.Kind import Data.Misc (HttpsUrl) +import Data.Monoid import Data.OpenApi qualified as S import Data.Proxy +import Data.SOP import Data.Schema import Data.Scientific (toBoundedInteger) import Data.Text qualified as T @@ -122,7 +114,7 @@ import Test.QuickCheck.Arbitrary (arbitrary) import Test.QuickCheck.Gen (suchThat) import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite (CipherSuiteTag (MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519)) -import Wire.API.Routes.Named (RenderableSymbol (renderSymbol)) +import Wire.API.Routes.Named import Wire.Arbitrary (Arbitrary, GenericUniform (..)) ---------------------------------------------------------------------- @@ -130,60 +122,72 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -- | Checklist for adding a new feature -- --- 1. Add a data type for your feature's "config" part, naming convention: --- **Config**. If your feature doesn't have a config besides --- being enabled/disabled, locked/unlocked, then the config should be a unit --- type, e.g. **data MyFeatureConfig = MyFeatureConfig**. Add a singleton for --- the new data type. Implement type classes 'RenderableSymbol', 'ToSchema', --- 'IsFeatureConfig' and 'Arbitrary'. If your feature doesn't have a config --- implement 'FeatureTrivialConfig'. +-- Assume we want to add a new feature called @dummy@. Every appearance of +-- @dummy@ or @Dummy@ in the following has to be replaced with the actual name +-- of the feature being added. -- --- 2. Add the config to 'AllFeatureConfigs'. +-- 1. Create a new type in this module for the feature configuration, called +-- @DummyConfig@. If your feature doesn't have a config besides being 'status' +-- and 'lockStatus', then the config should be a unit type, e.g. @data +-- DummyConfig = DummyConfig@. Derive 'Eq', 'Show', 'Generic', 'Arbitrary', +-- 'RenderableSymbol', 'FromJSON', 'ToJSON' and 'S.ToSchema'. Implement a +-- 'ToSchema' instance. Add a singleton. Add the config type to 'Features'. -- --- 3. If your feature is configurable on a per-team basis, add a schema --- migration in galley and extend 'getFeatureStatus' and similar functions in --- Galley.Cassandra.TeamFeatures +-- 2. Create a schema migration in galley, adding a column for each +-- configurable value of the feature. The new columns must contain all the +-- information needed to reconstruct a value of type 'LockableFeature +-- DummyConfig'. -- --- 4. Add the feature to the config schema of galley in Galley.Types.Teams. --- and extend the Arbitrary instance of FeatureConfigs in the unit tests --- Test.Galley.Types +-- 3. In 'Galley.Cassandra.MakeFeature', implement the 'MakeFeature' type +-- class: set 'FeatureRow' to the list of types of the rows added by the +-- migration. If the lock status is configurable (it should be in most cases), +-- it must be the first in the list. Set 'featureColumns' to the names of the +-- columns, in the same order. Implement `rowToFeature` and `featureToRow`. -- --- 5. Implement 'GetFeatureConfig' and 'SetFeatureConfig' in --- Galley.API.Teams.Features which defines the main business logic for getting --- and setting (with side-effects). Note that we don't have to check the --- lockstatus inside 'setConfigForTeam' because the lockstatus is checked in --- 'setFeatureStatus' before which is the public API for setting the feature --- status. +-- 4. Implement 'GetFeatureConfig' and 'SetFeatureConfig' in +-- 'Galley.API.Teams.Features'. Empty instances will work fine unless this +-- feature requires custom logic. -- --- 6. Add public routes to Wire.API.Routes.Public.Galley.Feature: --- 'FeatureStatusGet', 'FeatureStatusPut' (optional). Then implement them in --- Galley.API.Public.Feature. +-- 5. Add a public route to 'Wire.API.Routes.Public.Galley.Feature' and the +-- corresponding implementation in 'Galley.API.Public.Feature'. -- --- 7. Add internal routes in Wire.API.Routes.Internal.Galley and implement them --- in Galley.API.Internal. +-- 6. Add an internal route in 'Wire.API.Routes.Internal.Galley' and the +-- corresponding implementation in 'Galley.API.Internal'. -- --- 8. If the feature should be configurable via Stern add routes to Stern.API. +-- 7. If the feature should be configurable via Stern add routes to Stern.API. -- Manually check that the swagger looks okay and works. -- --- 9. If the feature is configured on a per-user level, see the --- 'ConferenceCallingConfig' as an example. --- (https://github.com/wireapp/wire-server/pull/1811, --- https://github.com/wireapp/wire-server/pull/1818) +-- 8. In 'Galley.Types.Team', add a new data instance @DummyDefaults@ to +-- represent the server-wide feature defaults read from the configuration file. +-- In most cases, this should be a newtype over 'LockableFeature DummyConfig'. +-- Then derive all the instances like for the other features in that module. +-- Note that 'ParseFeatureDefaults' can be derived either via 'OptionalField' +-- or 'RequiredField', depending on whether the feature configuration should be +-- optional or required. -- --- 10. Extend the integration tests with cases. +-- 9. If necessary, add configuration for the feature in +-- 'galley.integration.yaml', update the config map in +-- 'charts/galley/templates/configmap.yaml' and set defaults in +-- 'charts/galley/values.yaml'. Make sure that the configuration for CI matches +-- the local one, or adjust 'hack/helm_vars/wire-server/values.yaml' +-- accordingly. -- --- 11. If applicable, edit/update the configurations: --- - optionally add the config for local integration tests to 'galley.integration.yaml' --- - add a config mapping to 'charts/galley/templates/configmap.yaml' --- - add the defaults to 'charts/galley/values.yaml' --- - optionally add config for CI to 'hack/helm_vars/wire-server/values.yaml' +-- 10. Add the default values of this feature in 'testAllFeatures' +-- ('Test.FeatureFlags'). Add feature-specific integration tests. -- --- 12. Add a section to the documentation at an appropriate place +-- 11. Add a section to the documentation at an appropriate place -- (e.g. 'docs/src/developer/reference/config-options.md' (if applicable) or -- 'docs/src/understand/team-feature-settings.md') -class IsFeatureConfig cfg where +class + ( Default cfg, + ToSchema cfg, + Default (LockableFeature cfg), + KnownSymbol (FeatureSymbol cfg), + NpProject cfg Features + ) => + IsFeatureConfig cfg + where type FeatureSymbol cfg :: Symbol - defFeatureStatus :: WithStatus cfg featureSingleton :: FeatureSingleton cfg objectSchema :: @@ -210,194 +214,169 @@ data FeatureSingleton cfg where FeatureSingletonExposeInvitationURLsToTeamAdminConfig :: FeatureSingleton ExposeInvitationURLsToTeamAdminConfig FeatureSingletonOutlookCalIntegrationConfig :: FeatureSingleton OutlookCalIntegrationConfig FeatureSingletonMlsE2EIdConfig :: FeatureSingleton MlsE2EIdConfig - FeatureSingletonMlsMigration :: - -- FUTUREWORK: rename to `FeatureSingletonMlsMigrationConfig` (or drop the `Config` from - -- all other constructors) - FeatureSingleton MlsMigrationConfig + FeatureSingletonMlsMigrationConfig :: FeatureSingleton MlsMigrationConfig FeatureSingletonEnforceFileDownloadLocationConfig :: FeatureSingleton EnforceFileDownloadLocationConfig FeatureSingletonLimitedEventFanoutConfig :: FeatureSingleton LimitedEventFanoutConfig -class FeatureTrivialConfig cfg where - trivialConfig :: cfg - -class HasDeprecatedFeatureName cfg where - type DeprecatedFeatureName cfg :: Symbol +type family DeprecatedFeatureName cfg :: Symbol -featureName :: forall cfg. (KnownSymbol (FeatureSymbol cfg)) => Text +featureName :: forall cfg. (IsFeatureConfig cfg) => Text featureName = T.pack $ symbolVal (Proxy @(FeatureSymbol cfg)) -featureNameBS :: forall cfg. (KnownSymbol (FeatureSymbol cfg)) => ByteString +featureNameBS :: forall cfg. (IsFeatureConfig cfg) => ByteString featureNameBS = UTF8.fromString $ symbolVal (Proxy @(FeatureSymbol cfg)) ----------------------------------------------------------------------- --- WithStatusBase - -data WithStatusBase (m :: Type -> Type) (cfg :: Type) = WithStatusBase - { wsbStatus :: m FeatureStatus, - wsbLockStatus :: m LockStatus, - wsbConfig :: m cfg, - wsbTTL :: m FeatureTTL - } - deriving stock (Generic, Typeable, Functor) - ----------------------------------------------------------------------- --- WithStatus - --- FUTUREWORK: use lenses, maybe? -wsStatus :: WithStatus cfg -> FeatureStatus -wsStatus = runIdentity . wsbStatus - -wsLockStatus :: WithStatus cfg -> LockStatus -wsLockStatus = runIdentity . wsbLockStatus - -wsConfig :: WithStatus cfg -> cfg -wsConfig = runIdentity . wsbConfig - -wsTTL :: WithStatus cfg -> FeatureTTL -wsTTL = runIdentity . wsbTTL - -withStatus :: FeatureStatus -> LockStatus -> cfg -> FeatureTTL -> WithStatus cfg -withStatus s ls c ttl = WithStatusBase (Identity s) (Identity ls) (Identity c) (Identity ttl) - -setStatus :: FeatureStatus -> WithStatus cfg -> WithStatus cfg -setStatus s (WithStatusBase _ ls c ttl) = WithStatusBase (Identity s) ls c ttl - -setLockStatus :: LockStatus -> WithStatus cfg -> WithStatus cfg -setLockStatus ls (WithStatusBase s _ c ttl) = WithStatusBase s (Identity ls) c ttl - -setConfig :: cfg -> WithStatus cfg -> WithStatus cfg -setConfig = setConfig' - -setConfig' :: forall (m :: Type -> Type) (cfg :: Type). (Applicative m) => cfg -> WithStatusBase m cfg -> WithStatusBase m cfg -setConfig' c (WithStatusBase s ls _ ttl) = WithStatusBase s ls (pure c) ttl - -setTTL :: forall (m :: Type -> Type) (cfg :: Type). (Applicative m) => FeatureTTL -> WithStatusBase m cfg -> WithStatusBase m cfg -setTTL ttl (WithStatusBase s ls c _) = WithStatusBase s ls c (pure ttl) +-------------------------------------------------------------------------------- +-- DbFeature -setWsTTL :: FeatureTTL -> WithStatus cfg -> WithStatus cfg -setWsTTL = setTTL +-- | Feature data stored in the database, as a function of its default values. +newtype DbFeature cfg = DbFeature + {applyDbFeature :: LockableFeature cfg -> LockableFeature cfg} + deriving (Semigroup, Monoid) via Endo (LockableFeature cfg) -type WithStatus (cfg :: Type) = WithStatusBase Identity cfg +dbFeatureLockStatus :: LockStatus -> DbFeature cfg +dbFeatureLockStatus s = DbFeature $ \w -> w {lockStatus = s} -deriving instance (Eq cfg) => Eq (WithStatus cfg) +dbFeatureStatus :: FeatureStatus -> DbFeature cfg +dbFeatureStatus s = DbFeature $ \w -> w {status = s} -deriving instance (Show cfg) => Show (WithStatus cfg) +dbFeatureConfig :: cfg -> DbFeature cfg +dbFeatureConfig c = DbFeature $ \w -> w {config = c} -deriving via (Schema (WithStatus cfg)) instance (ToSchema (WithStatus cfg)) => ToJSON (WithStatus cfg) +dbFeatureModConfig :: (cfg -> cfg) -> DbFeature cfg +dbFeatureModConfig f = DbFeature $ \w -> w {config = f w.config} -deriving via (Schema (WithStatus cfg)) instance (ToSchema (WithStatus cfg)) => FromJSON (WithStatus cfg) +---------------------------------------------------------------------- +-- LockableFeature -deriving via (Schema (WithStatus cfg)) instance (ToSchema (WithStatus cfg), Typeable cfg) => S.ToSchema (WithStatus cfg) +-- [Note: unsettable features] +-- +-- Some feature flags (e.g. sso) don't have a lock status stored in the +-- database. Instead, they are considered unlocked by default, but behave as if +-- they were locked, since they lack a public PUT endpoint. +-- +-- This trick has caused a lot of confusion in the past, and cannot be extended +-- to flags that have non-trivial configuration. For this reason, we are in the +-- process of changing this mechanism to make it work like every other feature. +-- +-- That means that such features will afterwards be toggled by setting their +-- lock status instead. And we'll have some logic in place to make the default +-- status when unlocked be enabled. This achieves a similar behaviour but with +-- fewer exceptional code paths. +-- +-- See the implementation of 'computeFeature' for 'ConferenceCallingConfig' for +-- an example of this mechanism in practice. -instance (ToSchema cfg, IsFeatureConfig cfg) => ToSchema (WithStatus cfg) where +data LockableFeature cfg = LockableFeature + { status :: FeatureStatus, + lockStatus :: LockStatus, + config :: cfg + } + deriving stock (Eq, Show) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema (LockableFeature cfg) + +instance (Default (LockableFeature cfg)) => Default (Feature cfg) where + def = forgetLock def + +-- | A feature that is disabled and locked. +defLockedFeature :: (Default cfg) => LockableFeature cfg +defLockedFeature = + LockableFeature + { status = FeatureStatusDisabled, + lockStatus = LockStatusLocked, + config = def + } + +-- | A feature that is enabled and unlocked. +defUnlockedFeature :: (Default cfg) => LockableFeature cfg +defUnlockedFeature = + LockableFeature + { status = FeatureStatusEnabled, + lockStatus = LockStatusUnlocked, + config = def + } + +instance (IsFeatureConfig cfg) => ToSchema (LockableFeature cfg) where schema = object name $ - WithStatusBase - <$> (runIdentity . wsbStatus) .= (Identity <$> field "status" schema) - <*> (runIdentity . wsbLockStatus) .= (Identity <$> field "lockStatus" schema) - <*> (runIdentity . wsbConfig) .= (Identity <$> objectSchema @cfg) - <*> (runIdentity . wsbTTL) .= (Identity . fromMaybe FeatureTTLUnlimited <$> optField "ttl" schema) + LockableFeature + <$> (.status) .= field "status" schema + <*> (.lockStatus) .= field "lockStatus" schema + <*> (.config) .= objectSchema @cfg + <* const FeatureTTLUnlimited + .= optField + "ttl" + (schema :: ValueSchema NamedSwaggerDoc FeatureTTL) where inner = schema @cfg - name = fromMaybe "" (getName (schemaDoc inner)) <> ".WithStatus" + name = fromMaybe "" (getName (schemaDoc inner)) <> ".LockableFeature" -instance (Arbitrary cfg, IsFeatureConfig cfg) => Arbitrary (WithStatus cfg) where - arbitrary = WithStatusBase <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary +instance (Arbitrary cfg, IsFeatureConfig cfg) => Arbitrary (LockableFeature cfg) where + arbitrary = LockableFeature <$> arbitrary <*> arbitrary <*> arbitrary ---------------------------------------------------------------------- --- WithStatusPatch - -type WithStatusPatch (cfg :: Type) = WithStatusBase Maybe cfg - -deriving instance (Eq cfg) => Eq (WithStatusPatch cfg) - -deriving instance (Show cfg) => Show (WithStatusPatch cfg) - -deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch cfg)) => ToJSON (WithStatusPatch cfg) - -deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch cfg)) => FromJSON (WithStatusPatch cfg) - -deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch cfg), Typeable cfg) => S.ToSchema (WithStatusPatch cfg) - -wsPatch :: Maybe FeatureStatus -> Maybe LockStatus -> Maybe cfg -> Maybe FeatureTTL -> WithStatusPatch cfg -wsPatch = WithStatusBase - -wspStatus :: WithStatusPatch cfg -> Maybe FeatureStatus -wspStatus = wsbStatus +-- LockableFeaturePatch -wspLockStatus :: WithStatusPatch cfg -> Maybe LockStatus -wspLockStatus = wsbLockStatus - -wspConfig :: WithStatusPatch cfg -> Maybe cfg -wspConfig = wsbConfig - -wspTTL :: WithStatusPatch cfg -> Maybe FeatureTTL -wspTTL = wsbTTL - -withStatus' :: Maybe FeatureStatus -> Maybe LockStatus -> Maybe cfg -> Maybe FeatureTTL -> WithStatusPatch cfg -withStatus' = WithStatusBase +data LockableFeaturePatch (cfg :: Type) = LockableFeaturePatch + { status :: Maybe FeatureStatus, + lockStatus :: Maybe LockStatus, + config :: Maybe cfg + } + deriving stock (Eq, Show) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema (LockableFeaturePatch cfg)) --- | The ToJSON implementation of `WithStatusPatch` will encode the trivial config as `"config": {}` +-- | The ToJSON implementation of `LockableFeaturePatch` will encode the trivial config as `"config": {}` -- when the value is a `Just`, if it's `Nothing` it will be omitted, which is the important part. -instance (ToSchema cfg) => ToSchema (WithStatusPatch cfg) where +instance (ToSchema cfg) => ToSchema (LockableFeaturePatch cfg) where schema = object name $ - WithStatusBase - <$> wsbStatus .= maybe_ (optField "status" schema) - <*> wsbLockStatus .= maybe_ (optField "lockStatus" schema) - <*> wsbConfig .= maybe_ (optField "config" schema) - <*> wsbTTL .= maybe_ (optField "ttl" schema) + LockableFeaturePatch + <$> (.status) .= maybe_ (optField "status" schema) + <*> (.lockStatus) .= maybe_ (optField "lockStatus" schema) + <*> (.config) .= maybe_ (optField "config" schema) + <* const FeatureTTLUnlimited + .= optField + "ttl" + (schema :: ValueSchema NamedSwaggerDoc FeatureTTL) where inner = schema @cfg - name = fromMaybe "" (getName (schemaDoc inner)) <> ".WithStatusPatch" + name = fromMaybe "" (getName (schemaDoc inner)) <> ".LockableFeaturePatch" -instance (Arbitrary cfg, IsFeatureConfig cfg) => Arbitrary (WithStatusPatch cfg) where - arbitrary = WithStatusBase <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary +instance (Arbitrary cfg, IsFeatureConfig cfg) => Arbitrary (LockableFeaturePatch cfg) where + arbitrary = LockableFeaturePatch <$> arbitrary <*> arbitrary <*> arbitrary ---------------------------------------------------------------------- --- WithStatusNoLock - --- FUTUREWORK(fisx): remove this type. we want all features to have fields `lockStatus` and --- `status`, and we want them to have the same semantics everywhere. currently we have --- eg. conf calling, which was introduced before `lockStatus`, and where `status` means --- `lockStatus`. TTL always refers to `lockStatus`, not `status`. In order to keep current --- (desired) behavior, consider eg. conf calling: let's only allow setting `lockStatus`, but --- if we switch to `unlocked`, we auto-enable the feature, and if we switch to locked, we --- auto-disable it. But we need to change the API to force clients to use `lockStatus` --- instead of `status`, current behavior is just wrong. -data WithStatusNoLock (cfg :: Type) = WithStatusNoLock - { wssStatus :: FeatureStatus, - wssConfig :: cfg, - wssTTL :: FeatureTTL +-- Feature + +data Feature (cfg :: Type) = Feature + { status :: FeatureStatus, + config :: cfg } deriving stock (Eq, Show, Generic, Typeable, Functor) - deriving (ToJSON, FromJSON, S.ToSchema) via (Schema (WithStatusNoLock cfg)) - -instance (Arbitrary cfg) => Arbitrary (WithStatusNoLock cfg) where - arbitrary = WithStatusNoLock <$> arbitrary <*> arbitrary <*> arbitrary + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema (Feature cfg)) -forgetLock :: WithStatus a -> WithStatusNoLock a -forgetLock ws = WithStatusNoLock (wsStatus ws) (wsConfig ws) (wsTTL ws) +instance (Arbitrary cfg) => Arbitrary (Feature cfg) where + arbitrary = Feature <$> arbitrary <*> arbitrary -withLockStatus :: LockStatus -> WithStatusNoLock a -> WithStatus a -withLockStatus ls (WithStatusNoLock s c ttl) = withStatus s ls c ttl +forgetLock :: LockableFeature a -> Feature a +forgetLock ws = Feature ws.status ws.config -withUnlocked :: WithStatusNoLock a -> WithStatus a -withUnlocked = withLockStatus LockStatusUnlocked +withLockStatus :: LockStatus -> Feature a -> LockableFeature a +withLockStatus ls (Feature s c) = LockableFeature s ls c -withLocked :: WithStatusNoLock a -> WithStatus a -withLocked = withLockStatus LockStatusLocked - -instance (ToSchema cfg, IsFeatureConfig cfg) => ToSchema (WithStatusNoLock cfg) where +instance (ToSchema cfg, IsFeatureConfig cfg) => ToSchema (Feature cfg) where schema = object name $ - WithStatusNoLock - <$> wssStatus .= field "status" schema - <*> wssConfig .= objectSchema @cfg - <*> wssTTL .= (fromMaybe FeatureTTLUnlimited <$> optField "ttl" schema) + Feature + <$> (.status) .= field "status" schema + <*> (.config) .= objectSchema @cfg + <* const FeatureTTLUnlimited + .= optField + "ttl" + (schema :: ValueSchema NamedSwaggerDoc FeatureTTL) where inner = schema @cfg - name = fromMaybe "" (getName (schemaDoc inner)) <> ".WithStatusNoLock" + name = fromMaybe "" (getName (schemaDoc inner)) <> ".Feature" ---------------------------------------------------------------------- -- FeatureTTL @@ -417,14 +396,6 @@ type FeatureTTL = FeatureTTL' 'FeatureTTLUnitSeconds type FeatureTTLDays = FeatureTTL' 'FeatureTTLUnitDays -convertFeatureTTLDaysToSeconds :: FeatureTTLDays -> FeatureTTL -convertFeatureTTLDaysToSeconds FeatureTTLUnlimited = FeatureTTLUnlimited -convertFeatureTTLDaysToSeconds (FeatureTTLSeconds d) = FeatureTTLSeconds (d * (60 * 60 * 24)) - -convertFeatureTTLSecondsToDays :: FeatureTTL -> FeatureTTLDays -convertFeatureTTLSecondsToDays FeatureTTLUnlimited = FeatureTTLUnlimited -convertFeatureTTLSecondsToDays (FeatureTTLSeconds d) = FeatureTTLSeconds (d `div` (60 * 60 * 24)) - instance Arbitrary FeatureTTL where arbitrary = (nonZero <$> arbitrary) @@ -561,27 +532,15 @@ instance ToSchema LockStatusResponse where LockStatusResponse <$> _unlockStatus .= field "lockStatus" schema -newtype ImplicitLockStatus (cfg :: Type) = ImplicitLockStatus {_unImplicitLockStatus :: WithStatus cfg} - deriving newtype (Eq, Show, Arbitrary) - -instance (IsFeatureConfig a, ToSchema a) => ToJSON (ImplicitLockStatus a) where - toJSON (ImplicitLockStatus a) = A.toJSON $ forgetLock a - -instance (IsFeatureConfig a, ToSchema a) => FromJSON (ImplicitLockStatus a) where - parseJSON v = ImplicitLockStatus . withLockStatus (wsLockStatus $ defFeatureStatus @a) <$> A.parseJSON v - --- | This contains the pure business logic for users from teams -computeFeatureConfigForTeamUser :: Maybe (WithStatusNoLock cfg) -> Maybe LockStatus -> WithStatus cfg -> WithStatus cfg -computeFeatureConfigForTeamUser mStatusDb mLockStatusDb defStatus = - case lockStatus of - LockStatusLocked -> - withLocked (forgetLock defStatus) - LockStatusUnlocked -> - withUnlocked $ case mStatusDb of - Nothing -> forgetLock defStatus - Just fs -> fs - where - lockStatus = fromMaybe (wsLockStatus defStatus) mLockStatusDb +-- | Convert a feature coming from the database to its public form. This can be +-- overridden on a feature basis by implementing the `computeFeature` method of +-- the `GetFeatureConfig` class. +genericComputeFeature :: forall cfg. LockableFeature cfg -> DbFeature cfg -> LockableFeature cfg +genericComputeFeature defFeature dbFeature = + let feat = applyDbFeature dbFeature defFeature + in case feat.lockStatus of + LockStatusLocked -> defFeature {lockStatus = LockStatusLocked} + LockStatusUnlocked -> feat -------------------------------------------------------------------------------- -- GuestLinks feature @@ -589,67 +548,68 @@ computeFeatureConfigForTeamUser mStatusDb mLockStatusDb defStatus = data GuestLinksConfig = GuestLinksConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform GuestLinksConfig) + deriving (RenderableSymbol) via (RenderableTypeName GuestLinksConfig) -instance RenderableSymbol GuestLinksConfig where - renderSymbol = "GuestLinksConfig" +instance Default GuestLinksConfig where + def = GuestLinksConfig instance ToSchema GuestLinksConfig where schema = object "GuestLinksConfig" objectSchema +instance Default (LockableFeature GuestLinksConfig) where + def = defUnlockedFeature + instance IsFeatureConfig GuestLinksConfig where type FeatureSymbol GuestLinksConfig = "conversationGuestLinks" - defFeatureStatus = withStatus FeatureStatusEnabled LockStatusUnlocked GuestLinksConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonGuestLinksConfig objectSchema = pure GuestLinksConfig -instance FeatureTrivialConfig GuestLinksConfig where - trivialConfig = GuestLinksConfig - -------------------------------------------------------------------------------- -- Legalhold feature data LegalholdConfig = LegalholdConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform LegalholdConfig) + deriving (RenderableSymbol) via (RenderableTypeName LegalholdConfig) -instance RenderableSymbol LegalholdConfig where - renderSymbol = "LegalholdConfig" +instance Default LegalholdConfig where + def = LegalholdConfig + +instance Default (LockableFeature LegalholdConfig) where + def = defUnlockedFeature {status = FeatureStatusDisabled} instance IsFeatureConfig LegalholdConfig where type FeatureSymbol LegalholdConfig = "legalhold" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusUnlocked LegalholdConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonLegalholdConfig objectSchema = pure LegalholdConfig instance ToSchema LegalholdConfig where schema = object "LegalholdConfig" objectSchema -instance FeatureTrivialConfig LegalholdConfig where - trivialConfig = LegalholdConfig - -------------------------------------------------------------------------------- -- SSO feature +-- | This feature does not have a PUT endpoint. See [Note: unsettable features]. data SSOConfig = SSOConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform SSOConfig) + deriving (RenderableSymbol) via (RenderableTypeName SSOConfig) -instance RenderableSymbol SSOConfig where - renderSymbol = "SSOConfig" +instance Default SSOConfig where + def = SSOConfig + +instance Default (LockableFeature SSOConfig) where + def = defUnlockedFeature {status = FeatureStatusDisabled} instance IsFeatureConfig SSOConfig where type FeatureSymbol SSOConfig = "sso" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusUnlocked SSOConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonSSOConfig objectSchema = pure SSOConfig instance ToSchema SSOConfig where schema = object "SSOConfig" objectSchema -instance FeatureTrivialConfig SSOConfig where - trivialConfig = SSOConfig - -------------------------------------------------------------------------------- -- SearchVisibility available feature @@ -658,96 +618,126 @@ instance FeatureTrivialConfig SSOConfig where data SearchVisibilityAvailableConfig = SearchVisibilityAvailableConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform SearchVisibilityAvailableConfig) + deriving (RenderableSymbol) via (RenderableTypeName SearchVisibilityAvailableConfig) -instance RenderableSymbol SearchVisibilityAvailableConfig where - renderSymbol = "SearchVisibilityAvailableConfig" +instance Default SearchVisibilityAvailableConfig where + def = SearchVisibilityAvailableConfig + +instance Default (LockableFeature SearchVisibilityAvailableConfig) where + def = defUnlockedFeature {status = FeatureStatusDisabled} instance IsFeatureConfig SearchVisibilityAvailableConfig where type FeatureSymbol SearchVisibilityAvailableConfig = "searchVisibility" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusUnlocked SearchVisibilityAvailableConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonSearchVisibilityAvailableConfig objectSchema = pure SearchVisibilityAvailableConfig instance ToSchema SearchVisibilityAvailableConfig where schema = object "SearchVisibilityAvailableConfig" objectSchema -instance FeatureTrivialConfig SearchVisibilityAvailableConfig where - trivialConfig = SearchVisibilityAvailableConfig - -instance HasDeprecatedFeatureName SearchVisibilityAvailableConfig where - type DeprecatedFeatureName SearchVisibilityAvailableConfig = "search-visibility" +type instance DeprecatedFeatureName SearchVisibilityAvailableConfig = "search-visibility" -------------------------------------------------------------------------------- -- ValidateSAMLEmails feature +-- | This feature does not have a PUT endpoint. See [Note: unsettable features]. data ValidateSAMLEmailsConfig = ValidateSAMLEmailsConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ValidateSAMLEmailsConfig) + deriving (RenderableSymbol) via (RenderableTypeName ValidateSAMLEmailsConfig) -instance RenderableSymbol ValidateSAMLEmailsConfig where - renderSymbol = "ValidateSAMLEmailsConfig" +instance Default ValidateSAMLEmailsConfig where + def = ValidateSAMLEmailsConfig instance ToSchema ValidateSAMLEmailsConfig where schema = object "ValidateSAMLEmailsConfig" objectSchema +instance Default (LockableFeature ValidateSAMLEmailsConfig) where + def = defUnlockedFeature + instance IsFeatureConfig ValidateSAMLEmailsConfig where type FeatureSymbol ValidateSAMLEmailsConfig = "validateSAMLemails" - defFeatureStatus = withStatus FeatureStatusEnabled LockStatusUnlocked ValidateSAMLEmailsConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonValidateSAMLEmailsConfig objectSchema = pure ValidateSAMLEmailsConfig -instance HasDeprecatedFeatureName ValidateSAMLEmailsConfig where - type DeprecatedFeatureName ValidateSAMLEmailsConfig = "validate-saml-emails" - -instance FeatureTrivialConfig ValidateSAMLEmailsConfig where - trivialConfig = ValidateSAMLEmailsConfig +type instance DeprecatedFeatureName ValidateSAMLEmailsConfig = "validate-saml-emails" -------------------------------------------------------------------------------- -- DigitalSignatures feature +-- | This feature does not have a PUT endpoint. See [Note: unsettable features]. data DigitalSignaturesConfig = DigitalSignaturesConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform DigitalSignaturesConfig) + deriving (RenderableSymbol) via (RenderableTypeName DigitalSignaturesConfig) -instance RenderableSymbol DigitalSignaturesConfig where - renderSymbol = "DigitalSignaturesConfig" +instance Default DigitalSignaturesConfig where + def = DigitalSignaturesConfig + +instance Default (LockableFeature DigitalSignaturesConfig) where + def = defUnlockedFeature {status = FeatureStatusDisabled} instance IsFeatureConfig DigitalSignaturesConfig where type FeatureSymbol DigitalSignaturesConfig = "digitalSignatures" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusUnlocked DigitalSignaturesConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonDigitalSignaturesConfig objectSchema = pure DigitalSignaturesConfig -instance HasDeprecatedFeatureName DigitalSignaturesConfig where - type DeprecatedFeatureName DigitalSignaturesConfig = "digital-signatures" +type instance DeprecatedFeatureName DigitalSignaturesConfig = "digital-signatures" instance ToSchema DigitalSignaturesConfig where schema = object "DigitalSignaturesConfig" objectSchema -instance FeatureTrivialConfig DigitalSignaturesConfig where - trivialConfig = DigitalSignaturesConfig - -------------------------------------------------------------------------------- -- ConferenceCalling feature +data One2OneCalls = One2OneCallsTurn | One2OneCallsSft + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform One2OneCalls) + +one2OneCallsFromUseSftFlag :: Bool -> One2OneCalls +one2OneCallsFromUseSftFlag False = One2OneCallsTurn +one2OneCallsFromUseSftFlag True = One2OneCallsSft + +instance Default One2OneCalls where + def = One2OneCallsTurn + +instance Cass.Cql One2OneCalls where + ctype = Cass.Tagged Cass.IntColumn + + fromCql (Cass.CqlInt n) = case n of + 0 -> pure One2OneCallsTurn + 1 -> pure One2OneCallsSft + _ -> Left "fromCql: Invalid One2OneCalls" + fromCql _ = Left "fromCql: One2OneCalls: CqlInt expected" + + toCql One2OneCallsTurn = Cass.CqlInt 0 + toCql One2OneCallsSft = Cass.CqlInt 1 + data ConferenceCallingConfig = ConferenceCallingConfig + { one2OneCalls :: One2OneCalls + } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ConferenceCallingConfig) + deriving (RenderableSymbol) via (RenderableTypeName ConferenceCallingConfig) -instance RenderableSymbol ConferenceCallingConfig where - renderSymbol = "ConferenceCallingConfig" +instance Default ConferenceCallingConfig where + def = ConferenceCallingConfig {one2OneCalls = def} + +instance Default (LockableFeature ConferenceCallingConfig) where + def = defLockedFeature {status = FeatureStatusEnabled} instance IsFeatureConfig ConferenceCallingConfig where type FeatureSymbol ConferenceCallingConfig = "conferenceCalling" - defFeatureStatus = withStatus FeatureStatusEnabled LockStatusUnlocked ConferenceCallingConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonConferenceCallingConfig - objectSchema = pure ConferenceCallingConfig + objectSchema = fromMaybe def <$> optField "config" schema instance ToSchema ConferenceCallingConfig where - schema = object "ConferenceCallingConfig" objectSchema - -instance FeatureTrivialConfig ConferenceCallingConfig where - trivialConfig = ConferenceCallingConfig + schema = + object "ConferenceCallingConfig" $ + ConferenceCallingConfig + <$> ((== One2OneCallsSft) . one2OneCalls) + .= ( maybe def one2OneCallsFromUseSftFlag + <$> optField "useSFTForOneToOneCalls" schema + ) -------------------------------------------------------------------------------- -- SndFactorPasswordChallenge feature @@ -755,22 +745,22 @@ instance FeatureTrivialConfig ConferenceCallingConfig where data SndFactorPasswordChallengeConfig = SndFactorPasswordChallengeConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform SndFactorPasswordChallengeConfig) + deriving (RenderableSymbol) via (RenderableTypeName SndFactorPasswordChallengeConfig) -instance RenderableSymbol SndFactorPasswordChallengeConfig where - renderSymbol = "SndFactorPasswordChallengeConfig" +instance Default SndFactorPasswordChallengeConfig where + def = SndFactorPasswordChallengeConfig instance ToSchema SndFactorPasswordChallengeConfig where schema = object "SndFactorPasswordChallengeConfig" objectSchema +instance Default (LockableFeature SndFactorPasswordChallengeConfig) where + def = defLockedFeature + instance IsFeatureConfig SndFactorPasswordChallengeConfig where type FeatureSymbol SndFactorPasswordChallengeConfig = "sndFactorPasswordChallenge" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusLocked SndFactorPasswordChallengeConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonSndFactorPasswordChallengeConfig objectSchema = pure SndFactorPasswordChallengeConfig -instance FeatureTrivialConfig SndFactorPasswordChallengeConfig where - trivialConfig = SndFactorPasswordChallengeConfig - -------------------------------------------------------------------------------- -- SearchVisibilityInbound feature @@ -778,33 +768,37 @@ data SearchVisibilityInboundConfig = SearchVisibilityInboundConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform SearchVisibilityInboundConfig) deriving (S.ToSchema) via Schema SearchVisibilityInboundConfig + deriving (RenderableSymbol) via (RenderableTypeName SearchVisibilityInboundConfig) -instance RenderableSymbol SearchVisibilityInboundConfig where - renderSymbol = "SearchVisibilityInboundConfig" +instance Default SearchVisibilityInboundConfig where + def = SearchVisibilityInboundConfig + +instance Default (LockableFeature SearchVisibilityInboundConfig) where + def = defUnlockedFeature {status = FeatureStatusDisabled} instance IsFeatureConfig SearchVisibilityInboundConfig where type FeatureSymbol SearchVisibilityInboundConfig = "searchVisibilityInbound" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusUnlocked SearchVisibilityInboundConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonSearchVisibilityInboundConfig objectSchema = pure SearchVisibilityInboundConfig instance ToSchema SearchVisibilityInboundConfig where schema = object "SearchVisibilityInboundConfig" objectSchema -instance FeatureTrivialConfig SearchVisibilityInboundConfig where - trivialConfig = SearchVisibilityInboundConfig - ---------------------------------------------------------------------- -- ClassifiedDomains feature +-- | This feature is quite special, in that it does not have any database +-- state. Its value cannot be updated dynamically, and is always set to the +-- server default taken from the backend configuration. data ClassifiedDomainsConfig = ClassifiedDomainsConfig { classifiedDomainsDomains :: [Domain] } deriving stock (Show, Eq, Generic) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema ClassifiedDomainsConfig) + deriving (RenderableSymbol) via (RenderableTypeName ClassifiedDomainsConfig) -instance RenderableSymbol ClassifiedDomainsConfig where - renderSymbol = "ClassifiedDomainsConfig" +instance Default ClassifiedDomainsConfig where + def = ClassifiedDomainsConfig [] deriving via (GenericUniform ClassifiedDomainsConfig) instance Arbitrary ClassifiedDomainsConfig @@ -814,15 +808,12 @@ instance ToSchema ClassifiedDomainsConfig where ClassifiedDomainsConfig <$> classifiedDomainsDomains .= field "domains" (array schema) +instance Default (LockableFeature ClassifiedDomainsConfig) where + def = defUnlockedFeature {status = FeatureStatusDisabled} + instance IsFeatureConfig ClassifiedDomainsConfig where type FeatureSymbol ClassifiedDomainsConfig = "classifiedDomains" - defFeatureStatus = - withStatus - FeatureStatusDisabled - LockStatusUnlocked - (ClassifiedDomainsConfig []) - FeatureTTLUnlimited featureSingleton = FeatureSingletonClassifiedDomainsConfig objectSchema = field "config" schema @@ -836,9 +827,10 @@ data AppLockConfig = AppLockConfig deriving stock (Eq, Show, Generic) deriving (FromJSON, ToJSON, S.ToSchema) via (Schema AppLockConfig) deriving (Arbitrary) via (GenericUniform AppLockConfig) + deriving (RenderableSymbol) via (RenderableTypeName AppLockConfig) -instance RenderableSymbol AppLockConfig where - renderSymbol = "AppLockConfig" +instance Default AppLockConfig where + def = AppLockConfig (EnforceAppLock False) 60 instance ToSchema AppLockConfig where schema = @@ -847,15 +839,12 @@ instance ToSchema AppLockConfig where <$> applockEnforceAppLock .= field "enforceAppLock" schema <*> applockInactivityTimeoutSecs .= field "inactivityTimeoutSecs" schema +instance Default (LockableFeature AppLockConfig) where + def = defUnlockedFeature + instance IsFeatureConfig AppLockConfig where type FeatureSymbol AppLockConfig = "appLock" - defFeatureStatus = - withStatus - FeatureStatusEnabled - LockStatusUnlocked - (AppLockConfig (EnforceAppLock False) 60) - FeatureTTLUnlimited featureSingleton = FeatureSingletonAppLockConfig objectSchema = field "config" schema @@ -873,22 +862,22 @@ instance ToSchema EnforceAppLock where data FileSharingConfig = FileSharingConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform FileSharingConfig) + deriving (RenderableSymbol) via (RenderableTypeName FileSharingConfig) -instance RenderableSymbol FileSharingConfig where - renderSymbol = "FileSharingConfig" +instance Default FileSharingConfig where + def = FileSharingConfig + +instance Default (LockableFeature FileSharingConfig) where + def = defUnlockedFeature instance IsFeatureConfig FileSharingConfig where type FeatureSymbol FileSharingConfig = "fileSharing" - defFeatureStatus = withStatus FeatureStatusEnabled LockStatusUnlocked FileSharingConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonFileSharingConfig objectSchema = pure FileSharingConfig instance ToSchema FileSharingConfig where schema = object "FileSharingConfig" objectSchema -instance FeatureTrivialConfig FileSharingConfig where - trivialConfig = FileSharingConfig - ---------------------------------------------------------------------- -- SelfDeletingMessagesConfig @@ -898,9 +887,10 @@ newtype SelfDeletingMessagesConfig = SelfDeletingMessagesConfig deriving stock (Eq, Show, Generic) deriving (FromJSON, ToJSON, S.ToSchema) via (Schema SelfDeletingMessagesConfig) deriving (Arbitrary) via (GenericUniform SelfDeletingMessagesConfig) + deriving (RenderableSymbol) via (RenderableTypeName SelfDeletingMessagesConfig) -instance RenderableSymbol SelfDeletingMessagesConfig where - renderSymbol = "SelfDeletingMessagesConfig" +instance Default SelfDeletingMessagesConfig where + def = SelfDeletingMessagesConfig 0 instance ToSchema SelfDeletingMessagesConfig where schema = @@ -908,14 +898,11 @@ instance ToSchema SelfDeletingMessagesConfig where SelfDeletingMessagesConfig <$> sdmEnforcedTimeoutSeconds .= field "enforcedTimeoutSeconds" schema +instance Default (LockableFeature SelfDeletingMessagesConfig) where + def = defUnlockedFeature + instance IsFeatureConfig SelfDeletingMessagesConfig where type FeatureSymbol SelfDeletingMessagesConfig = "selfDeletingMessages" - defFeatureStatus = - withStatus - FeatureStatusEnabled - LockStatusUnlocked - (SelfDeletingMessagesConfig 0) - FeatureTTLUnlimited featureSingleton = FeatureSingletonSelfDeletingMessagesConfig objectSchema = field "config" schema @@ -931,9 +918,16 @@ data MLSConfig = MLSConfig } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform MLSConfig) + deriving (RenderableSymbol) via (RenderableTypeName MLSConfig) -instance RenderableSymbol MLSConfig where - renderSymbol = "MLSConfig" +instance Default MLSConfig where + def = + MLSConfig + [] + ProtocolProteusTag + [MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519] + MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + [ProtocolProteusTag, ProtocolMLSTag] instance ToSchema MLSConfig where schema = @@ -945,17 +939,11 @@ instance ToSchema MLSConfig where <*> mlsDefaultCipherSuite .= field "defaultCipherSuite" schema <*> mlsSupportedProtocols .= field "supportedProtocols" (array schema) +instance Default (LockableFeature MLSConfig) where + def = defUnlockedFeature {status = FeatureStatusDisabled} + instance IsFeatureConfig MLSConfig where type FeatureSymbol MLSConfig = "mls" - defFeatureStatus = - let config = - MLSConfig - [] - ProtocolProteusTag - [MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519] - MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 - [ProtocolProteusTag, ProtocolMLSTag] - in withStatus FeatureStatusDisabled LockStatusUnlocked config FeatureTTLUnlimited featureSingleton = FeatureSingletonMLSConfig objectSchema = field "config" schema @@ -965,22 +953,22 @@ instance IsFeatureConfig MLSConfig where data ExposeInvitationURLsToTeamAdminConfig = ExposeInvitationURLsToTeamAdminConfig deriving stock (Show, Eq, Generic) deriving (Arbitrary) via (GenericUniform ExposeInvitationURLsToTeamAdminConfig) + deriving (RenderableSymbol) via (RenderableTypeName ExposeInvitationURLsToTeamAdminConfig) + +instance Default ExposeInvitationURLsToTeamAdminConfig where + def = ExposeInvitationURLsToTeamAdminConfig -instance RenderableSymbol ExposeInvitationURLsToTeamAdminConfig where - renderSymbol = "ExposeInvitationURLsToTeamAdminConfig" +instance Default (LockableFeature ExposeInvitationURLsToTeamAdminConfig) where + def = defLockedFeature instance IsFeatureConfig ExposeInvitationURLsToTeamAdminConfig where type FeatureSymbol ExposeInvitationURLsToTeamAdminConfig = "exposeInvitationURLsToTeamAdmin" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusLocked ExposeInvitationURLsToTeamAdminConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonExposeInvitationURLsToTeamAdminConfig objectSchema = pure ExposeInvitationURLsToTeamAdminConfig instance ToSchema ExposeInvitationURLsToTeamAdminConfig where schema = object "ExposeInvitationURLsToTeamAdminConfig" objectSchema -instance FeatureTrivialConfig ExposeInvitationURLsToTeamAdminConfig where - trivialConfig = ExposeInvitationURLsToTeamAdminConfig - ---------------------------------------------------------------------- -- OutlookCalIntegrationConfig @@ -989,22 +977,22 @@ instance FeatureTrivialConfig ExposeInvitationURLsToTeamAdminConfig where data OutlookCalIntegrationConfig = OutlookCalIntegrationConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform OutlookCalIntegrationConfig) + deriving (RenderableSymbol) via (RenderableTypeName OutlookCalIntegrationConfig) + +instance Default OutlookCalIntegrationConfig where + def = OutlookCalIntegrationConfig -instance RenderableSymbol OutlookCalIntegrationConfig where - renderSymbol = "OutlookCalIntegrationConfig" +instance Default (LockableFeature OutlookCalIntegrationConfig) where + def = defLockedFeature instance IsFeatureConfig OutlookCalIntegrationConfig where type FeatureSymbol OutlookCalIntegrationConfig = "outlookCalIntegration" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusLocked OutlookCalIntegrationConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonOutlookCalIntegrationConfig objectSchema = pure OutlookCalIntegrationConfig instance ToSchema OutlookCalIntegrationConfig where schema = object "OutlookCalIntegrationConfig" objectSchema -instance FeatureTrivialConfig OutlookCalIntegrationConfig where - trivialConfig = OutlookCalIntegrationConfig - ---------------------------------------------------------------------- -- MlsE2EId @@ -1015,9 +1003,10 @@ data MlsE2EIdConfig = MlsE2EIdConfig useProxyOnMobile :: Bool } deriving stock (Eq, Show, Generic) + deriving (RenderableSymbol) via (RenderableTypeName MlsE2EIdConfig) -instance RenderableSymbol MlsE2EIdConfig where - renderSymbol = "MlsE2EIdConfig" +instance Default MlsE2EIdConfig where + def = MlsE2EIdConfig (fromIntegral @Int (60 * 60 * 24)) Nothing Nothing False instance Arbitrary MlsE2EIdConfig where arbitrary = @@ -1048,7 +1037,7 @@ instance ToSchema MlsE2EIdConfig where 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 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 \ @@ -1058,11 +1047,11 @@ instance ToSchema MlsE2EIdConfig where \this team. It is of the form \"https://acme.{backendDomain}/acme/{provisionerName}/discovery\". For example: \ \`https://acme.example.com/acme/provisioner1/discovery`." +instance Default (LockableFeature MlsE2EIdConfig) where + def = defLockedFeature + instance IsFeatureConfig MlsE2EIdConfig where type FeatureSymbol MlsE2EIdConfig = "mlsE2EId" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusUnlocked defValue FeatureTTLUnlimited - where - defValue = MlsE2EIdConfig (fromIntegral @Int (60 * 60 * 24)) Nothing Nothing False featureSingleton = FeatureSingletonMlsE2EIdConfig objectSchema = field "config" schema @@ -1074,9 +1063,10 @@ data MlsMigrationConfig = MlsMigrationConfig finaliseRegardlessAfter :: Maybe UTCTime } deriving stock (Eq, Show, Generic) + deriving (RenderableSymbol) via (RenderableTypeName MlsMigrationConfig) -instance RenderableSymbol MlsMigrationConfig where - renderSymbol = "MlsMigrationConfig" +instance Default MlsMigrationConfig where + def = MlsMigrationConfig Nothing Nothing instance Arbitrary MlsMigrationConfig where arbitrary = do @@ -1095,12 +1085,12 @@ instance ToSchema MlsMigrationConfig where <$> startTime .= maybe_ (optField "startTime" utcTimeSchema) <*> finaliseRegardlessAfter .= maybe_ (optField "finaliseRegardlessAfter" utcTimeSchema) +instance Default (LockableFeature MlsMigrationConfig) where + def = defLockedFeature + instance IsFeatureConfig MlsMigrationConfig where type FeatureSymbol MlsMigrationConfig = "mlsMigration" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusLocked defValue FeatureTTLUnlimited - where - defValue = MlsMigrationConfig Nothing Nothing - featureSingleton = FeatureSingletonMlsMigration + featureSingleton = FeatureSingletonMlsMigrationConfig objectSchema = field "config" schema ---------------------------------------------------------------------- @@ -1110,9 +1100,10 @@ data EnforceFileDownloadLocationConfig = EnforceFileDownloadLocationConfig { enforcedDownloadLocation :: Maybe Text } deriving stock (Eq, Show, Generic) + deriving (RenderableSymbol) via (RenderableTypeName EnforceFileDownloadLocationConfig) -instance RenderableSymbol EnforceFileDownloadLocationConfig where - renderSymbol = "EnforceFileDownloadLocationConfig" +instance Default EnforceFileDownloadLocationConfig where + def = EnforceFileDownloadLocationConfig Nothing instance Arbitrary EnforceFileDownloadLocationConfig where arbitrary = EnforceFileDownloadLocationConfig . fmap (T.pack . getPrintableString) <$> arbitrary @@ -1123,9 +1114,11 @@ instance ToSchema EnforceFileDownloadLocationConfig where EnforceFileDownloadLocationConfig <$> enforcedDownloadLocation .= maybe_ (optField "enforcedDownloadLocation" schema) +instance Default (LockableFeature EnforceFileDownloadLocationConfig) where + def = defLockedFeature + instance IsFeatureConfig EnforceFileDownloadLocationConfig where type FeatureSymbol EnforceFileDownloadLocationConfig = "enforceFileDownloadLocation" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusLocked (EnforceFileDownloadLocationConfig Nothing) FeatureTTLUnlimited featureSingleton = FeatureSingletonEnforceFileDownloadLocationConfig objectSchema = field "config" schema @@ -1136,25 +1129,26 @@ instance IsFeatureConfig EnforceFileDownloadLocationConfig where -- months of its introduction, namely once all clients get a chance to adapt to -- a limited event fanout. +-- | This feature does not have a PUT endpoint. See [Note: unsettable features]. data LimitedEventFanoutConfig = LimitedEventFanoutConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform LimitedEventFanoutConfig) + deriving (RenderableSymbol) via (RenderableTypeName LimitedEventFanoutConfig) -instance RenderableSymbol LimitedEventFanoutConfig where - renderSymbol = "LimitedEventFanoutConfig" +instance Default LimitedEventFanoutConfig where + def = LimitedEventFanoutConfig + +instance Default (LockableFeature LimitedEventFanoutConfig) where + def = defUnlockedFeature instance IsFeatureConfig LimitedEventFanoutConfig where type FeatureSymbol LimitedEventFanoutConfig = "limitedEventFanout" - defFeatureStatus = withStatus FeatureStatusEnabled LockStatusUnlocked LimitedEventFanoutConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonLimitedEventFanoutConfig objectSchema = pure LimitedEventFanoutConfig instance ToSchema LimitedEventFanoutConfig where schema = object "LimitedEventFanoutConfig" objectSchema -instance FeatureTrivialConfig LimitedEventFanoutConfig where - trivialConfig = LimitedEventFanoutConfig - ---------------------------------------------------------------------- -- FeatureStatus @@ -1215,112 +1209,109 @@ instance Cass.Cql FeatureStatus where toCql FeatureStatusDisabled = Cass.CqlInt 0 toCql FeatureStatusEnabled = Cass.CqlInt 1 -defFeatureStatusNoLock :: (IsFeatureConfig cfg) => WithStatusNoLock cfg -defFeatureStatusNoLock = forgetLock defFeatureStatus - -data AllFeatureConfigs = AllFeatureConfigs - { afcLegalholdStatus :: WithStatus LegalholdConfig, - afcSSOStatus :: WithStatus SSOConfig, - afcTeamSearchVisibilityAvailable :: WithStatus SearchVisibilityAvailableConfig, - afcSearchVisibilityInboundConfig :: WithStatus SearchVisibilityInboundConfig, - afcValidateSAMLEmails :: WithStatus ValidateSAMLEmailsConfig, - afcDigitalSignatures :: WithStatus DigitalSignaturesConfig, - afcAppLock :: WithStatus AppLockConfig, - afcFileSharing :: WithStatus FileSharingConfig, - afcClassifiedDomains :: WithStatus ClassifiedDomainsConfig, - afcConferenceCalling :: WithStatus ConferenceCallingConfig, - afcSelfDeletingMessages :: WithStatus SelfDeletingMessagesConfig, - afcGuestLink :: WithStatus GuestLinksConfig, - afcSndFactorPasswordChallenge :: WithStatus SndFactorPasswordChallengeConfig, - afcMLS :: WithStatus MLSConfig, - afcExposeInvitationURLsToTeamAdmin :: WithStatus ExposeInvitationURLsToTeamAdminConfig, - afcOutlookCalIntegration :: WithStatus OutlookCalIntegrationConfig, - afcMlsE2EId :: WithStatus MlsE2EIdConfig, - afcMlsMigration :: WithStatus MlsMigrationConfig, - afcEnforceFileDownloadLocation :: WithStatus EnforceFileDownloadLocationConfig, - afcLimitedEventFanout :: WithStatus LimitedEventFanoutConfig - } - deriving stock (Eq, Show) - deriving (FromJSON, ToJSON, S.ToSchema) via (Schema AllFeatureConfigs) - -instance Default AllFeatureConfigs where - def = - AllFeatureConfigs - { afcLegalholdStatus = defFeatureStatus, - afcSSOStatus = defFeatureStatus, - afcTeamSearchVisibilityAvailable = defFeatureStatus, - afcSearchVisibilityInboundConfig = defFeatureStatus, - afcValidateSAMLEmails = defFeatureStatus, - afcDigitalSignatures = defFeatureStatus, - afcAppLock = defFeatureStatus, - afcFileSharing = defFeatureStatus, - afcClassifiedDomains = defFeatureStatus, - afcConferenceCalling = defFeatureStatus, - afcSelfDeletingMessages = defFeatureStatus, - afcGuestLink = defFeatureStatus, - afcSndFactorPasswordChallenge = defFeatureStatus, - afcMLS = defFeatureStatus, - afcExposeInvitationURLsToTeamAdmin = defFeatureStatus, - afcOutlookCalIntegration = defFeatureStatus, - afcMlsE2EId = defFeatureStatus, - afcMlsMigration = defFeatureStatus, - afcEnforceFileDownloadLocation = defFeatureStatus, - afcLimitedEventFanout = defFeatureStatus - } - -instance ToSchema AllFeatureConfigs where +-- | list of available features config types +type Features :: [Type] +type Features = + [ LegalholdConfig, + SSOConfig, + SearchVisibilityAvailableConfig, + SearchVisibilityInboundConfig, + ValidateSAMLEmailsConfig, + DigitalSignaturesConfig, + AppLockConfig, + FileSharingConfig, + ClassifiedDomainsConfig, + ConferenceCallingConfig, + SelfDeletingMessagesConfig, + GuestLinksConfig, + SndFactorPasswordChallengeConfig, + MLSConfig, + ExposeInvitationURLsToTeamAdminConfig, + OutlookCalIntegrationConfig, + MlsE2EIdConfig, + MlsMigrationConfig, + EnforceFileDownloadLocationConfig, + LimitedEventFanoutConfig + ] + +-- | list of available features as a record +type AllFeatures f = NP f Features + +-- | 'AllFeatures' specialised to the 'LockableFeature' functor +type AllTeamFeatures = AllFeatures LockableFeature + +class (Default (LockableFeature cfg)) => LockableFeatureDefault cfg + +instance (Default (LockableFeature cfg)) => LockableFeatureDefault cfg + +instance Default AllTeamFeatures where + def = hcpure (Proxy @LockableFeatureDefault) def + +-- | object schema for nary products +class HObjectSchema c xs where + hobjectSchema :: (forall cfg. (c cfg) => ObjectSchema SwaggerDoc (f cfg)) -> ObjectSchema SwaggerDoc (NP f xs) + +instance HObjectSchema c '[] where + hobjectSchema _ = pure Nil + +instance (HObjectSchema c xs, c x) => HObjectSchema c ((x :: Type) : xs) where + hobjectSchema f = (:*) <$> hd .= f <*> tl .= hobjectSchema @c @xs f + +-- | constraint synonym for 'ToSchema' 'AllTeamFeatures' +class (IsFeatureConfig cfg, ToSchema cfg) => FeatureFieldConstraints cfg + +instance (IsFeatureConfig cfg, ToSchema cfg) => FeatureFieldConstraints cfg + +instance ToSchema AllTeamFeatures where schema = - object "AllFeatureConfigs" $ - AllFeatureConfigs - <$> afcLegalholdStatus .= featureField - <*> afcSSOStatus .= featureField - <*> afcTeamSearchVisibilityAvailable .= featureField - <*> afcSearchVisibilityInboundConfig .= featureField - <*> afcValidateSAMLEmails .= featureField - <*> afcDigitalSignatures .= featureField - <*> afcAppLock .= featureField - <*> afcFileSharing .= featureField - <*> afcClassifiedDomains .= featureField - <*> afcConferenceCalling .= featureField - <*> afcSelfDeletingMessages .= featureField - <*> afcGuestLink .= featureField - <*> afcSndFactorPasswordChallenge .= featureField - <*> afcMLS .= featureField - <*> afcExposeInvitationURLsToTeamAdmin .= featureField - <*> afcOutlookCalIntegration .= featureField - <*> afcMlsE2EId .= featureField - <*> afcMlsMigration .= featureField - <*> afcEnforceFileDownloadLocation .= featureField - <*> afcLimitedEventFanout .= featureField + object "AllTeamFeatures" $ hobjectSchema @FeatureFieldConstraints featureField where - featureField :: - forall cfg. - (IsFeatureConfig cfg, ToSchema cfg, KnownSymbol (FeatureSymbol cfg)) => - ObjectSchema SwaggerDoc (WithStatus cfg) + featureField :: forall cfg. (FeatureFieldConstraints cfg) => ObjectSchema SwaggerDoc (LockableFeature cfg) featureField = field (T.pack (symbolVal (Proxy @(FeatureSymbol cfg)))) schema -instance Arbitrary AllFeatureConfigs where - arbitrary = - AllFeatureConfigs - <$> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary +class (Arbitrary cfg, IsFeatureConfig cfg) => ArbitraryFeatureConfig cfg + +instance (Arbitrary cfg, IsFeatureConfig cfg) => ArbitraryFeatureConfig cfg + +instance Arbitrary AllTeamFeatures where + arbitrary = hsequence' $ hcpure (Proxy @ArbitraryFeatureConfig) (Comp arbitrary) + +-- | FUTUREWORK: 'NpProject' and 'NpUpdate' can be useful for more than +-- features. Maybe they should be moved somewhere else. +class NpProject x xs where + npProject' :: Proxy x -> NP f xs -> f x + +instance {-# OVERLAPPING #-} NpProject x (x : xs) where + npProject' _ (x :* _) = x + +instance (NpProject x xs) => NpProject x (y : xs) where + npProject' p (_ :* xs) = npProject' p xs + +instance (TypeError ('ShowType x :<>: 'Text " not found")) => NpProject x '[] where + npProject' = error "npProject': someone naughty removed the type error constraint" + +-- | Get the first field of a given type out of an @'NP' f xs@. +npProject :: forall x f xs. (NpProject x xs) => NP f xs -> f x +npProject = npProject' (Proxy @x) + +class NpUpdate x xs where + npUpdate' :: Proxy x -> f x -> NP f xs -> NP f xs + +instance {-# OVERLAPPING #-} NpUpdate x (x : xs) where + npUpdate' _ x (_ :* xs) = x :* xs + +instance (NpUpdate x xs) => NpUpdate x (y : xs) where + npUpdate' p x (y :* xs) = y :* npUpdate' p x xs + +instance (TypeError ('ShowType x :<>: 'Text " not found")) => NpUpdate x '[] where + npUpdate' = error "npUpdate': someone naughty removed the type error constraint" + +-- | Update the first field of a given type in an @'NP' f xs@. +npUpdate :: forall x f xs. (NpUpdate x xs) => f x -> NP f xs -> NP f xs +npUpdate = npUpdate' (Proxy @x) + +deriving via (Schema AllTeamFeatures) instance (FromJSON AllTeamFeatures) + +deriving via (Schema AllTeamFeatures) instance (ToJSON AllTeamFeatures) -makeLenses ''ImplicitLockStatus +deriving via (Schema AllTeamFeatures) instance (S.ToSchema AllTeamFeatures) diff --git a/libs/wire-api/src/Wire/API/Team/Invitation.hs b/libs/wire-api/src/Wire/API/Team/Invitation.hs index c51492dc19c..49fe051705a 100644 --- a/libs/wire-api/src/Wire/API/Team/Invitation.hs +++ b/libs/wire-api/src/Wire/API/Team/Invitation.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE StrictData #-} -- This file is part of the Wire Server implementation. @@ -22,6 +23,7 @@ module Wire.API.Team.Invitation Invitation (..), InvitationList (..), InvitationLocation (..), + AcceptTeamInvitation (..), HeadInvitationByEmailResult (..), HeadInvitationsResponses, ) @@ -32,6 +34,7 @@ import Data.Aeson qualified as A import Data.ByteString.Conversion import Data.Id import Data.Json.Util +import Data.Misc import Data.OpenApi qualified as S import Data.SOP import Data.Schema @@ -41,22 +44,19 @@ import Servant (FromHttpApiData (..), ToHttpApiData (..)) import URI.ByteString import Wire.API.Error import Wire.API.Error.Brig -import Wire.API.Locale (Locale) import Wire.API.Routes.MultiVerb import Wire.API.Team.Role (Role, defaultRole) -import Wire.API.User.Identity (Email, Phone) -import Wire.API.User.Profile (Name) +import Wire.API.User import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -------------------------------------------------------------------------------- -- InvitationRequest data InvitationRequest = InvitationRequest - { irLocale :: Maybe Locale, - irRole :: Maybe Role, - irInviteeName :: Maybe Name, - irInviteeEmail :: Email, - irInviteePhone :: Maybe Phone + { locale :: Maybe Locale, + role :: Maybe Role, + inviteeName :: Maybe Name, + inviteeEmail :: EmailAddress } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform InvitationRequest) @@ -66,32 +66,29 @@ instance ToSchema InvitationRequest where schema = objectWithDocModifier "InvitationRequest" (description ?~ "A request to join a team on Wire.") $ InvitationRequest - <$> irLocale + <$> locale .= optFieldWithDocModifier "locale" (description ?~ "Locale to use for the invitation.") (maybeWithDefault A.Null schema) - <*> irRole + <*> (.role) .= optFieldWithDocModifier "role" (description ?~ "Role of the invitee (invited user).") (maybeWithDefault A.Null schema) - <*> irInviteeName + <*> (.inviteeName) .= optFieldWithDocModifier "name" (description ?~ "Name of the invitee (1 - 128 characters).") (maybeWithDefault A.Null schema) - <*> irInviteeEmail + <*> (.inviteeEmail) .= fieldWithDocModifier "email" (description ?~ "Email of the invitee.") schema - <*> irInviteePhone - .= optFieldWithDocModifier "phone" (description ?~ "Phone number of the invitee, in the E.164 format.") (maybeWithDefault A.Null schema) -------------------------------------------------------------------------------- -- Invitation data Invitation = Invitation - { inTeam :: TeamId, - inRole :: Role, - inInvitation :: InvitationId, - inCreatedAt :: UTCTimeMillis, + { team :: TeamId, + role :: Role, + invitationId :: InvitationId, + createdAt :: UTCTimeMillis, -- | this is always 'Just' for new invitations, but for -- migration it is allowed to be 'Nothing'. - inCreatedBy :: Maybe UserId, - inInviteeEmail :: Email, - inInviteeName :: Maybe Name, - inInviteePhone :: Maybe Phone, - inInviteeUrl :: Maybe (URIRef Absolute) + createdBy :: Maybe UserId, + inviteeEmail :: EmailAddress, + inviteeName :: Maybe Name, + inviteeUrl :: Maybe (URIRef Absolute) } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform Invitation) @@ -99,29 +96,29 @@ data Invitation = Invitation instance ToSchema Invitation where schema = - objectWithDocModifier "Invitation" (description ?~ "An invitation to join a team on Wire") $ - Invitation - <$> inTeam + objectWithDocModifier + "Invitation" + (description ?~ "An invitation to join a team on Wire") + $ Invitation + <$> (.team) .= fieldWithDocModifier "team" (description ?~ "Team ID of the inviting team") schema - <*> inRole + <*> (.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) - <*> inInvitation + <*> (.invitationId) .= fieldWithDocModifier "id" (description ?~ "UUID used to refer the invitation") schema - <*> inCreatedAt + <*> (.createdAt) .= fieldWithDocModifier "created_at" (description ?~ "Timestamp of invitation creation") schema - <*> inCreatedBy + <*> (.createdBy) .= optFieldWithDocModifier "created_by" (description ?~ "ID of the inviting user") (maybeWithDefault A.Null schema) - <*> inInviteeEmail + <*> (.inviteeEmail) .= fieldWithDocModifier "email" (description ?~ "Email of the invitee") schema - <*> inInviteeName + <*> (.inviteeName) .= optFieldWithDocModifier "name" (description ?~ "Name of the invitee (1 - 128 characters)") (maybeWithDefault A.Null schema) - <*> inInviteePhone - .= optFieldWithDocModifier "phone" (description ?~ "Phone number of the invitee, in the E.164 format") (maybeWithDefault A.Null schema) - <*> (fmap (TE.decodeUtf8 . serializeURIRef') . inInviteeUrl) + <*> (fmap (TE.decodeUtf8 . serializeURIRef') . inviteeUrl) .= optFieldWithDocModifier "url" (description ?~ "URL of the invitation link to be sent to the invitee") (maybeWithDefault A.Null urlSchema) where - urlSchema = parsedText "URIRef Absolute" (runParser (uriParser strictURIParserOptions) . TE.encodeUtf8) + urlSchema = parsedText "URIRef_Absolute" (runParser (uriParser strictURIParserOptions) . TE.encodeUtf8) newtype InvitationLocation = InvitationLocation { unInvitationLocation :: ByteString @@ -182,3 +179,20 @@ instance ToSchema InvitationList where .= field "invitations" (array schema) <*> ilHasMore .= fieldWithDocModifier "has_more" (description ?~ "Indicator that the server has more invitations than returned.") schema + +-------------------------------------------------------------------------------- +-- AcceptTeamInvitation + +data AcceptTeamInvitation = AcceptTeamInvitation + { code :: InvitationCode, + password :: PlainTextPassword6 + } + deriving stock (Eq, Show, Generic) + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via (Schema AcceptTeamInvitation) + +instance ToSchema AcceptTeamInvitation where + schema = + objectWithDocModifier "AcceptTeamInvitation" (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 diff --git a/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs b/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs index a38b8ee5096..1cb233020fd 100644 --- a/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs +++ b/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs @@ -20,21 +20,28 @@ -- | Types used by the Wire server for outbound requests to a LegalHold service. module Wire.API.Team.LegalHold.External ( -- * initiate + RequestNewLegalHoldClientV0 (..), RequestNewLegalHoldClient (..), NewLegalHoldClient (..), -- * confirm + LegalHoldServiceConfirmV0 (..), LegalHoldServiceConfirm (..), -- * remove + LegalHoldServiceRemoveV0 (..), LegalHoldServiceRemove (..), + + -- * SupportedVersions + SupportedVersions (..), ) where -import Data.Aeson hiding (fieldLabelModifier) +import Data.Aeson qualified as A hiding (fieldLabelModifier) import Data.Id -import Data.Json.Util ((#)) -import Data.OpenApi +import Data.OpenApi qualified as OpenApi +import Data.Qualified +import Data.Schema import Imports import Wire.API.User.Client.Prekey import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -43,38 +50,35 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -- initiate -- | Request payload that the LH service endpoint @/initiate@ expects -data RequestNewLegalHoldClient = RequestNewLegalHoldClient +data RequestNewLegalHoldClientV0 = RequestNewLegalHoldClientV0 { userId :: UserId, teamId :: TeamId } deriving stock (Show, Eq, Generic) + deriving (Arbitrary) via (GenericUniform RequestNewLegalHoldClientV0) + deriving (A.ToJSON, A.FromJSON) via (Schema RequestNewLegalHoldClientV0) + +instance ToSchema RequestNewLegalHoldClientV0 where + schema = + object "RequestNewLegalHoldClientV0" $ + RequestNewLegalHoldClientV0 + <$> (.userId) .= field "user_id" schema + <*> (.teamId) .= field "team_id" schema + +data RequestNewLegalHoldClient = RequestNewLegalHoldClient + { userId :: Qualified UserId, + teamId :: TeamId + } + deriving stock (Show, Eq, Generic) + deriving (A.ToJSON, A.FromJSON) via (Schema RequestNewLegalHoldClient) deriving (Arbitrary) via (GenericUniform RequestNewLegalHoldClient) instance ToSchema RequestNewLegalHoldClient where - declareNamedSchema = genericDeclareNamedSchema opts - where - opts = - defaultSchemaOptions - { fieldLabelModifier = \case - "userId" -> "user_id" - "teamId" -> "team_id" - _ -> "" - } - -instance ToJSON RequestNewLegalHoldClient where - toJSON (RequestNewLegalHoldClient userId teamId) = - object $ - "user_id" - .= userId - # "team_id" - .= teamId - # [] - -instance FromJSON RequestNewLegalHoldClient where - parseJSON = withObject "RequestNewLegalHoldClient" $ \o -> - RequestNewLegalHoldClient - <$> o .: "user_id" - <*> o .: "team_id" + schema = + object "RequestNewLegalHoldClient" $ + RequestNewLegalHoldClient + <$> (.userId) .= field "qualified_user_id" schema + <*> (.teamId) .= field "team_id" schema -- | Response payload that the LH service returns upon calling @/initiate@ data NewLegalHoldClient = NewLegalHoldClient @@ -83,38 +87,51 @@ data NewLegalHoldClient = NewLegalHoldClient } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform NewLegalHoldClient) + deriving (A.ToJSON, A.FromJSON) via (Schema NewLegalHoldClient) -instance ToSchema NewLegalHoldClient where - declareNamedSchema = genericDeclareNamedSchema opts +instance OpenApi.ToSchema NewLegalHoldClient where + declareNamedSchema = OpenApi.genericDeclareNamedSchema opts where opts = - defaultSchemaOptions - { fieldLabelModifier = \case + OpenApi.defaultSchemaOptions + { OpenApi.fieldLabelModifier = \case "newLegalHoldClientPrekeys" -> "prekeys" "newLegalHoldClientLastKey" -> "last_prekey" _ -> "" } -instance ToJSON NewLegalHoldClient where - toJSON c = - object $ - "prekeys" - .= newLegalHoldClientPrekeys c - # "last_prekey" - .= newLegalHoldClientLastKey c - # [] - -instance FromJSON NewLegalHoldClient where - parseJSON = withObject "NewLegalHoldClient" $ \o -> - NewLegalHoldClient - <$> o .: "prekeys" - <*> o .: "last_prekey" +instance ToSchema NewLegalHoldClient where + schema = + object "NewLegalHoldClient" $ + NewLegalHoldClient + <$> (.newLegalHoldClientPrekeys) .= field "prekeys" (array schema) + <*> (.newLegalHoldClientLastKey) .= field "last_prekey" schema -------------------------------------------------------------------------------- -- confirm -- Request payload for the @/confirm@ endpoint on the LegalHold Service data LegalHoldServiceConfirm = LegalHoldServiceConfirm + { clientId :: ClientId, + userId :: Qualified UserId, + teamId :: TeamId, + -- | Replace with Legal Hold Token Type + refreshToken :: Text + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform LegalHoldServiceConfirm) + deriving (A.ToJSON, A.FromJSON) via (Schema LegalHoldServiceConfirm) + +instance ToSchema LegalHoldServiceConfirm where + schema = + object "LegalHoldServiceConfirm" $ + LegalHoldServiceConfirm + <$> (.clientId) .= field "client_id" schema + <*> (.userId) .= field "qualified_user_id" schema + <*> (.teamId) .= field "team_id" schema + <*> (.refreshToken) .= field "refresh_token" schema + +data LegalHoldServiceConfirmV0 = LegalHoldServiceConfirmV0 { lhcClientId :: ClientId, lhcUserId :: UserId, lhcTeamId :: TeamId, @@ -122,51 +139,61 @@ data LegalHoldServiceConfirm = LegalHoldServiceConfirm lhcRefreshToken :: Text } deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform LegalHoldServiceConfirm) - -instance ToJSON LegalHoldServiceConfirm where - toJSON (LegalHoldServiceConfirm clientId userId teamId refreshToken) = - object $ - "client_id" - .= clientId - # "user_id" - .= userId - # "team_id" - .= teamId - # "refresh_token" - .= refreshToken - # [] - -instance FromJSON LegalHoldServiceConfirm where - parseJSON = withObject "LegalHoldServiceConfirm" $ \o -> - LegalHoldServiceConfirm - <$> o .: "client_id" - <*> o .: "user_id" - <*> o .: "team_id" - <*> o .: "refresh_token" + deriving (Arbitrary) via (GenericUniform LegalHoldServiceConfirmV0) + deriving (A.ToJSON, A.FromJSON) via (Schema LegalHoldServiceConfirmV0) + +instance ToSchema LegalHoldServiceConfirmV0 where + schema = + object "LegalHoldServiceConfirmV0" $ + LegalHoldServiceConfirmV0 + <$> (.lhcClientId) .= field "client_id" schema + <*> (.lhcUserId) .= field "user_id" schema + <*> (.lhcTeamId) .= field "team_id" schema + <*> (.lhcRefreshToken) .= field "refresh_token" schema -------------------------------------------------------------------------------- -- remove -- Request payload for the @/remove@ endpoint on the LegalHold Service data LegalHoldServiceRemove = LegalHoldServiceRemove + { userId :: Qualified UserId, + teamId :: TeamId + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform LegalHoldServiceRemove) + deriving (A.ToJSON, A.FromJSON) via (Schema LegalHoldServiceRemove) + +instance ToSchema LegalHoldServiceRemove where + schema = + object "LegalHoldServiceRemove" $ + LegalHoldServiceRemove + <$> (.userId) .= field "qualified_user_id" schema + <*> (.teamId) .= field "team_id" schema + +data LegalHoldServiceRemoveV0 = LegalHoldServiceRemoveV0 { lhrUserId :: UserId, lhrTeamId :: TeamId } deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform LegalHoldServiceRemove) + deriving (Arbitrary) via (GenericUniform LegalHoldServiceRemoveV0) + deriving (A.ToJSON, A.FromJSON) via (Schema LegalHoldServiceRemoveV0) + +instance ToSchema LegalHoldServiceRemoveV0 where + schema = + object "LegalHoldServiceRemoveV0" $ + LegalHoldServiceRemoveV0 + <$> (.lhrUserId) .= field "user_id" schema + <*> (.lhrTeamId) .= field "team_id" schema + +-------------------------------------------------------------------------------- +-- SupportedVersions + +newtype SupportedVersions = SupportedVersions {supported :: [Int]} + deriving (A.FromJSON) via (Schema SupportedVersions) -instance ToJSON LegalHoldServiceRemove where - toJSON (LegalHoldServiceRemove userId teamId) = - object $ - "user_id" - .= userId - # "team_id" - .= teamId - # [] - -instance FromJSON LegalHoldServiceRemove where - parseJSON = withObject "LegalHoldServiceRemove" $ \o -> - LegalHoldServiceRemove - <$> o .: "user_id" - <*> o .: "team_id" +instance ToSchema SupportedVersions where + schema = + object "SupportedVersions " $ + SupportedVersions + <$> supported + .= field "supported" (array schema) diff --git a/libs/wire-api/src/Wire/API/Team/Member.hs b/libs/wire-api/src/Wire/API/Team/Member.hs index 812c63c000d..108e19adb98 100644 --- a/libs/wire-api/src/Wire/API/Team/Member.hs +++ b/libs/wire-api/src/Wire/API/Team/Member.hs @@ -22,13 +22,13 @@ module Wire.API.Team.Member ( -- * TeamMember TeamMember, + newTeamMember, mkTeamMember, userId, permissions, invitation, legalHoldStatus, ntmNewTeamMember, - teamMemberJson, setOptionalPerms, setOptionalPermsMany, teamMemberObjectSchema, @@ -426,9 +426,6 @@ permissions = newTeamMember . nPermissions invitation :: Lens' TeamMember (Maybe (UserId, UTCTimeMillis)) invitation = newTeamMember . nInvitation -teamMemberJson :: (TeamMember -> Bool) -> TeamMember -> Value -teamMemberJson withPerms = toJSON . setOptionalPerms withPerms - setOptionalPerms :: (TeamMember -> Bool) -> TeamMember -> TeamMember' 'Optional setOptionalPerms withPerms m = m & permissions %~ setPerm (withPerms m) @@ -589,10 +586,10 @@ class IsPerm perm where instance IsPerm Perm where type PermError p = 'MissingPermission ('Just p) - roleHasPerm r p = p `Set.member` (rolePermissions r ^. self) - roleGrantsPerm r p = p `Set.member` (rolePermissions r ^. copy) - hasPermission tm p = p `Set.member` (tm ^. permissions . self) - mayGrantPermission tm p = p `Set.member` (tm ^. permissions . copy) + roleHasPerm r p = p `Set.member` ((rolePermissions r).self) + roleGrantsPerm r p = p `Set.member` ((rolePermissions r).copy) + hasPermission tm p = p `Set.member` ((tm ^. permissions).self) + mayGrantPermission tm p = p `Set.member` ((tm ^. permissions).copy) instance IsPerm HiddenPerm where type PermError p = OperationDenied diff --git a/libs/wire-api/src/Wire/API/Team/Permission.hs b/libs/wire-api/src/Wire/API/Team/Permission.hs index b4ac0d90455..6cf79772019 100644 --- a/libs/wire-api/src/Wire/API/Team/Permission.hs +++ b/libs/wire-api/src/Wire/API/Team/Permission.hs @@ -26,8 +26,6 @@ module Wire.API.Team.Permission ( -- * Permissions Permissions (..), - self, - copy, newPermissions, fullPermissions, noPermissions, @@ -45,7 +43,7 @@ where import Cassandra qualified as Cql import Control.Error.Util qualified as Err -import Control.Lens (makeLenses, (?~), (^.)) +import Control.Lens ((?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Bits (testBit, (.|.)) import Data.OpenApi qualified as S @@ -61,8 +59,10 @@ import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) -- Permissions data Permissions = Permissions - { _self :: Set Perm, - _copy :: Set Perm + { -- | User's permissions + self :: Set Perm, + -- | Permissions this user is allowed to grant others + copy :: Set Perm } deriving stock (Eq, Ord, Show, Generic) deriving (FromJSON, ToJSON, S.ToSchema) via (Schema Permissions) @@ -71,9 +71,11 @@ permissionsSchema :: ValueSchema NamedSwaggerDoc Permissions permissionsSchema = objectWithDocModifier "Permissions" (description ?~ docs) $ Permissions - <$> (permsToInt . _self) .= field "self" (intToPerms <$> schema) - <*> (permsToInt . _copy) .= field "copy" (intToPerms <$> schema) + <$> (permsToInt . self) .= fieldWithDocModifier "self" selfDoc (intToPerms <$> schema) + <*> (permsToInt . copy) .= fieldWithDocModifier "copy" copyDoc (intToPerms <$> schema) where + selfDoc = S.description ?~ "Permissions that the user has" + copyDoc = S.description ?~ "Permissions that this user is able to grant others" docs = "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 \ @@ -198,14 +200,12 @@ intToPerm 0x0800 = Just DeleteTeam intToPerm 0x1000 = Just SetMemberPermissions intToPerm _ = Nothing -makeLenses ''Permissions - instance Cql.Cql Permissions where ctype = Cql.Tagged $ Cql.UdtColumn "permissions" [("self", Cql.BigIntColumn), ("copy", Cql.BigIntColumn)] toCql p = let f = Cql.CqlBigInt . fromIntegral . permsToInt - in Cql.CqlUdt [("self", f (p ^. self)), ("copy", f (p ^. copy))] + in Cql.CqlUdt [("self", f p.self), ("copy", f p.copy)] fromCql (Cql.CqlUdt p) = do let f = intToPerms . fromIntegral :: Int64 -> Set.Set Perm diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index e24f63536f1..25cf9e88172 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -28,24 +28,29 @@ module Wire.API.User qualifiedUserIdListObjectSchema, LimitedQualifiedUserIdList (..), ScimUserInfo (..), - ScimUserInfos (..), UserSet (..), -- Profiles UserProfile (..), SelfProfile (..), -- User (should not be here) User (..), + isSamlUser, userId, + userDeleted, userEmail, userSSOId, userIssuer, - userSCIMExternalId, scimExternalId, ssoIssuerAndNameId, mkUserProfile, mkUserProfileWithEmail, userObjectSchema, + -- * UpgradePersonalToTeam + CreateUserTeam (..), + UpgradePersonalToTeamResponses, + UpgradePersonalToTeamError (..), + -- * NewUser NewUserPublic (..), RegisterError (..), @@ -59,10 +64,7 @@ module Wire.API.User CreateUserSparInternalResponses, newUserFromSpar, urefToExternalId, - urefToExternalIdUnsafe, - urefToEmail, ExpiresIn, - newUserInvitationCode, newUserTeam, newUserEmail, newUserSSOId, @@ -99,7 +101,6 @@ module Wire.API.User DeleteUser (..), mkDeleteUser, VerifyDeleteUser (..), - mkVerifyDeleteUser, DeletionCodeTimeout (..), DeleteUserResponse (..), DeleteUserResult (..), @@ -109,9 +110,6 @@ module Wire.API.User AccountStatusUpdate (..), AccountStatusResp (..), - -- * Account - UserAccount (..), - -- * Scim invitations NewUserScimInvitation (..), @@ -123,7 +121,6 @@ module Wire.API.User GetPasswordResetCodeResp (..), CheckBlacklistResponse (..), ManagedByUpdate (..), - HavePendingInvitations (..), RichInfoUpdate (..), PasswordResetPair, UpdateSSOIdResponse (..), @@ -156,7 +153,7 @@ import Cassandra qualified as C import Control.Applicative import Control.Arrow ((&&&)) import Control.Error.Safe (rightMay) -import Control.Lens (makePrisms, over, view, (.~), (?~), (^.)) +import Control.Lens (makePrisms, over, view, (.~), (?~)) import Data.Aeson (FromJSON (..), ToJSON (..), withText) import Data.Aeson.Types qualified as A import Data.Attoparsec.ByteString qualified as Parser @@ -194,7 +191,6 @@ import GHC.TypeLits import Generics.SOP qualified as GSOP import Imports import SAML2.WebSSO qualified as SAML -import SAML2.WebSSO.Types.Email qualified as SAMLEmail import Servant (FromHttpApiData (..), ToHttpApiData (..), type (.++)) import Test.QuickCheck qualified as QC import URI.ByteString (serializeURIRef) @@ -206,13 +202,13 @@ import Wire.API.Error.Brig qualified as E import Wire.API.Locale import Wire.API.Provider.Service (ServiceRef) import Wire.API.Routes.MultiVerb -import Wire.API.Team (BindingNewTeam, bindingNewTeamObjectSchema) +import Wire.API.Team 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.Identity +import Wire.API.User.Identity hiding (toByteString) import Wire.API.User.Password import Wire.API.User.Profile import Wire.API.User.RichInfo @@ -310,11 +306,6 @@ instance ToSchema ManagedByUpdate where ManagedByUpdate <$> mbuManagedBy .= field "managed_by" schema -data HavePendingInvitations - = WithPendingInvitations - | NoPendingInvitations - deriving (Eq, Show, Generic) - newtype RichInfoUpdate = RichInfoUpdate {riuRichInfo :: RichInfoAssocList} deriving (Eq, Show, Generic) deriving newtype (Arbitrary) @@ -479,6 +470,7 @@ instance (1 <= max) => ToJSON (LimitedQualifiedUserIdList max) where data UserProfile = UserProfile { profileQualifiedId :: Qualified UserId, profileName :: Name, + profileTextStatus :: Maybe TextStatus, -- | DEPRECATED profilePict :: Pict, profileAssets :: [Asset], @@ -490,7 +482,7 @@ data UserProfile = UserProfile profileHandle :: Maybe Handle, profileExpire :: Maybe UTCTimeMillis, profileTeam :: Maybe TeamId, - profileEmail :: Maybe Email, + profileEmail :: Maybe EmailAddress, profileLegalholdStatus :: UserLegalHoldStatus, profileSupportedProtocols :: Set BaseProtocolTag } @@ -508,6 +500,8 @@ instance ToSchema UserProfile where .= optional (field "id" (deprecatedSchema "qualified_id" schema)) <*> profileName .= field "name" schema + <*> profileTextStatus + .= maybe_ (optField "text_status" schema) <*> profilePict .= (field "picture" schema <|> pure noPict) <*> profileAssets @@ -560,13 +554,16 @@ data User = User -- the user is activated, and the email/phone contained in it will be guaranteedly -- verified. {#RefActivation} userIdentity :: Maybe UserIdentity, + userEmailUnvalidated :: Maybe EmailAddress, -- | required; non-unique userDisplayName :: Name, + -- | text status + userTextStatus :: Maybe TextStatus, -- | DEPRECATED userPict :: Pict, userAssets :: [Asset], userAccentId :: ColourId, - userDeleted :: Bool, + userStatus :: AccountStatus, userLocale :: Locale, -- | Set if the user represents an external service, -- i.e. it is a "bot". @@ -582,13 +579,22 @@ data User = User userManagedBy :: ManagedBy, userSupportedProtocols :: Set BaseProtocolTag } - deriving stock (Eq, Show, Generic) + deriving stock (Eq, Ord, Show, Generic) deriving (Arbitrary) via (GenericUniform User) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema User) +isSamlUser :: User -> Bool +isSamlUser usr = do + case usr.userIdentity of + Just (SSOIdentity (UserSSOId _) _) -> True + _ -> False + userId :: User -> UserId userId = qUnqualified . userQualifiedId +userDeleted :: User -> Bool +userDeleted u = userStatus u == Deleted + -- -- FUTUREWORK: -- -- disentangle json serializations for 'User', 'NewUser', 'UserIdentity', 'NewUserOrigin'. instance ToSchema User where @@ -601,40 +607,34 @@ userObjectSchema = .= field "qualified_id" schema <* userId .= optional (field "id" (deprecatedSchema "qualified_id" schema)) - <*> userIdentity - .= maybeUserIdentityObjectSchema + <*> userIdentity .= maybeUserIdentityObjectSchema + <*> userEmailUnvalidated .= maybe_ (optField "email_unvalidated" schema) <*> userDisplayName .= field "name" schema + <*> userTextStatus + .= maybe_ (optField "text_status" schema) <*> userPict .= (fromMaybe noPict <$> optField "picture" schema) <*> userAssets .= (fromMaybe [] <$> optField "assets" (array schema)) - <*> userAccentId - .= field "accent_id" schema - <*> (fromMaybe False <$> (\u -> if userDeleted u then Just True else Nothing) .= maybe_ (optField "deleted" schema)) - <*> userLocale - .= field "locale" schema - <*> userService - .= maybe_ (optField "service" schema) - <*> userHandle - .= maybe_ (optField "handle" schema) - <*> userExpire - .= maybe_ (optField "expires_at" schema) - <*> userTeam - .= maybe_ (optField "team" schema) + <*> userAccentId .= field "accent_id" schema + <*> userStatus .= field "status" schema + <*> userLocale .= field "locale" schema + <*> userService .= maybe_ (optField "service" schema) + <*> userHandle .= maybe_ (optField "handle" schema) + <*> userExpire .= maybe_ (optField "expires_at" schema) + <*> userTeam .= maybe_ (optField "team" schema) <*> userManagedBy .= (fromMaybe ManagedByWire <$> optField "managed_by" schema) <*> userSupportedProtocols .= supportedProtocolsObjectSchema + <* (fromMaybe False <$> (\u -> if userDeleted u then Just True else Nothing) .= maybe_ (optField "deleted" schema)) -userEmail :: User -> Maybe Email +userEmail :: User -> Maybe EmailAddress userEmail = emailIdentity <=< userIdentity userSSOId :: User -> Maybe UserSSOId userSSOId = ssoIdentity <=< userIdentity -userSCIMExternalId :: User -> Maybe Text -userSCIMExternalId usr = scimExternalId (userManagedBy usr) =<< userSSOId usr - -- FUTUREWORK: this is only ignoring case in the email format, and emails should be -- handled case-insensitively. https://wearezeta.atlassian.net/browse/SQSERVICES-909 scimExternalId :: ManagedBy -> UserSSOId -> Maybe Text @@ -684,7 +684,7 @@ instance FromJSON (EmailVisibility ()) where "visible_to_self" -> pure EmailVisibleToSelf _ -> fail "unexpected value for EmailVisibility settings" -mkUserProfileWithEmail :: Maybe Email -> User -> UserLegalHoldStatus -> UserProfile +mkUserProfileWithEmail :: Maybe EmailAddress -> User -> UserLegalHoldStatus -> UserProfile mkUserProfileWithEmail memail u 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. @@ -692,6 +692,7 @@ mkUserProfileWithEmail memail u legalHoldStatus = { profileQualifiedId = userQualifiedId u, profileHandle = userHandle u, profileName = userDisplayName u, + profileTextStatus = userTextStatus u, profilePict = userPict u, profileAssets = userAssets u, profileAccentId = userAccentId u, @@ -774,6 +775,47 @@ isNewUserTeamMember u = case newUserTeam u of instance Arbitrary NewUserPublic where arbitrary = arbitrary `QC.suchThatMap` (rightMay . validateNewUserPublic) +data CreateUserTeam = CreateUserTeam + { createdTeamId :: !TeamId, + createdTeamName :: !Text + } + deriving (Show) + deriving (FromJSON, ToJSON, S.ToSchema) via Schema CreateUserTeam + +instance ToSchema CreateUserTeam where + schema = + object "CreateUserTeam" $ + CreateUserTeam + <$> createdTeamId .= field "team_id" schema + <*> createdTeamName .= field "team_name" schema + +data UpgradePersonalToTeamError + = UpgradePersonalToTeamErrorAlreadyInATeam + | UpgradePersonalToTeamErrorUserNotFound + deriving (Show) + +type UpgradePersonalToTeamResponses = + '[ ErrorResponse UserAlreadyInATeam, + ErrorResponse UserNotFound, + Respond 200 "Team created" CreateUserTeam + ] + +instance + AsUnion + UpgradePersonalToTeamResponses + (Either UpgradePersonalToTeamError CreateUserTeam) + where + toUnion (Left UpgradePersonalToTeamErrorAlreadyInATeam) = + Z (I (dynError @(MapError UserAlreadyInATeam))) + toUnion (Left UpgradePersonalToTeamErrorUserNotFound) = + S (Z (I (dynError @(MapError UserNotFound)))) + toUnion (Right x) = S (S (Z (I x))) + + fromUnion (Z (I _)) = Left UpgradePersonalToTeamErrorAlreadyInATeam + fromUnion (S (Z (I _))) = Left UpgradePersonalToTeamErrorAlreadyInATeam + fromUnion (S (S (Z (I x)))) = Right x + fromUnion (S (S (S x))) = case x of {} + data RegisterError = RegisterErrorAllowlistError | RegisterErrorInvalidInvitationCode @@ -844,14 +886,6 @@ instance (res ~ RegisterInternalResponses) => AsUnion res (Either RegisterError urefToExternalId :: SAML.UserRef -> Maybe Text urefToExternalId = fmap CI.original . SAML.shortShowNameID . view SAML.uidSubject -urefToEmail :: SAML.UserRef -> Maybe Email -urefToEmail uref = case uref ^. SAML.uidSubject . SAML.nameID of - SAML.UNameIDEmail email -> parseEmail . SAMLEmail.render . CI.original $ email - _ -> Nothing - -urefToExternalIdUnsafe :: SAML.UserRef -> Text -urefToExternalIdUnsafe = CI.original . SAML.unsafeShowNameID . view SAML.uidSubject - data CreateUserSparError = CreateUserSparHandleError ChangeHandleError | CreateUserSparRegistrationError RegisterError @@ -941,12 +975,10 @@ newUserFromSpar new = { newUserDisplayName = newUserSparDisplayName new, newUserUUID = Just $ newUserSparUUID new, newUserIdentity = Just $ SSOIdentity (newUserSparSSOId new) Nothing, - newUserPhone = Nothing, newUserPict = Nothing, newUserAssets = [], newUserAccentId = Nothing, newUserEmailCode = Nothing, - newUserPhoneCode = Nothing, newUserOrigin = Just . NewUserOriginTeamUser . NewTeamMemberSSO $ newUserSparTeamId new, newUserLabel = Nothing, newUserPassword = Nothing, @@ -961,13 +993,11 @@ data NewUser = NewUser -- | use this as 'UserId' (if 'Nothing', call 'Data.UUID.nextRandom'). newUserUUID :: Maybe UUID, newUserIdentity :: Maybe UserIdentity, - newUserPhone :: Maybe Phone, -- | DEPRECATED newUserPict :: Maybe Pict, newUserAssets :: [Asset], newUserAccentId :: Maybe ColourId, newUserEmailCode :: Maybe ActivationCode, - newUserPhoneCode :: Maybe ActivationCode, newUserOrigin :: Maybe NewUserOrigin, newUserLabel :: Maybe CookieLabel, newUserLocale :: Maybe Locale, @@ -985,12 +1015,10 @@ emptyNewUser name = { newUserDisplayName = name, newUserUUID = Nothing, newUserIdentity = Nothing, - newUserPhone = Nothing, newUserPict = Nothing, newUserAssets = [], newUserAccentId = Nothing, newUserEmailCode = Nothing, - newUserPhoneCode = Nothing, newUserOrigin = Nothing, newUserLabel = Nothing, newUserLocale = Nothing, @@ -1007,17 +1035,13 @@ type ExpiresIn = Range 1 604800 Integer data NewUserRaw = NewUserRaw { newUserRawDisplayName :: Name, newUserRawUUID :: Maybe UUID, - newUserRawEmail :: Maybe Email, - -- | This is deprecated and it should always be 'Nothing'. - newUserRawPhone :: Maybe Phone, + newUserRawEmail :: Maybe EmailAddress, newUserRawSSOId :: Maybe UserSSOId, -- | DEPRECATED newUserRawPict :: Maybe Pict, newUserRawAssets :: [Asset], newUserRawAccentId :: Maybe ColourId, newUserRawEmailCode :: Maybe ActivationCode, - -- | This is deprecated and it should always be 'Nothing'. - newUserRawPhoneCode :: Maybe ActivationCode, newUserRawInvitationCode :: Maybe InvitationCode, newUserRawTeamCode :: Maybe InvitationCode, newUserRawTeam :: Maybe BindingNewTeamUser, @@ -1039,8 +1063,6 @@ newUserRawObjectSchema = .= maybe_ (optField "uuid" genericToSchema) <*> newUserRawEmail .= maybe_ (optField "email" schema) - <*> newUserRawPhone - .= maybe_ (optField "phone" schema) <*> newUserRawSSOId .= maybe_ (optField "sso_id" genericToSchema) <*> newUserRawPict @@ -1051,8 +1073,6 @@ newUserRawObjectSchema = .= maybe_ (optField "accent_id" schema) <*> newUserRawEmailCode .= maybe_ (optField "email_code" schema) - <*> newUserRawPhoneCode - .= maybe_ (optField "phone_code" schema) <*> newUserRawInvitationCode .= maybe_ (optField "invitation_code" schema) <*> newUserRawTeamCode @@ -1085,13 +1105,11 @@ newUserToRaw NewUser {..} = { newUserRawDisplayName = newUserDisplayName, newUserRawUUID = newUserUUID, newUserRawEmail = emailIdentity =<< newUserIdentity, - newUserRawPhone = newUserPhone, newUserRawSSOId = ssoIdentity =<< newUserIdentity, newUserRawPict = newUserPict, newUserRawAssets = newUserAssets, newUserRawAccentId = newUserAccentId, newUserRawEmailCode = newUserEmailCode, - newUserRawPhoneCode = newUserPhoneCode, newUserRawInvitationCode = newUserOriginInvitationCode =<< newUserOrigin, newUserRawTeamCode = newTeamUserCode =<< maybeOriginNTU, newUserRawTeam = newTeamUserCreator =<< maybeOriginNTU, @@ -1124,12 +1142,10 @@ newUserFromRaw NewUserRaw {..} = do { newUserDisplayName = newUserRawDisplayName, newUserUUID = newUserRawUUID, newUserIdentity = identity, - newUserPhone = newUserRawPhone, newUserPict = newUserRawPict, newUserAssets = newUserRawAssets, newUserAccentId = newUserRawAccentId, newUserEmailCode = newUserRawEmailCode, - newUserPhoneCode = newUserRawPhoneCode, newUserOrigin = origin, newUserLabel = newUserRawLabel, newUserLocale = newUserRawLocale, @@ -1143,7 +1159,6 @@ newUserFromRaw NewUserRaw {..} = do instance Arbitrary NewUser where arbitrary = do newUserIdentity <- arbitrary - newUserPhone <- arbitrary newUserOrigin <- genUserOrigin newUserIdentity newUserDisplayName <- arbitrary newUserUUID <- QC.elements [Just nil, Nothing] @@ -1151,7 +1166,6 @@ instance Arbitrary NewUser where newUserAssets <- arbitrary newUserAccentId <- arbitrary newUserEmailCode <- arbitrary - newUserPhoneCode <- arbitrary newUserLabel <- arbitrary newUserLocale <- arbitrary newUserPassword <- genUserPassword newUserIdentity newUserOrigin @@ -1182,17 +1196,12 @@ instance Arbitrary NewUser where genUserExpiresIn newUserIdentity = if isJust newUserIdentity then pure Nothing else arbitrary -newUserInvitationCode :: NewUser -> Maybe InvitationCode -newUserInvitationCode nu = case newUserOrigin nu of - Just (NewUserOriginInvitationCode ic) -> Just ic - _ -> Nothing - newUserTeam :: NewUser -> Maybe NewTeamUser newUserTeam nu = case newUserOrigin nu of Just (NewUserOriginTeamUser tu) -> Just tu _ -> Nothing -newUserEmail :: NewUser -> Maybe Email +newUserEmail :: NewUser -> Maybe EmailAddress newUserEmail = emailIdentity <=< newUserIdentity newUserSSOId :: NewUser -> Maybe UserSSOId @@ -1244,7 +1253,7 @@ maybeNewUserOriginFromComponents hasPassword hasSSO (invcode, teamcode, team, te -- | A random invitation code for use during registration newtype InvitationCode = InvitationCode {fromInvitationCode :: AsciiBase64Url} - deriving stock (Eq, Show, Generic) + deriving stock (Eq, Ord, Show, Generic) deriving newtype (ToSchema, ToByteString, FromByteString, Arbitrary) deriving (FromJSON, ToJSON, S.ToSchema) via Schema InvitationCode @@ -1294,7 +1303,7 @@ newTeamUserTeamId = \case NewTeamMemberSSO tid -> Just tid data BindingNewTeamUser = BindingNewTeamUser - { bnuTeam :: BindingNewTeam, + { bnuTeam :: NewTeam, bnuCurrency :: Maybe Currency.Alpha -- FUTUREWORK: -- Remove Currency selection once billing supports currency changes after team creation @@ -1308,7 +1317,7 @@ instance ToSchema BindingNewTeamUser where object "BindingNewTeamUser" $ BindingNewTeamUser <$> bnuTeam - .= bindingNewTeamObjectSchema + .= newTeamObjectSchema <*> bnuCurrency .= maybe_ (optField "currency" genericToSchema) @@ -1332,18 +1341,6 @@ instance ToSchema ScimUserInfo where <*> suiCreatedOn .= maybe_ (optField "created_on" schema) -newtype ScimUserInfos = ScimUserInfos {scimUserInfos :: [ScimUserInfo]} - deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform ScimUserInfos) - deriving (ToJSON, FromJSON, S.ToSchema) via (Schema ScimUserInfos) - -instance ToSchema ScimUserInfos where - schema = - object "ScimUserInfos" $ - ScimUserInfos - <$> scimUserInfos - .= field "scim_user_infos" (array schema) - ------------------------------------------------------------------------------- -- UserSet @@ -1368,6 +1365,7 @@ instance ToSchema UserSet where data UserUpdate = UserUpdate { uupName :: Maybe Name, + uupTextStatus :: Maybe TextStatus, -- | DEPRECATED uupPict :: Maybe Pict, uupAssets :: Maybe [Asset], @@ -1383,6 +1381,8 @@ instance ToSchema UserUpdate where UserUpdate <$> uupName .= maybe_ (optField "name" schema) + <*> uupTextStatus + .= maybe_ (optField "text_status" schema) <*> uupPict .= maybe_ (optField "picture" schema) <*> uupAssets @@ -1408,8 +1408,8 @@ instance (res ~ PutSelfResponses) => AsUnion res (Maybe UpdateProfileError) wher -- | The payload for setting or changing a password. data PasswordChange = PasswordChange - { cpOldPassword :: Maybe PlainTextPassword6, - cpNewPassword :: PlainTextPassword8 + { oldPassword :: Maybe PlainTextPassword6, + newPassword :: PlainTextPassword8 } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform PasswordChange) @@ -1425,9 +1425,9 @@ instance ToSchema PasswordChange where ) . object "PasswordChange" $ PasswordChange - <$> cpOldPassword + <$> oldPassword .= maybe_ (optField "old_password" schema) - <*> cpNewPassword + <*> newPassword .= field "new_password" schema data ChangePasswordError @@ -1464,7 +1464,7 @@ instance ToSchema LocaleUpdate where <$> luLocale .= field "locale" schema -newtype EmailUpdate = EmailUpdate {euEmail :: Email} +newtype EmailUpdate = EmailUpdate {euEmail :: EmailAddress} deriving stock (Eq, Show, Generic) deriving newtype (Arbitrary) deriving (S.ToSchema) via (Schema EmailUpdate) @@ -1639,9 +1639,6 @@ data VerifyDeleteUser = VerifyDeleteUser deriving (Arbitrary) via (GenericUniform VerifyDeleteUser) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema VerifyDeleteUser) -mkVerifyDeleteUser :: Code.Key -> Code.Value -> VerifyDeleteUser -mkVerifyDeleteUser = VerifyDeleteUser - instance ToSchema VerifyDeleteUser where schema = objectWithDocModifier "VerifyDeleteUser" (description ?~ "Data for verifying an account deletion.") $ @@ -1802,24 +1799,6 @@ instance Schema.ToSchema AccountStatusUpdate where ------------------------------------------------------------------------------- -- UserAccount --- | A UserAccount is targeted to be used by our \"backoffice\" and represents --- all the data related to a user in our system, regardless of whether they --- are active or not, their status, etc. -data UserAccount = UserAccount - { accountUser :: !User, - accountStatus :: !AccountStatus - } - deriving (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform UserAccount) - deriving (ToJSON, FromJSON, S.ToSchema) via Schema.Schema UserAccount - -instance Schema.ToSchema UserAccount where - schema = - Schema.object "UserAccount" $ - UserAccount - <$> accountUser Schema..= userObjectSchema - <*> accountStatus Schema..= Schema.field "status" Schema.schema - ------------------------------------------------------------------------------- -- NewUserScimInvitation @@ -1827,9 +1806,10 @@ data NewUserScimInvitation = NewUserScimInvitation -- FIXME: the TID should be captured in the route as usual { newUserScimInvTeamId :: TeamId, newUserScimInvUserId :: UserId, + newUserScimExternalId :: Text, newUserScimInvLocale :: Maybe Locale, newUserScimInvName :: Name, - newUserScimInvEmail :: Email, + newUserScimInvEmail :: EmailAddress, newUserScimInvRole :: Role } deriving (Eq, Show, Generic) @@ -1842,6 +1822,7 @@ instance Schema.ToSchema NewUserScimInvitation where NewUserScimInvitation <$> newUserScimInvTeamId Schema..= Schema.field "team_id" Schema.schema <*> newUserScimInvUserId Schema..= Schema.field "user_id" Schema.schema + <*> newUserScimExternalId Schema..= field "external_id" schema <*> newUserScimInvLocale Schema..= maybe_ (optField "locale" Schema.schema) <*> newUserScimInvName Schema..= Schema.field "name" Schema.schema <*> newUserScimInvEmail Schema..= Schema.field "email" Schema.schema @@ -1900,7 +1881,7 @@ instance ToHttpApiData VerificationAction where data SendVerificationCode = SendVerificationCode { svcAction :: VerificationAction, - svcEmail :: Email + svcEmail :: EmailAddress } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform SendVerificationCode) diff --git a/libs/wire-api/src/Wire/API/User/Activation.hs b/libs/wire-api/src/Wire/API/User/Activation.hs index ff21fc57ac7..5e347a54afe 100644 --- a/libs/wire-api/src/Wire/API/User/Activation.hs +++ b/libs/wire-api/src/Wire/API/User/Activation.hs @@ -45,7 +45,6 @@ import Data.OpenApi (ToParamSchema) import Data.OpenApi qualified as S import Data.Schema import Data.Text.Ascii -import Data.Tuple.Extra (fst3, snd3, thd3) import Imports import Servant (FromHttpApiData (..)) import Wire.API.Locale @@ -59,17 +58,14 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) data ActivationTarget = -- | An opaque key for some email awaiting activation. ActivateKey ActivationKey - | -- | A known phone number awaiting activation. - ActivatePhone Phone | -- | A known email address awaiting activation. - ActivateEmail Email + ActivateEmail EmailAddress deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ActivationTarget) instance ToByteString ActivationTarget where builder (ActivateKey k) = builder k builder (ActivateEmail e) = builder e - builder (ActivatePhone p) = builder p -- | An opaque identifier of a 'UserKey' awaiting activation. newtype ActivationKey = ActivationKey @@ -142,33 +138,29 @@ instance ToSchema Activate where \cookies or tokens on success but failures still count \ \towards the maximum failure count." - maybeActivationTargetObjectSchema :: ObjectSchemaP SwaggerDoc (Maybe ActivationKey, Maybe Phone, Maybe Email) ActivationTarget + maybeActivationTargetObjectSchema :: ObjectSchemaP SwaggerDoc (Maybe ActivationKey, Maybe EmailAddress) ActivationTarget maybeActivationTargetObjectSchema = withParser activationTargetTupleObjectSchema maybeActivationTargetTargetFromTuple where - activationTargetTupleObjectSchema :: ObjectSchema SwaggerDoc (Maybe ActivationKey, Maybe Phone, Maybe Email) + activationTargetTupleObjectSchema :: ObjectSchema SwaggerDoc (Maybe ActivationKey, Maybe EmailAddress) activationTargetTupleObjectSchema = - (,,) - <$> fst3 .= maybe_ (optFieldWithDocModifier "key" keyDocs schema) - <*> snd3 .= maybe_ (optFieldWithDocModifier "phone" phoneDocs schema) - <*> thd3 .= maybe_ (optFieldWithDocModifier "email" emailDocs schema) + (,) + <$> fst .= maybe_ (optFieldWithDocModifier "key" keyDocs schema) + <*> snd .= maybe_ (optFieldWithDocModifier "email" emailDocs schema) where keyDocs = description ?~ "An opaque key to activate, as it was sent by the API." - phoneDocs = description ?~ "A known phone number to activate." emailDocs = description ?~ "A known email address to activate." - maybeActivationTargetTargetFromTuple :: (Maybe ActivationKey, Maybe Phone, Maybe Email) -> Parser ActivationTarget + maybeActivationTargetTargetFromTuple :: (Maybe ActivationKey, Maybe EmailAddress) -> Parser ActivationTarget maybeActivationTargetTargetFromTuple = \case - (Just key, _, _) -> pure $ ActivateKey key - (_, _, Just email) -> pure $ ActivateEmail email - (_, Just phone, _) -> pure $ ActivatePhone phone - _ -> fail "key, email or phone must be present" + (Just key, _) -> pure $ ActivateKey key + (_, Just email) -> pure $ ActivateEmail email + _ -> fail "key or email must be present" - maybeActivationTargetToTuple :: ActivationTarget -> (Maybe ActivationKey, Maybe Phone, Maybe Email) + maybeActivationTargetToTuple :: ActivationTarget -> (Maybe ActivationKey, Maybe EmailAddress) maybeActivationTargetToTuple = \case - ActivateKey key -> (Just key, Nothing, Nothing) - ActivatePhone phone -> (Nothing, Just phone, Nothing) - ActivateEmail email -> (Nothing, Nothing, Just email) + ActivateKey key -> (Just key, Nothing) + ActivateEmail email -> (Nothing, Just email) -- | Information returned as part of a successful activation. data ActivationResponse = ActivationResponse @@ -191,13 +183,11 @@ instance ToSchema ActivationResponse where -------------------------------------------------------------------------------- -- SendActivationCode --- | Payload for a request to (re-)send an activation code --- for a phone number or e-mail address. If a phone is used, --- one can also request a call instead of SMS. +-- | Payload for a request to (re-)send an activation code for an e-mail +-- address. data SendActivationCode = SendActivationCode - { saUserKey :: Either Email Phone, - saLocale :: Maybe Locale, - saCall :: Bool + { emailKey :: EmailAddress, + locale :: Maybe Locale } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform SendActivationCode) @@ -207,37 +197,17 @@ instance ToSchema SendActivationCode where schema = objectWithDocModifier "SendActivationCode" objectDesc $ SendActivationCode - <$> (maybeUserKeyToTuple . saUserKey) .= userKeyObjectSchema - <*> saLocale .= maybe_ (optFieldWithDocModifier "locale" (description ?~ "Locale to use for the activation code template.") schema) - <*> saCall .= (fromMaybe False <$> optFieldWithDocModifier "voice_call" (description ?~ "Request the code with a call instead (default is SMS).") schema) + <$> emailKey .= field "email" schema + <*> locale + .= maybe_ + ( optFieldWithDocModifier + "locale" + ( description ?~ "Locale to use for the activation code template." + ) + schema + ) where - maybeUserKeyToTuple :: Either Email Phone -> (Maybe Email, Maybe Phone) - maybeUserKeyToTuple = \case - Left email -> (Just email, Nothing) - Right phone -> (Nothing, Just phone) - objectDesc :: NamedSwaggerDoc -> NamedSwaggerDoc objectDesc = description - ?~ "Data for requesting an email or phone activation code to be sent. \ - \One of 'email' or 'phone' must be present." - - userKeyObjectSchema :: ObjectSchemaP SwaggerDoc (Maybe Email, Maybe Phone) (Either Email Phone) - userKeyObjectSchema = - withParser userKeyTupleObjectSchema maybeUserKeyFromTuple - where - userKeyTupleObjectSchema :: ObjectSchema SwaggerDoc (Maybe Email, Maybe Phone) - userKeyTupleObjectSchema = - (,) - <$> fst .= maybe_ (optFieldWithDocModifier "email" phoneDocs schema) - <*> snd .= maybe_ (optFieldWithDocModifier "phone" emailDocs schema) - where - emailDocs = description ?~ "Email address to send the code to." - phoneDocs = description ?~ "E.164 phone number to send the code to." - - maybeUserKeyFromTuple :: (Maybe Email, Maybe Phone) -> Parser (Either Email Phone) - maybeUserKeyFromTuple = \case - (Just _, Just _) -> fail "Only one of 'email' or 'phone' allowed." - (Just email, Nothing) -> pure $ Left email - (Nothing, Just phone) -> pure $ Right phone - (Nothing, Nothing) -> fail "One of 'email' or 'phone' required." + ?~ "Data for requesting an email code to be sent. 'email' must be present." diff --git a/libs/wire-api/src/Wire/API/User/Auth.hs b/libs/wire-api/src/Wire/API/User/Auth.hs index ad49c8be0b8..e395fece0e6 100644 --- a/libs/wire-api/src/Wire/API/User/Auth.hs +++ b/libs/wire-api/src/Wire/API/User/Auth.hs @@ -1,6 +1,5 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE StrictData #-} -{-# LANGUAGE TemplateHaskell #-} -- This file is part of the Wire Server implementation. -- @@ -22,9 +21,6 @@ module Wire.API.User.Auth ( -- * Login Login (..), - PasswordLoginData (..), - SmsLoginData (..), - loginLabel, LoginCode (..), LoginId (..), PendingLoginCode (..), @@ -63,7 +59,6 @@ where import Cassandra import Control.Applicative import Control.Lens ((?~), (^.)) -import Control.Lens.TH import Data.Aeson (FromJSON, ToJSON) import Data.Aeson.Types qualified as A import Data.Bifunctor @@ -91,46 +86,42 @@ import Imports import Servant import Web.Cookie import Wire.API.Routes.MultiVerb -import Wire.API.User.Identity (Email, Phone) +import Wire.API.User.Identity (EmailAddress, Phone) import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) -------------------------------------------------------------------------------- -- LoginId +-- | The login ID for client API versions v0..v5 data LoginId - = LoginByEmail Email - | LoginByPhone Phone + = LoginByEmail EmailAddress | LoginByHandle Handle deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform LoginId) deriving (FromJSON, ToJSON, S.ToSchema) via Schema LoginId --- NB. this should fail if (e.g.) the email is present but unparseable even if the JSON contains a valid phone number or handle. --- See tests in `Test.Wire.API.User.Auth`. +-- NB. this should fail if (e.g.) the email is present but unparseable even if +-- the JSON contains a valid handle. instance ToSchema LoginId where - schema = object "LoginId" $ loginObjectSchema + schema = object "LoginId" loginObjectSchema loginObjectSchema :: ObjectSchema SwaggerDoc LoginId loginObjectSchema = fromLoginId .= tupleSchema `withParser` validate where - fromLoginId :: LoginId -> (Maybe Email, Maybe Phone, Maybe Handle) + fromLoginId :: LoginId -> (Maybe EmailAddress, Maybe Handle) fromLoginId = \case - LoginByEmail e -> (Just e, Nothing, Nothing) - LoginByPhone p -> (Nothing, Just p, Nothing) - LoginByHandle h -> (Nothing, Nothing, Just h) - tupleSchema :: ObjectSchema SwaggerDoc (Maybe Email, Maybe Phone, Maybe Handle) + LoginByEmail e -> (Just e, Nothing) + LoginByHandle h -> (Nothing, Just h) + tupleSchema :: ObjectSchema SwaggerDoc (Maybe EmailAddress, Maybe Handle) tupleSchema = - (,,) - <$> fst3 .= maybe_ (optField "email" schema) - <*> snd3 .= maybe_ (optField "phone" schema) - <*> thd3 .= maybe_ (optField "handle" schema) - validate :: (Maybe Email, Maybe Phone, Maybe Handle) -> A.Parser LoginId - validate (mEmail, mPhone, mHandle) = - maybe (fail "'email', 'phone' or 'handle' required") pure $ - (LoginByEmail <$> mEmail) - <|> (LoginByPhone <$> mPhone) - <|> (LoginByHandle <$> mHandle) + (,) + <$> fst .= maybe_ (optField "email" schema) + <*> snd .= maybe_ (optField "handle" schema) + validate :: (Maybe EmailAddress, Maybe Handle) -> A.Parser LoginId + validate (mEmail, mHandle) = + maybe (fail "'email' or 'handle' required") pure $ + (LoginByEmail <$> mEmail) <|> (LoginByHandle <$> mHandle) -------------------------------------------------------------------------------- -- LoginCode @@ -336,69 +327,24 @@ toUnitCookie c = c {cookieValue = ()} -------------------------------------------------------------------------------- -- Login --- | Different kinds of logins. -data Login - = PasswordLogin PasswordLoginData - | SmsLogin SmsLoginData - deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform Login) - -data PasswordLoginData = PasswordLoginData - { plId :: LoginId, - plPassword :: PlainTextPassword6, - plLabel :: Maybe CookieLabel, - plCode :: Maybe Code.Value - } - deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform PasswordLoginData) - -passwordLoginSchema :: ObjectSchema SwaggerDoc PasswordLoginData -passwordLoginSchema = - PasswordLoginData - <$> plId .= loginObjectSchema - <*> plPassword .= field "password" schema - <*> plLabel .= optField "label" (maybeWithDefault A.Null schema) - <*> plCode .= optField "verification_code" (maybeWithDefault A.Null schema) - -data SmsLoginData = SmsLoginData - { slPhone :: Phone, - slCode :: LoginCode, - slLabel :: Maybe CookieLabel +data Login = MkLogin + { lId :: LoginId, + lPassword :: PlainTextPassword6, + lLabel :: Maybe CookieLabel, + lCode :: Maybe Code.Value } deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform SmsLoginData) - -smsLoginSchema :: ObjectSchema SwaggerDoc SmsLoginData -smsLoginSchema = - SmsLoginData - <$> slPhone .= field "phone" schema - <*> slCode .= field "code" schema - <*> slLabel - .= optFieldWithDocModifier - "label" - ( description - ?~ "This label can be used to delete all cookies matching it\ - \ (cf. /cookies/remove)" - ) - (maybeWithDefault A.Null schema) - -$(makePrisms ''Login) + deriving (Arbitrary) via (GenericUniform Login) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema Login) instance ToSchema Login where schema = object "Login" $ - tag _PasswordLogin passwordLoginSchema - <> tag _SmsLogin smsLoginSchema - -deriving via Schema Login instance FromJSON Login - -deriving via Schema Login instance ToJSON Login - -deriving via Schema Login instance S.ToSchema Login - -loginLabel :: Login -> Maybe CookieLabel -loginLabel (PasswordLogin pl) = plLabel pl -loginLabel (SmsLogin sl) = slLabel sl + MkLogin + <$> lId .= loginObjectSchema + <*> lPassword .= field "password" schema + <*> lLabel .= optField "label" (maybeWithDefault A.Null schema) + <*> lCode .= optField "verification_code" (maybeWithDefault A.Null schema) -------------------------------------------------------------------------------- -- RemoveCookies diff --git a/libs/wire-api/src/Wire/API/User/Client.hs b/libs/wire-api/src/Wire/API/User/Client.hs index 35bbde4892d..683417c0d6b 100644 --- a/libs/wire-api/src/Wire/API/User/Client.hs +++ b/libs/wire-api/src/Wire/API/User/Client.hs @@ -36,7 +36,6 @@ module Wire.API.User.Client mkQualifiedUserClientPrekeyMap, qualifiedUserClientPrekeyMapFromList, UserClientsFull (..), - userClientsFullToUserClients, UserClients (..), mkUserClients, QualifiedUserClients (..), @@ -89,7 +88,6 @@ import Data.Qualified import Data.SOP hiding (fn) import Data.Schema import Data.Set qualified as Set -import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.Time.Clock import Data.UUID (toASCIIBytes) @@ -148,6 +146,7 @@ data ClientCapability = -- | Clients have minimum support for LH, but not for explicit consent. Implicit consent -- is granted via the galley server config and cassandra table `galley.legalhold_whitelisted`. ClientSupportsLegalholdImplicitConsent + | ClientSupportsConsumableNotifications deriving stock (Eq, Ord, Bounded, Enum, Show, Generic) deriving (Arbitrary) via (GenericUniform ClientCapability) deriving (ToJSON, FromJSON, Swagger.ToSchema) via Schema ClientCapability @@ -156,14 +155,17 @@ instance ToSchema ClientCapability where schema = enum @Text "ClientCapability" $ element "legalhold-implicit-consent" ClientSupportsLegalholdImplicitConsent + <> element "consumable-notifications" ClientSupportsConsumableNotifications instance C.Cql ClientCapability where ctype = C.Tagged C.IntColumn toCql ClientSupportsLegalholdImplicitConsent = C.CqlInt 1 + toCql ClientSupportsConsumableNotifications = C.CqlInt 2 fromCql (C.CqlInt i) = case i of 1 -> pure ClientSupportsLegalholdImplicitConsent + 2 -> pure ClientSupportsConsumableNotifications n -> Left $ "Unexpected ClientCapability value: " ++ show n fromCql _ = Left "ClientCapability value: int expected" @@ -175,21 +177,27 @@ newtype ClientCapabilityList = ClientCapabilityList {fromClientCapabilityList :: deriving (ToJSON, FromJSON, Swagger.ToSchema) via (Schema ClientCapabilityList) instance ToSchema ClientCapabilityList where + schema = capabilitiesSchema Nothing + +instance ToSchema (Versioned V6 ClientCapabilityList) where schema = - object "ClientCapabilityList" $ - ClientCapabilityList <$> fromClientCapabilityList .= fmap runIdentity capabilitiesFieldSchema - -capabilitiesFieldSchema :: - (FieldFunctor SwaggerDoc f) => - ObjectSchemaP SwaggerDoc (Set ClientCapability) (f (Set ClientCapability)) -capabilitiesFieldSchema = - Set.toList - .= fieldWithDocModifierF "capabilities" mods (Set.fromList <$> array schema) + object "ClientCapabilityListV6" $ + Versioned + <$> unVersioned .= field "capabilities" (capabilitiesSchema (Just V6)) + +capabilitiesSchema :: + Maybe Version -> + ValueSchema NamedSwaggerDoc ClientCapabilityList +capabilitiesSchema mVersion = + named "ClientCapabilityList" $ + ClientCapabilityList + <$> (Set.toList . dropIncompatibleCapabilities . fromClientCapabilityList) .= (Set.fromList <$> array schema) where - mods = - description - ?~ "Hints provided by the client for the backend so it can \ - \behave in a backwards-compatible way." + dropIncompatibleCapabilities :: Set ClientCapability -> Set ClientCapability + dropIncompatibleCapabilities caps = + case mVersion of + Just v | v <= V6 -> Set.delete ClientSupportsConsumableNotifications caps + _ -> caps -------------------------------------------------------------------------------- -- UserClientMap @@ -394,9 +402,6 @@ instance FromJSON UserClientsFull where instance Arbitrary UserClientsFull where arbitrary = UserClientsFull <$> mapOf' arbitrary (setOf' arbitrary) -userClientsFullToUserClients :: UserClientsFull -> UserClients -userClientsFullToUserClients (UserClientsFull mp) = UserClients $ Set.map clientId <$> mp - newtype UserClients = UserClients { userClients :: Map UserId (Set ClientId) } @@ -505,8 +510,8 @@ mlsPublicKeysSchema = mapSchema = map_ base64Schema clientSchema :: Maybe Version -> ValueSchema NamedSwaggerDoc Client -clientSchema mv = - object ("Client" <> T.pack (foldMap show mv)) $ +clientSchema mVersion = + object "Client" $ Client <$> clientId .= field "id" schema <*> clientType .= field "type" schema @@ -515,27 +520,29 @@ clientSchema mv = <*> clientLabel .= maybe_ (optField "label" schema) <*> clientCookie .= maybe_ (optField "cookie" schema) <*> clientModel .= maybe_ (optField "model" schema) - <*> clientCapabilities .= (fromMaybe mempty <$> caps) + <*> clientCapabilities .= (fromMaybe mempty <$> optField "capabilities" caps) <*> clientMLSPublicKeys .= mlsPublicKeysFieldSchema <*> clientLastActive .= maybe_ (optField "last_active" utcTimeSchema) where - caps :: ObjectSchemaP SwaggerDoc ClientCapabilityList (Maybe ClientCapabilityList) - caps = case mv of + caps :: ValueSchema NamedSwaggerDoc ClientCapabilityList + caps = case mVersion of -- broken capability serialisation for backwards compatibility - Just v | v <= V5 -> optField "capabilities" schema - _ -> fmap ClientCapabilityList <$> fromClientCapabilityList .= capabilitiesFieldSchema + Just v + | v <= V6 -> + dimap Versioned unVersioned $ schema @(Versioned V6 ClientCapabilityList) + _ -> schema @ClientCapabilityList instance ToSchema Client where schema = clientSchema Nothing -instance ToSchema (Versioned 'V5 Client) where - schema = Versioned <$> unVersioned .= clientSchema (Just V5) +instance ToSchema (Versioned 'V6 Client) where + schema = Versioned <$> unVersioned .= clientSchema (Just V6) -instance {-# OVERLAPPING #-} ToSchema (Versioned 'V5 [Client]) where +instance {-# OVERLAPPING #-} ToSchema (Versioned 'V6 [Client]) where schema = Versioned <$> unVersioned - .= named "ClientList" (array (clientSchema (Just V5))) + .= named "ClientList" (array (clientSchema (Just V6))) mlsPublicKeysFieldSchema :: ObjectSchema SwaggerDoc MLSPublicKeys mlsPublicKeysFieldSchema = fromMaybe mempty <$> optField "mls_public_keys" mlsPublicKeysSchema @@ -667,7 +674,7 @@ data NewClient = NewClient newClientCookie :: Maybe CookieLabel, newClientPassword :: Maybe PlainTextPassword6, newClientModel :: Maybe Text, - newClientCapabilities :: Maybe (Set ClientCapability), + newClientCapabilities :: Maybe ClientCapabilityList, newClientMLSPublicKeys :: MLSPublicKeys, newClientVerificationCode :: Maybe Code.Value } @@ -731,7 +738,16 @@ instance ToSchema NewClient where schema ) <*> newClientModel .= maybe_ (optField "model" schema) - <*> newClientCapabilities .= maybe_ capabilitiesFieldSchema + <*> newClientCapabilities + .= maybe_ + ( optFieldWithDocModifier + "capabilities" + ( description + ?~ "Hints provided by the client for the backend so it can \ + \behave in a backwards-compatible way." + ) + schema + ) <*> newClientMLSPublicKeys .= mlsPublicKeysFieldSchema <*> newClientVerificationCode .= maybe_ (optField "verification_code" schema) @@ -759,7 +775,7 @@ data UpdateClient = UpdateClient updateClientLastKey :: Maybe LastPrekey, updateClientLabel :: Maybe Text, -- | see haddocks for 'ClientCapability' - updateClientCapabilities :: Maybe (Set ClientCapability), + updateClientCapabilities :: Maybe ClientCapabilityList, updateClientMLSPublicKeys :: MLSPublicKeys } deriving stock (Eq, Show, Generic) @@ -801,7 +817,13 @@ instance ToSchema UpdateClient where (description ?~ "A new name for this client.") schema ) - <*> updateClientCapabilities .= maybe_ capabilitiesFieldSchema + <*> updateClientCapabilities + .= maybe_ + ( optFieldWithDocModifier + "capabilities" + (description ?~ "Hints provided by the client for the backend so it can behave in a backwards-compatible way.") + schema + ) <*> updateClientMLSPublicKeys .= mlsPublicKeysFieldSchema -------------------------------------------------------------------------------- diff --git a/libs/wire-api/src/Wire/API/User/EmailAddress.hs b/libs/wire-api/src/Wire/API/User/EmailAddress.hs new file mode 100644 index 00000000000..ffde490b59e --- /dev/null +++ b/libs/wire-api/src/Wire/API/User/EmailAddress.hs @@ -0,0 +1,122 @@ +{-# OPTIONS_GHC -Wno-orphans #-} + +module Wire.API.User.EmailAddress + ( fromEmail, + emailAddress, + emailAddressText, + module Text.Email.Parser, + emailToSAMLNameID, + emailFromSAML, + ) +where + +----- +-- This is where we declare orphan instances +----- + +import Cassandra.CQL qualified as C +import Data.ByteString.Conversion hiding (toByteString) +import Data.Data (Proxy (..)) +import Data.OpenApi hiding (Schema, ToSchema) +import Data.Schema +import Data.Text hiding (null) +import Data.Text.Encoding +import Data.Text.Encoding.Error +import Deriving.Aeson +import Imports +import SAML2.WebSSO.Types qualified as SAML +import SAML2.WebSSO.Types.Email qualified as SAMLEmail +import Servant.API qualified as S +import Test.QuickCheck +import Text.Email.Parser +import Text.Email.Validate + +-------------------------------------------------------------------------------- +-- Email + +instance ToByteString EmailAddress where + builder = builder . fromEmail + +instance FromByteString EmailAddress where + parser = parser >>= maybe (fail "Invalid email") pure . emailAddress + +deriving via (Schema EmailAddress) instance ToJSON EmailAddress + +deriving via (Schema EmailAddress) instance FromJSON EmailAddress + +instance ToParamSchema EmailAddress where + toParamSchema _ = toParamSchema (Proxy @Text) + +instance ToSchema EmailAddress where + schema = + fromEmail + .= parsedText + "Email" + ( maybe + (Left "Invalid email. Expected '@'.") + pure + . emailAddressText + ) + +instance S.FromHttpApiData EmailAddress where + parseUrlPiece = maybe (Left "Invalid email") Right . fromByteString . encodeUtf8 + +instance S.ToHttpApiData EmailAddress where + toUrlPiece = decodeUtf8With lenientDecode . toByteString' + +instance Arbitrary EmailAddress where + -- By generating arbitrary Text and then encoding as bytestrings + -- we avoid the risk of generating invalid UTF-8 bytes. + arbitrary = arbitraryValidMail + +-- loc <- fromString <$> listOf1 arbitraryMailString +-- dom <- fromString <$> listOf1 arbitraryMailString +-- pure $ unsafeEmailAddress loc dom + +instance C.Cql EmailAddress where + ctype = C.Tagged C.TextColumn + + fromCql (C.CqlText t) = case emailAddressText t of + Just e -> pure e + Nothing -> Left "fromCql: Invalid email" + fromCql _ = Left "fromCql: email: CqlText expected" + + toCql = C.toCql . fromEmail + +fromEmail :: EmailAddress -> Text +fromEmail = decodeUtf8 . toByteString + +emailAddressText :: Text -> Maybe EmailAddress +emailAddressText = emailAddress . encodeUtf8 + +-- | Generates any Unicode character (but not a surrogate) +arbitraryValidMail :: Gen EmailAddress +arbitraryValidMail = do + loc <- arbitrary `suchThat` isValidLoc + dom <- arbitrary `suchThat` isValidDom + pure . fromJust $ emailAddress (fromString $ loc <> "@" <> dom) + where + notAt :: String -> Bool + notAt = notElem '@' + + notNull = not . null + + isValidLoc :: String -> Bool + isValidLoc x = + notNull x + && notAt x + && isValid (fromString (x <> "@mail.com")) + + isValidDom :: String -> Bool + isValidDom x = + notNull x + && notAt x + && isValid (fromString ("me@" <> x)) + +-- | FUTUREWORK(fisx): if saml2-web-sso exported the 'NameID' constructor, we could make this +-- function total without all that praying and hoping. +emailToSAMLNameID :: EmailAddress -> Either String SAML.NameID +emailToSAMLNameID = SAML.emailNameID . fromEmail + +emailFromSAML :: SAMLEmail.Email -> EmailAddress +emailFromSAML = fromJust . emailAddressText . SAMLEmail.render diff --git a/libs/wire-api/src/Wire/API/User/Identity.hs b/libs/wire-api/src/Wire/API/User/Identity.hs index b96ad4135fa..65b6a5ede61 100644 --- a/libs/wire-api/src/Wire/API/User/Identity.hs +++ b/libs/wire-api/src/Wire/API/User/Identity.hs @@ -30,42 +30,29 @@ module Wire.API.User.Identity maybeUserIdentityObjectSchema, maybeUserIdentityFromComponents, - -- * Email - Email (..), - fromEmail, - parseEmail, - validateEmail, - -- * Phone Phone (..), parsePhone, isValidPhone, + -- * Email + module Wire.API.User.EmailAddress, + -- * UserSSOId UserSSOId (..), - emailFromSAML, - emailToSAML, - emailToSAMLNameID, - emailFromSAMLNameID, mkSampleUref, mkSimpleSampleUref, ) where import Cassandra qualified as C -import Control.Applicative (optional) import Control.Error (hush) -import Control.Lens (dimap, over, (.~), (?~), (^.)) +import Control.Lens (dimap, (.~), (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A import Data.Aeson.Types qualified as A -import Data.Attoparsec.Text -import Data.Bifunctor (first) import Data.ByteString (fromStrict, toStrict) -import Data.ByteString.Conversion import Data.ByteString.UTF8 qualified as UTF8 -import Data.CaseInsensitive qualified as CI -import Data.OpenApi (ToParamSchema (..)) import Data.OpenApi qualified as S import Data.Schema import Data.Text qualified as Text @@ -76,17 +63,17 @@ import Imports import SAML2.WebSSO (UserRef (..)) import SAML2.WebSSO.Test.Arbitrary () import SAML2.WebSSO.Types qualified as SAML -import SAML2.WebSSO.Types.Email qualified as SAMLEmail import SAML2.WebSSO.XML qualified as SAML import Servant -import Servant.API qualified as S import System.FilePath (()) -import Test.QuickCheck qualified as QC -import Text.Email.Validate qualified as Email.V +import Text.Email.Parser import URI.ByteString qualified as URI import URI.ByteString.QQ (uri) +import Web.Scim.Schema.User.Email () +import Wire.API.User.EmailAddress +import Wire.API.User.Phone import Wire.API.User.Profile (fromName, mkName) -import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) +import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -------------------------------------------------------------------------------- -- UserIdentity @@ -94,9 +81,9 @@ import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) -- | The private unique user identity that is used for login and -- account recovery. data UserIdentity - = EmailIdentity Email - | SSOIdentity UserSSOId (Maybe Email) - deriving stock (Eq, Show, Generic) + = EmailIdentity EmailAddress + | SSOIdentity UserSSOId (Maybe EmailAddress) + deriving stock (Eq, Ord, Show, Generic) deriving (Arbitrary) via (GenericUniform UserIdentity) isSSOIdentity :: UserIdentity -> Bool @@ -111,7 +98,7 @@ maybeUserIdentityObjectSchema :: ObjectSchema SwaggerDoc (Maybe UserIdentity) maybeUserIdentityObjectSchema = dimap maybeUserIdentityToComponents maybeUserIdentityFromComponents userIdentityComponentsObjectSchema -type UserIdentityComponents = (Maybe Email, Maybe UserSSOId) +type UserIdentityComponents = (Maybe EmailAddress, Maybe UserSSOId) userIdentityComponentsObjectSchema :: ObjectSchema SwaggerDoc UserIdentityComponents userIdentityComponentsObjectSchema = @@ -130,12 +117,12 @@ maybeUserIdentityToComponents Nothing = (Nothing, Nothing) maybeUserIdentityToComponents (Just (EmailIdentity email)) = (Just email, Nothing) maybeUserIdentityToComponents (Just (SSOIdentity ssoid m_email)) = (m_email, Just ssoid) -newIdentity :: Maybe Email -> Maybe UserSSOId -> Maybe UserIdentity +newIdentity :: Maybe EmailAddress -> Maybe UserSSOId -> Maybe UserIdentity newIdentity email (Just sso) = Just $! SSOIdentity sso email newIdentity (Just e) Nothing = Just $! EmailIdentity e newIdentity Nothing Nothing = Nothing -emailIdentity :: UserIdentity -> Maybe Email +emailIdentity :: UserIdentity -> Maybe EmailAddress emailIdentity (EmailIdentity email) = Just email emailIdentity (SSOIdentity _ (Just email)) = Just email emailIdentity (SSOIdentity _ _) = Nothing @@ -144,164 +131,6 @@ ssoIdentity :: UserIdentity -> Maybe UserSSOId ssoIdentity (SSOIdentity ssoid _) = Just ssoid ssoIdentity _ = Nothing --------------------------------------------------------------------------------- --- Email - --- FUTUREWORK: replace this type with 'EmailAddress' -data Email = Email - { emailLocal :: Text, - emailDomain :: Text - } - deriving stock (Eq, Ord, Generic) - deriving (FromJSON, ToJSON, S.ToSchema) via Schema Email - -instance ToParamSchema Email where - toParamSchema _ = toParamSchema (Proxy @Text) - -instance ToSchema Email where - schema = - fromEmail - .= parsedText - "Email" - ( maybe - (Left "Invalid email. Expected '@'.") - pure - . parseEmail - ) - -instance Show Email where - show = Text.unpack . fromEmail - -instance ToByteString Email where - builder = builder . fromEmail - -instance FromByteString Email where - parser = parser >>= maybe (fail "Invalid email") pure . parseEmail - -instance S.FromHttpApiData Email where - parseUrlPiece = maybe (Left "Invalid email") Right . fromByteString . encodeUtf8 - -instance S.ToHttpApiData Email where - toUrlPiece = decodeUtf8With lenientDecode . toByteString' - -instance Arbitrary Email where - arbitrary = do - localPart <- Text.filter (/= '@') <$> arbitrary - domain <- Text.filter (/= '@') <$> arbitrary - pure $ Email localPart domain - -instance C.Cql Email where - ctype = C.Tagged C.TextColumn - - fromCql (C.CqlText t) = case parseEmail t of - Just e -> pure e - Nothing -> Left "fromCql: Invalid email" - fromCql _ = Left "fromCql: email: CqlText expected" - - toCql = C.toCql . fromEmail - -fromEmail :: Email -> Text -fromEmail (Email loc dom) = loc <> "@" <> dom - --- | Parses an email address of the form @. -parseEmail :: Text -> Maybe Email -parseEmail t = case Text.split (== '@') t of - [localPart, domain] -> Just $! Email localPart domain - _ -> Nothing - --- | --- FUTUREWORK: --- --- * Enforce these constrains during parsing already or use a separate type, see --- [Parse, don't validate](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate). --- --- * Check for differences to validation of `Data.Domain.Domain` and decide whether to --- align/de-duplicate the two. --- --- * Drop dependency on email-validate? We do our own email domain validation anyways, --- is the dependency worth it just for validating the local part? -validateEmail :: Email -> Either String Email -validateEmail = - pure - . uncurry Email - <=< validateDomain - <=< validateExternalLib - <=< validateLength - . fromEmail - where - validateLength e - | len <= 100 = Right e - | otherwise = Left $ "length " <> show len <> " exceeds 100" - where - len = Text.length e - validateExternalLib e = do - email <- Email.V.validate $ encodeUtf8 e - l <- first show . decodeUtf8' $ Email.V.localPart email - d <- first show . decodeUtf8' $ Email.V.domainPart email - pure (l, d) - -- cf. https://en.wikipedia.org/wiki/Email_address#Domain - -- n.b. We do not allow IP address literals, comments or non-ASCII - -- characters, mostly because SES (and probably many other mail - -- systems) don't support that (yet?) either. - validateDomain (l, d) = parseOnly domain d - where - domain = (label *> many1 (char '.' *> label) *> endOfInput) $> (l, d) - label = - satisfy (inClass "a-zA-Z0-9") - *> count 61 (optional (satisfy (inClass "-a-zA-Z0-9"))) - *> optional (satisfy (inClass "a-zA-Z0-9")) - --------------------------------------------------------------------------------- --- Phone - -newtype Phone = Phone {fromPhone :: Text} - deriving stock (Eq, Ord, Show, Generic) - deriving (ToJSON, FromJSON, S.ToSchema) via (Schema Phone) - -instance ToParamSchema Phone where - toParamSchema _ = toParamSchema (Proxy @Text) - -instance ToSchema Phone where - schema = - over doc (S.description ?~ "E.164 phone number") $ - fromPhone - .= parsedText "PhoneNumber" (maybe (Left "Invalid phone number. Expected E.164 format.") Right . parsePhone) - -instance ToByteString Phone where - builder = builder . fromPhone - -instance FromByteString Phone where - parser = parser >>= maybe (fail "Invalid phone") pure . parsePhone - -instance S.FromHttpApiData Phone where - parseUrlPiece = maybe (Left "Invalid phone") Right . fromByteString . encodeUtf8 - -instance S.ToHttpApiData Phone where - toUrlPiece = decodeUtf8With lenientDecode . toByteString' - -instance Arbitrary Phone where - arbitrary = - Phone . Text.pack <$> do - let mkdigits n = replicateM n (QC.elements ['0' .. '9']) - mini <- mkdigits 8 - maxi <- mkdigits =<< QC.chooseInt (0, 7) - pure $ '+' : mini <> maxi - -deriving instance C.Cql Phone - --- | Parses a phone number in E.164 format with a mandatory leading '+'. -parsePhone :: Text -> Maybe Phone -parsePhone p - | isValidPhone p = Just $! Phone p - | otherwise = Nothing - --- | Checks whether a phone number is valid, i.e. it is in E.164 format --- with a mandatory leading '+' followed by 10-15 digits. -isValidPhone :: Text -> Bool -isValidPhone = either (const False) (const True) . parseOnly e164 - where - e164 = char '+' *> count 8 digit *> count 7 (optional digit) *> endOfInput - -------------------------------------------------------------------------------- -- UserSSOId @@ -402,8 +231,8 @@ lenientlyParseSAMLNameID (Just txt) = do asemail = maybe (Left "not an email") - (fmap emailToSAMLNameID . validateEmail) - (parseEmail . LT.toStrict $ txt) + emailToSAMLNameID + (emailAddressText . LT.toStrict $ txt) astxt :: Either String SAML.NameID astxt = do @@ -418,22 +247,6 @@ lenientlyParseSAMLNameID (Just txt) = do (pure . Just) (hush asxml <|> hush asemail <|> hush astxt) -emailFromSAML :: (HasCallStack) => SAMLEmail.Email -> Email -emailFromSAML = fromJust . parseEmail . SAMLEmail.render - -emailToSAML :: (HasCallStack) => Email -> SAMLEmail.Email -emailToSAML = CI.original . fromRight (error "emailToSAML") . SAMLEmail.validate . toByteString - --- | FUTUREWORK(fisx): if saml2-web-sso exported the 'NameID' constructor, we could make this --- function total without all that praying and hoping. -emailToSAMLNameID :: (HasCallStack) => Email -> SAML.NameID -emailToSAMLNameID = fromRight (error "impossible") . SAML.emailNameID . fromEmail - -emailFromSAMLNameID :: (HasCallStack) => SAML.NameID -> Maybe Email -emailFromSAMLNameID nid = case nid ^. SAML.nameID of - SAML.UNameIDEmail email -> Just . emailFromSAML . CI.original $ email - _ -> Nothing - -- | For testing. Create a sample 'SAML.UserRef' value with random seeds to make 'Issuer' and -- 'NameID' unique. FUTUREWORK: move to saml2-web-sso. mkSampleUref :: Text -> Text -> SAML.UserRef diff --git a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs index 9100be731e2..7283333f7a2 100644 --- a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs +++ b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs @@ -132,13 +132,9 @@ instance Cql.Cql WireIdPAPIVersion where -- | A list of 'IdP's, returned by some endpoints. Wrapped into an object to -- allow extensibility later on. -data IdPList = IdPList - { _providers :: [IdP] - } +newtype IdPList = IdPList {providers :: [IdP]} deriving (Eq, Show, Generic) -makeLenses ''IdPList - -- Same as WireIdP, we want the lenses, so we have to drop a prefix deriveJSON (defaultOptsDropChar '_') ''IdPList diff --git a/libs/wire-api/src/Wire/API/User/Orphans.hs b/libs/wire-api/src/Wire/API/User/Orphans.hs index 0f019fdc1f9..316889c115a 100644 --- a/libs/wire-api/src/Wire/API/User/Orphans.hs +++ b/libs/wire-api/src/Wire/API/User/Orphans.hs @@ -103,7 +103,11 @@ instance ToSchema (SAML.FormRedirect SAML.AuthnRequest) where & properties . at "xml" ?~ authnReqSchema instance ToSchema (SAML.ID SAML.AuthnRequest) where - declareNamedSchema = genericDeclareNamedSchema samlSchemaOptions + declareNamedSchema = + genericDeclareNamedSchema + samlSchemaOptions + { datatypeNameModifier = const "Id_AuthnRequest" + } instance ToSchema SAML.Time where declareNamedSchema = genericDeclareNamedSchema samlSchemaOptions diff --git a/libs/wire-api/src/Wire/API/User/Password.hs b/libs/wire-api/src/Wire/API/User/Password.hs index f3955f3cd4f..33ad254da73 100644 --- a/libs/wire-api/src/Wire/API/User/Password.hs +++ b/libs/wire-api/src/Wire/API/User/Password.hs @@ -51,7 +51,8 @@ import Data.Text.Ascii import Data.Tuple.Extra import Imports import Servant (FromHttpApiData (..)) -import Wire.API.User.Identity +import Wire.API.User.EmailAddress +import Wire.API.User.Phone import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -------------------------------------------------------------------------------- @@ -59,7 +60,7 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -- | The payload for initiating a password reset. data NewPasswordReset - = NewPasswordReset Email + = NewPasswordReset EmailAddress | -- | Resetting via phone is not really supported anymore, but this is still -- here to support older versions of the endpoint. NewPasswordResetUnsupportedPhone @@ -75,7 +76,7 @@ instance ToSchema NewPasswordReset where objectDesc :: NamedSwaggerDoc -> NamedSwaggerDoc objectDesc = description ?~ "Data to initiate a password reset" - newPasswordResetTupleObjectSchema :: ObjectSchema SwaggerDoc (Maybe Email, Maybe Text) + newPasswordResetTupleObjectSchema :: ObjectSchema SwaggerDoc (Maybe EmailAddress, Maybe Text) newPasswordResetTupleObjectSchema = (,) <$> fst .= maybe_ (optFieldWithDocModifier "email" phoneDocs schema) @@ -87,14 +88,14 @@ instance ToSchema NewPasswordReset where phoneDocs :: NamedSwaggerDoc -> NamedSwaggerDoc phoneDocs = description ?~ "Phone" - fromTuple :: (Maybe Email, Maybe a) -> Parser NewPasswordReset + fromTuple :: (Maybe EmailAddress, Maybe a) -> Parser NewPasswordReset fromTuple = \case (Just _, Just _) -> fail "Only one of 'email' or 'phone' allowed." (Just email, Nothing) -> pure $ NewPasswordReset email (Nothing, Just _) -> pure NewPasswordResetUnsupportedPhone (Nothing, Nothing) -> fail "One of 'email' or 'phone' required." - toTuple :: NewPasswordReset -> (Maybe Email, Maybe Text) + toTuple :: NewPasswordReset -> (Maybe EmailAddress, Maybe Text) toTuple = \case NewPasswordReset e -> (Just e, Nothing) NewPasswordResetUnsupportedPhone -> (Nothing, Just "") @@ -129,11 +130,11 @@ instance ToSchema CompletePasswordReset where pwDocs :: NamedSwaggerDoc -> NamedSwaggerDoc pwDocs = description ?~ "New password (6 - 1024 characters)" - maybePasswordResetIdentityObjectSchema :: ObjectSchemaP SwaggerDoc (Maybe PasswordResetKey, Maybe Email, Maybe Phone) PasswordResetIdentity + maybePasswordResetIdentityObjectSchema :: ObjectSchemaP SwaggerDoc (Maybe PasswordResetKey, Maybe EmailAddress, Maybe Phone) PasswordResetIdentity maybePasswordResetIdentityObjectSchema = withParser passwordResetIdentityTupleObjectSchema maybePasswordResetIdentityTargetFromTuple where - passwordResetIdentityTupleObjectSchema :: ObjectSchema SwaggerDoc (Maybe PasswordResetKey, Maybe Email, Maybe Phone) + passwordResetIdentityTupleObjectSchema :: ObjectSchema SwaggerDoc (Maybe PasswordResetKey, Maybe EmailAddress, Maybe Phone) passwordResetIdentityTupleObjectSchema = (,,) <$> fst3 .= maybe_ (optFieldWithDocModifier "key" keyDocs schema) @@ -144,14 +145,14 @@ instance ToSchema CompletePasswordReset where emailDocs = description ?~ "A known email with a pending password reset." phoneDocs = description ?~ "A known phone number with a pending password reset." - maybePasswordResetIdentityTargetFromTuple :: (Maybe PasswordResetKey, Maybe Email, Maybe Phone) -> Parser PasswordResetIdentity + maybePasswordResetIdentityTargetFromTuple :: (Maybe PasswordResetKey, Maybe EmailAddress, Maybe Phone) -> Parser PasswordResetIdentity maybePasswordResetIdentityTargetFromTuple = \case (Just key, _, _) -> pure $ PasswordResetIdentityKey key (_, Just email, _) -> pure $ PasswordResetEmailIdentity email (_, _, Just phone) -> pure $ PasswordResetPhoneIdentity phone _ -> fail "key, email or phone must be present" - maybePasswordResetIdentityToTuple :: PasswordResetIdentity -> (Maybe PasswordResetKey, Maybe Email, Maybe Phone) + maybePasswordResetIdentityToTuple :: PasswordResetIdentity -> (Maybe PasswordResetKey, Maybe EmailAddress, Maybe Phone) maybePasswordResetIdentityToTuple = \case PasswordResetIdentityKey key -> (Just key, Nothing, Nothing) PasswordResetEmailIdentity email -> (Nothing, Just email, Nothing) @@ -165,7 +166,7 @@ data PasswordResetIdentity = -- | An opaque identity key for a pending password reset. PasswordResetIdentityKey PasswordResetKey | -- | A known email address with a pending password reset. - PasswordResetEmailIdentity Email + PasswordResetEmailIdentity EmailAddress | -- | A known phone number with a pending password reset. PasswordResetPhoneIdentity Phone deriving stock (Eq, Show, Generic) diff --git a/libs/wire-api/src/Wire/API/User/Phone.hs b/libs/wire-api/src/Wire/API/User/Phone.hs new file mode 100644 index 00000000000..603103cc18c --- /dev/null +++ b/libs/wire-api/src/Wire/API/User/Phone.hs @@ -0,0 +1,77 @@ +module Wire.API.User.Phone + ( Phone (..), + parsePhone, + isValidPhone, + ) +where + +import Cassandra qualified as C +import Control.Applicative (optional) +import Control.Lens (over, (?~)) +import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.Attoparsec.Text +import Data.ByteString.Conversion +import Data.OpenApi (ToParamSchema (..)) +import Data.OpenApi qualified as S +import Data.Schema +import Data.Text qualified as Text +import Data.Text.Encoding +import Data.Text.Encoding.Error +import Imports +import SAML2.WebSSO.Test.Arbitrary () +import Servant +import Servant.API qualified as S +import Test.QuickCheck qualified as QC +import Web.Scim.Schema.User.Email () +import Wire.Arbitrary (Arbitrary (arbitrary)) + +-------------------------------------------------------------------------------- +-- Phone + +newtype Phone = Phone {fromPhone :: Text} + deriving stock (Eq, Ord, Show, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema Phone) + +instance ToParamSchema Phone where + toParamSchema _ = toParamSchema (Proxy @Text) + +instance ToSchema Phone where + schema = + over doc (S.description ?~ "E.164 phone number") $ + fromPhone + .= parsedText "PhoneNumber" (maybe (Left "Invalid phone number. Expected E.164 format.") Right . parsePhone) + +instance ToByteString Phone where + builder = builder . fromPhone + +instance FromByteString Phone where + parser = parser >>= maybe (fail "Invalid phone") pure . parsePhone + +instance S.FromHttpApiData Phone where + parseUrlPiece = maybe (Left "Invalid phone") Right . fromByteString . encodeUtf8 + +instance S.ToHttpApiData Phone where + toUrlPiece = decodeUtf8With lenientDecode . toByteString' + +instance Arbitrary Phone where + arbitrary = + Phone . Text.pack <$> do + let mkdigits n = replicateM n (QC.elements ['0' .. '9']) + mini <- mkdigits 8 + maxi <- mkdigits =<< QC.chooseInt (0, 7) + pure $ '+' : mini <> maxi + +deriving instance C.Cql Phone + +-- | Parses a phone number in E.164 format with a mandatory leading '+'. +parsePhone :: Text -> Maybe Phone +parsePhone p + | isValidPhone p = Just $! Phone p + | otherwise = Nothing + +-- | Checks whether a phone number is valid, i.e. it is in E.164 format +-- with a mandatory leading '+' followed by 10-15 digits. +isValidPhone :: Text -> Bool +isValidPhone = either (const False) (const True) . parseOnly e164 + where + e164 = char '+' *> count 8 digit *> count 7 (optional digit) *> endOfInput diff --git a/libs/wire-api/src/Wire/API/User/Profile.hs b/libs/wire-api/src/Wire/API/User/Profile.hs index 022c0cc50cf..cb6e3025a78 100644 --- a/libs/wire-api/src/Wire/API/User/Profile.hs +++ b/libs/wire-api/src/Wire/API/User/Profile.hs @@ -21,6 +21,9 @@ module Wire.API.User.Profile ( Name (..), mkName, + TextStatus, + mkTextStatus, + fromTextStatus, ColourId (..), defaultAccentId, @@ -72,6 +75,25 @@ instance ToSchema Name where deriving instance C.Cql Name +-------------------------------------------------------------------------------- +-- TextStatus + +-- Length is between 1 and 256 characters. +newtype TextStatus = TextStatus + {fromTextStatus :: Text} + deriving stock (Eq, Ord, Show, Generic) + deriving newtype (FromByteString, ToByteString) + deriving (Arbitrary) via (Ranged 1 256 Text) + deriving (FromJSON, ToJSON, S.ToSchema) via Schema TextStatus + +mkTextStatus :: Text -> Either String TextStatus +mkTextStatus txt = TextStatus . fromRange <$> checkedEitherMsg @_ @1 @256 "TextStatus" txt + +instance ToSchema TextStatus where + schema = TextStatus <$> fromTextStatus .= untypedRangedSchema 1 256 schema + +deriving instance C.Cql TextStatus + -------------------------------------------------------------------------------- -- Colour diff --git a/libs/wire-api/src/Wire/API/User/Saml.hs b/libs/wire-api/src/Wire/API/User/Saml.hs index fa97f24fb07..4bebb7bf6d0 100644 --- a/libs/wire-api/src/Wire/API/User/Saml.hs +++ b/libs/wire-api/src/Wire/API/User/Saml.hs @@ -24,7 +24,6 @@ -- for them. module Wire.API.User.Saml where -import Control.Lens (makeLenses) import Control.Monad.Except import Data.Aeson hiding (fieldLabelModifier) import Data.Aeson.TH hiding (fieldLabelModifier) @@ -62,8 +61,6 @@ data VerdictFormat | VerdictFormatMobile {_formatGrantedURI :: URI, _formatDeniedURI :: URI} deriving (Eq, Show, Generic) -makeLenses ''VerdictFormat - deriveJSON deriveJSONOptions ''VerdictFormat mkVerdictGrantedFormatMobile :: (MonadError String m) => URI -> SetCookie -> UserId -> m URI diff --git a/libs/wire-api/src/Wire/API/User/Scim.hs b/libs/wire-api/src/Wire/API/User/Scim.hs index e27bfcb26d2..07c07c3beea 100644 --- a/libs/wire-api/src/Wire/API/User/Scim.hs +++ b/libs/wire-api/src/Wire/API/User/Scim.hs @@ -42,7 +42,7 @@ -- * Request and response types for SCIM-related endpoints. module Wire.API.User.Scim where -import Control.Lens (Prism', makeLenses, mapped, prism', (.~), (?~), (^.)) +import Control.Lens (makeLenses, to, (.~), (^.)) import Control.Monad.Except (throwError) import Crypto.Hash (hash) import Crypto.Hash.Algorithms (SHA512) @@ -55,19 +55,23 @@ import Data.ByteString.Conversion (FromByteString (..), ToByteString (..)) import Data.CaseInsensitive qualified as CI import Data.Code as Code import Data.Handle (Handle) -import Data.Id (ScimTokenId, TeamId, UserId) -import Data.Json.Util ((#)) +import Data.Id +import Data.Json.Util import Data.Map qualified as Map import Data.Misc (PlainTextPassword6) -import Data.OpenApi hiding (Operation) -import Data.Proxy +import Data.OpenApi qualified as S +import Data.Schema as Schema import Data.Text qualified as T +import Data.Text qualified as Text import Data.Text.Encoding (decodeUtf8, encodeUtf8) +import Data.These +import Data.These.Combinators import Data.Time.Clock (UTCTime) import Imports import SAML2.WebSSO qualified as SAML import SAML2.WebSSO.Test.Arbitrary () import Servant.API (FromHttpApiData (..), ToHttpApiData (..)) +import Test.QuickCheck (Gen) import Test.QuickCheck qualified as QC import Web.HttpApiData (parseHeaderWithPrefix) import Web.Scim.AttrName (AttrName (..)) @@ -84,9 +88,10 @@ import Web.Scim.Schema.Schema qualified as Scim import Web.Scim.Schema.User qualified as Scim import Web.Scim.Schema.User qualified as Scim.User import Wire.API.Locale +import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.API.Team.Role (Role) -import Wire.API.User (emailFromSAMLNameID, urefToExternalIdUnsafe) -import Wire.API.User.Identity (Email, fromEmail) +import Wire.API.User.EmailAddress (EmailAddress, fromEmail) import Wire.API.User.Profile as BT import Wire.API.User.RichInfo qualified as RI import Wire.API.User.Saml () @@ -112,7 +117,11 @@ userSchemas = -- -- For SCIM authentication and token handling logic, see "Spar.Scim.Auth". newtype ScimToken = ScimToken {fromScimToken :: Text} - deriving (Eq, Ord, Show, FromJSON, ToJSON, FromByteString, ToByteString) + deriving (Eq, Ord, Show, FromByteString, ToByteString, Arbitrary) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema.Schema ScimToken) + +instance ToSchema ScimToken where + schema = ScimToken <$> fromScimToken .= schema newtype ScimTokenHash = ScimTokenHash {fromScimTokenHash :: Text} deriving (Eq, Show) @@ -145,9 +154,13 @@ data ScimTokenInfo = ScimTokenInfo stiIdP :: !(Maybe SAML.IdPId), -- | Free-form token description, can be set -- by the token creator as a mental aid - stiDescr :: !Text + stiDescr :: !Text, + -- | Name for the token, if not set by the user, the name will be equal to the token ID + stiName :: !Text } - deriving (Eq, Show) + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform ScimTokenInfo) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema.Schema ScimTokenInfo) instance FromHttpApiData ScimToken where parseHeader h = ScimToken <$> parseHeaderWithPrefix "Bearer " h @@ -157,29 +170,44 @@ instance ToHttpApiData ScimToken where toHeader (ScimToken s) = "Bearer " <> encodeUtf8 s toQueryParam (ScimToken s) = toQueryParam s -instance FromJSON ScimTokenInfo where - parseJSON = A.withObject "ScimTokenInfo" $ \o -> do - stiTeam <- o A..: "team" - stiId <- o A..: "id" - stiCreatedAt <- o A..: "created_at" - stiIdP <- o A..:? "idp" - stiDescr <- o A..: "description" - pure ScimTokenInfo {..} - -instance ToJSON ScimTokenInfo where - toJSON s = - A.object $ - "team" - A..= stiTeam s - # "id" - A..= stiId s - # "created_at" - A..= stiCreatedAt s - # "idp" - A..= stiIdP s - # "description" - A..= stiDescr s - # [] +instance ToSchema ScimTokenInfo where + schema = + object "ScimTokenInfo" $ + ScimTokenInfo + <$> (.stiTeam) .= field "team" schema + <*> (.stiId) .= field "id" schema + <*> (.stiCreatedAt) .= field "created_at" utcTimeSchema + <*> (fmap SAML.fromIdPId . (.stiIdP)) .= (SAML.IdPId <$$> maybe_ (optField "idp" uuidSchema)) + <*> (.stiDescr) .= field "description" schema + <*> (.stiName) .= field "name" schema + +-- | Metadata that we store about each token. +data ScimTokenInfoV6 = ScimTokenInfoV6 + { -- | Which team can be managed with the token + stiTeam :: !TeamId, + -- | Token ID, can be used to eg. delete the token + stiId :: !ScimTokenId, + -- | Time of token creation + stiCreatedAt :: !UTCTime, + -- | IdP that created users will "belong" to + stiIdP :: !(Maybe SAML.IdPId), + -- | Free-form token description, can be set + -- by the token creator as a mental aid + stiDescr :: !Text + } + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform ScimTokenInfoV6) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema.Schema ScimTokenInfoV6) + +instance ToSchema ScimTokenInfoV6 where + schema = + object "ScimTokenInfoV6" $ + ScimTokenInfoV6 + <$> (.stiTeam) .= field "team" schema + <*> (.stiId) .= field "id" schema + <*> (.stiCreatedAt) .= field "created_at" utcTimeSchema + <*> (fmap SAML.fromIdPId . (.stiIdP)) .= (SAML.IdPId <$$> maybe_ (optField "idp" uuidSchema)) + <*> (.stiDescr) .= field "description" schema ---------------------------------------------------------------------------- -- @hscim@ extensions and wrappers @@ -328,69 +356,61 @@ instance Scim.Patchable ScimUserExtra where -- and/or ignore POSTed content, returning the full representation can be useful to the -- client, enabling it to correlate the client's and server's views of the new resource." data ValidScimUser = ValidScimUser - { _vsuExternalId :: ValidExternalId, - _vsuHandle :: Handle, - _vsuName :: BT.Name, - _vsuRichInfo :: RI.RichInfo, - _vsuActive :: Bool, - _vsuLocale :: Maybe Locale, - _vsuRole :: Maybe Role + { externalId :: ValidScimId, + handle :: Handle, + name :: BT.Name, + emails :: [EmailAddress], + richInfo :: RI.RichInfo, + active :: Bool, + locale :: Maybe Locale, + role :: Maybe Role } deriving (Eq, Show) --- | Note that a 'SAML.UserRef' may contain an email. Even though it is possible to construct a 'ValidExternalId' from such a 'UserRef' with 'UrefOnly', --- this does not represent a valid 'ValidExternalId'. So in case of a 'UrefOnly', we can assume that the 'UserRef' does not contain an email. -data ValidExternalId - = EmailAndUref Email SAML.UserRef - | UrefOnly SAML.UserRef - | EmailOnly Email +-- | This type carries externalId, plus email address (validated if present, unvalidated if not) and saml credentials, +-- because those are sometimes derived from the externalId field. +data ValidScimId = ValidScimId + { validScimIdExternal :: Text, + validScimIdAuthInfo :: These EmailAddress SAML.UserRef + } deriving (Eq, Show, Generic) -instance Arbitrary ValidExternalId where - arbitrary = do - muref <- QC.arbitrary - case muref of - Just uref -> case emailFromSAMLNameID $ uref ^. SAML.uidSubject of - Just e -> pure $ EmailAndUref e uref - Nothing -> pure $ UrefOnly uref - Nothing -> EmailOnly <$> QC.arbitrary - --- | Take apart a 'ValidExternalId', using 'SAML.UserRef' if available, otherwise 'Email'. -runValidExternalIdEither :: (SAML.UserRef -> a) -> (Email -> a) -> ValidExternalId -> a -runValidExternalIdEither doUref doEmail = \case - EmailAndUref _ uref -> doUref uref - UrefOnly uref -> doUref uref - EmailOnly em -> doEmail em - --- | Take apart a 'ValidExternalId', use both 'SAML.UserRef', 'Email' if applicable, and --- merge the result with a given function. -runValidExternalIdBoth :: (a -> a -> a) -> (SAML.UserRef -> a) -> (Email -> a) -> ValidExternalId -> a -runValidExternalIdBoth merge doUref doEmail = \case - EmailAndUref eml uref -> doUref uref `merge` doEmail eml - UrefOnly uref -> doUref uref - EmailOnly em -> doEmail em - --- | Returns either the extracted `UnqualifiedNameID` if present and not qualified, or the email address. --- This throws an exception if there are any qualifiers. -runValidExternalIdUnsafe :: ValidExternalId -> Text -runValidExternalIdUnsafe = runValidExternalIdEither urefToExternalIdUnsafe fromEmail - -veidUref :: Prism' ValidExternalId SAML.UserRef -veidUref = prism' UrefOnly $ - \case - EmailAndUref _ uref -> Just uref - UrefOnly uref -> Just uref - EmailOnly _ -> Nothing - -veidEmail :: Prism' ValidExternalId Email -veidEmail = prism' EmailOnly $ - \case - EmailAndUref em _ -> Just em - UrefOnly _ -> Nothing - EmailOnly em -> Just em +instance Arbitrary ValidScimId where + arbitrary = + these onlyThis (pure . onlyThat) (\_ uref -> pure (onlyThat uref)) =<< QC.arbitrary + where + onlyThis :: EmailAddress -> Gen ValidScimId + onlyThis em = do + extIdNick <- T.pack . QC.getPrintableString <$> QC.arbitrary + extId <- QC.elements [extIdNick, fromEmail em] + pure $ ValidScimId {validScimIdExternal = extId, validScimIdAuthInfo = This em} + + -- `unsafeShowNameID` can name clash, if this is a problem consider using `arbitraryValidScimIdNoNameIDQualifiers` + onlyThat :: SAML.UserRef -> ValidScimId + onlyThat uref = ValidScimId {validScimIdExternal = uref ^. SAML.uidSubject . to SAML.unsafeShowNameID . to CI.original, validScimIdAuthInfo = That uref} + +newtype ValidScimIdNoNameIDQualifiers = ValidScimIdNoNameIDQualifiers ValidScimId + deriving (Eq, Show) + +instance Arbitrary ValidScimIdNoNameIDQualifiers where + arbitrary = ValidScimIdNoNameIDQualifiers <$> arbitraryValidScimIdNoNameIDQualifiers + +arbitraryValidScimIdNoNameIDQualifiers :: QC.Gen ValidScimId +arbitraryValidScimIdNoNameIDQualifiers = do + veid :: ValidScimId <- QC.arbitrary + pure $ ValidScimId veid.validScimIdExternal (veid.validScimIdAuthInfo & mapThere removeQualifiers) + where + removeQualifiers :: SAML.UserRef -> SAML.UserRef + removeQualifiers = + (SAML.uidSubject . SAML.nameIDNameQ .~ Nothing) + . (SAML.uidSubject . SAML.nameIDSPProvidedID .~ Nothing) + . (SAML.uidSubject . SAML.nameIDSPNameQ .~ Nothing) + +veidUref :: ValidScimId -> Maybe SAML.UserRef +veidUref = justThere . validScimIdAuthInfo makeLenses ''ValidScimUser -makeLenses ''ValidExternalId +makeLenses ''ValidScimId ---------------------------------------------------------------------------- -- Request and response types @@ -398,51 +418,63 @@ makeLenses ''ValidExternalId -- | Type used for request parameters to 'APIScimTokenCreate'. data CreateScimToken = CreateScimToken { -- | Token description (as memory aid for whoever is creating the token) - createScimTokenDescr :: !Text, + description :: !Text, -- | User password, which we ask for because creating a token is a "powerful" operation - createScimTokenPassword :: !(Maybe PlainTextPassword6), - -- | User code (sent by email), for 2nd factor to 'createScimTokenPassword' - createScimTokenCode :: !(Maybe Code.Value) + password :: !(Maybe PlainTextPassword6), + -- | User code (sent by email), for 2nd factor to 'password' + verificationCode :: !(Maybe Code.Value), + -- | Optional name for the token + name :: Maybe Text } deriving (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform CreateScimToken) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema.Schema CreateScimToken) + +createScimTokenSchema :: Maybe Version -> ValueSchema NamedSwaggerDoc CreateScimToken +createScimTokenSchema v = + object ("CreateScimToken" <> foldMap (Text.toUpper . versionText) v) $ + CreateScimToken + <$> (.description) .= field "description" schema + <*> password .= optField "password" (maybeWithDefault A.Null schema) + <*> verificationCode .= optField "verification_code" (maybeWithDefault A.Null schema) + <*> (if isJust v then const Nothing else (.name)) .= maybe_ (optField "name" schema) + +instance ToSchema CreateScimToken where + schema = createScimTokenSchema Nothing -instance A.FromJSON CreateScimToken where - parseJSON = A.withObject "CreateScimToken" $ \o -> do - createScimTokenDescr <- o A..: "description" - createScimTokenPassword <- o A..:? "password" - createScimTokenCode <- o A..:? "verification_code" - pure CreateScimToken {..} - --- Used for integration tests -instance A.ToJSON CreateScimToken where - toJSON CreateScimToken {..} = - A.object - [ "description" A..= createScimTokenDescr, - "password" A..= createScimTokenPassword, - "verification_code" A..= createScimTokenCode - ] +instance ToSchema (Versioned 'V6 CreateScimToken) where + schema = Versioned <$> unVersioned .= createScimTokenSchema (Just V6) -- | Type used for the response of 'APIScimTokenCreate'. data CreateScimTokenResponse = CreateScimTokenResponse - { createScimTokenResponseToken :: ScimToken, - createScimTokenResponseInfo :: ScimTokenInfo + { token :: ScimToken, + info :: ScimTokenInfo } - deriving (Eq, Show) + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform CreateScimTokenResponse) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema.Schema CreateScimTokenResponse) --- Used for integration tests -instance A.FromJSON CreateScimTokenResponse where - parseJSON = A.withObject "CreateScimTokenResponse" $ \o -> do - createScimTokenResponseToken <- o A..: "token" - createScimTokenResponseInfo <- o A..: "info" - pure CreateScimTokenResponse {..} +instance ToSchema CreateScimTokenResponse where + schema = + object "CreateScimTokenResponse" $ + CreateScimTokenResponse + <$> (.token) .= field "token" schema + <*> (.info) .= field "info" schema + +data CreateScimTokenResponseV6 = CreateScimTokenResponseV6 + { token :: ScimToken, + info :: ScimTokenInfoV6 + } + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform CreateScimTokenResponseV6) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema.Schema CreateScimTokenResponseV6) -instance A.ToJSON CreateScimTokenResponse where - toJSON CreateScimTokenResponse {..} = - A.object - [ "token" A..= createScimTokenResponseToken, - "info" A..= createScimTokenResponseInfo - ] +instance ToSchema CreateScimTokenResponseV6 where + schema = + object "CreateScimTokenResponseV6" $ + CreateScimTokenResponseV6 + <$> (.token) .= field "token" schema + <*> (.info) .= field "info" schema -- | Type used for responses of endpoints that return a list of SCIM tokens. -- Wrapped into an object to allow extensibility later on. @@ -452,84 +484,23 @@ data ScimTokenList = ScimTokenList { scimTokenListTokens :: [ScimTokenInfo] } deriving (Eq, Show) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema.Schema ScimTokenList) -instance A.FromJSON ScimTokenList where - parseJSON = A.withObject "ScimTokenList" $ \o -> do - scimTokenListTokens <- o A..: "tokens" - pure ScimTokenList {..} - -instance A.ToJSON ScimTokenList where - toJSON ScimTokenList {..} = - A.object - [ "tokens" A..= scimTokenListTokens - ] - --- Swagger - -instance ToParamSchema ScimToken where - toParamSchema _ = toParamSchema (Proxy @Text) - -instance ToSchema ScimToken where - declareNamedSchema _ = - declareNamedSchema (Proxy @Text) - & mapped . schema . description ?~ "Authentication token" +instance ToSchema ScimTokenList where + schema = object "ScimTokenList" $ ScimTokenList <$> (.scimTokenListTokens) .= field "tokens" (array schema) -instance ToSchema ScimTokenInfo where - declareNamedSchema _ = do - teamSchema <- declareSchemaRef (Proxy @TeamId) - idSchema <- declareSchemaRef (Proxy @ScimTokenId) - createdAtSchema <- declareSchemaRef (Proxy @UTCTime) - idpSchema <- declareSchemaRef (Proxy @SAML.IdPId) - descrSchema <- declareSchemaRef (Proxy @Text) - pure $ - NamedSchema (Just "ScimTokenInfo") $ - mempty - & type_ ?~ OpenApiObject - & properties - .~ [ ("team", teamSchema), - ("id", idSchema), - ("created_at", createdAtSchema), - ("idp", idpSchema), - ("description", descrSchema) - ] - & required .~ ["team", "id", "created_at", "description"] +data ScimTokenListV6 = ScimTokenListV6 + { scimTokenListTokens :: [ScimTokenInfoV6] + } + deriving (Eq, Show) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema.Schema ScimTokenListV6) -instance ToSchema CreateScimToken where - declareNamedSchema _ = do - textSchema <- declareSchemaRef (Proxy @Text) - pure $ - NamedSchema (Just "CreateScimToken") $ - mempty - & type_ ?~ OpenApiObject - & properties - .~ [ ("description", textSchema), - ("password", textSchema), - ("verification_code", textSchema) - ] - & required .~ ["description"] +instance ToSchema ScimTokenListV6 where + schema = object "ScimTokenListV6" $ ScimTokenListV6 <$> (.scimTokenListTokens) .= field "tokens" (array schema) -instance ToSchema CreateScimTokenResponse where - declareNamedSchema _ = do - tokenSchema <- declareSchemaRef (Proxy @ScimToken) - infoSchema <- declareSchemaRef (Proxy @ScimTokenInfo) - pure $ - NamedSchema (Just "CreateScimTokenResponse") $ - mempty - & type_ ?~ OpenApiObject - & properties - .~ [ ("token", tokenSchema), - ("info", infoSchema) - ] - & required .~ ["token", "info"] +newtype ScimTokenName = ScimTokenName {fromScimTokenName :: Text} + deriving (Eq, Show) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema.Schema ScimTokenName) -instance ToSchema ScimTokenList where - declareNamedSchema _ = do - infoListSchema <- declareSchemaRef (Proxy @[ScimTokenInfo]) - pure $ - NamedSchema (Just "ScimTokenList") $ - mempty - & type_ ?~ OpenApiObject - & properties - .~ [ ("tokens", infoListSchema) - ] - & required .~ ["tokens"] +instance ToSchema ScimTokenName where + schema = object "ScimTokenName" $ ScimTokenName <$> fromScimTokenName .= field "name" schema diff --git a/libs/wire-api/src/Wire/API/User/Search.hs b/libs/wire-api/src/Wire/API/User/Search.hs index dfeb601c4e0..21f4181d248 100644 --- a/libs/wire-api/src/Wire/API/User/Search.hs +++ b/libs/wire-api/src/Wire/API/User/Search.hs @@ -1,7 +1,6 @@ {-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE StrictData #-} -{-# LANGUAGE TemplateHaskell #-} -- This file is part of the Wire Server implementation. -- @@ -35,13 +34,12 @@ where import Cassandra qualified as C import Control.Error -import Control.Lens (makePrisms, (?~)) +import Control.Lens ((?~)) import Data.Aeson hiding (object, (.=)) import Data.Aeson qualified as Aeson import Data.Attoparsec.ByteString (sepBy) import Data.Attoparsec.ByteString.Char8 (char, string) import Data.ByteString.Conversion (FromByteString (..), ToByteString (..)) -import Data.Either.Combinators (mapLeft) import Data.Id (TeamId, UserId) import Data.Json.Util (UTCTimeMillis) import Data.OpenApi (ToParamSchema (..)) @@ -56,7 +54,7 @@ import Servant.API (FromHttpApiData, ToHttpApiData (..)) import Web.Internal.HttpApiData (parseQueryParam) import Wire.API.Team.Role (Role) import Wire.API.User (ManagedBy) -import Wire.API.User.Identity (Email (..)) +import Wire.API.User.Identity (EmailAddress) import Wire.Arbitrary (Arbitrary, GenericUniform (..)) ------------------------------------------------------------------------------- @@ -184,14 +182,14 @@ data TeamContact = TeamContact teamContactColorId :: Maybe Int, teamContactHandle :: Maybe Text, teamContactTeam :: Maybe TeamId, - teamContactEmail :: Maybe Email, + teamContactEmail :: Maybe EmailAddress, teamContactCreatedAt :: Maybe UTCTimeMillis, teamContactManagedBy :: Maybe ManagedBy, teamContactSAMLIdp :: Maybe Text, teamContactRole :: Maybe Role, teamContactScimExternalId :: Maybe Text, teamContactSso :: Maybe Sso, - teamContactEmailUnvalidated :: Maybe Email + teamContactEmailUnvalidated :: Maybe EmailAddress } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform TeamContact) @@ -329,5 +327,3 @@ instance C.Cql FederatedUserSearchPolicy where fromCql (C.CqlInt 1) = pure ExactHandleSearch fromCql (C.CqlInt 2) = pure FullSearch fromCql n = Left $ "Unexpected SearchVisibilityInbound: " ++ show n - -makePrisms ''FederatedUserSearchPolicy diff --git a/libs/wire-api/src/Wire/API/UserEvent.hs b/libs/wire-api/src/Wire/API/UserEvent.hs index 41cbc98fe75..db4c69d2c24 100644 --- a/libs/wire-api/src/Wire/API/UserEvent.hs +++ b/libs/wire-api/src/Wire/API/UserEvent.hs @@ -146,6 +146,7 @@ data ClientEvent data UserUpdatedData = UserUpdatedData { eupId :: !UserId, eupName :: !(Maybe Name), + eupTextStatus :: !(Maybe TextStatus), -- | DEPRECATED eupPict :: !(Maybe Pict), eupAccentId :: !(Maybe ColourId), @@ -155,20 +156,21 @@ data UserUpdatedData = UserUpdatedData eupManagedBy :: !(Maybe ManagedBy), eupSSOId :: !(Maybe UserSSOId), eupSSOIdRemoved :: Bool, - eupSupportedProtocols :: !(Maybe (Set BaseProtocolTag)) + eupSupportedProtocols :: !(Maybe (Set BaseProtocolTag)), + eupTeam :: !(Maybe TeamId) } deriving stock (Eq, Show) data UserIdentityUpdatedData = UserIdentityUpdatedData { eiuId :: !UserId, - eiuEmail :: !(Maybe Email), + eiuEmail :: !(Maybe EmailAddress), eiuPhone :: !(Maybe Phone) } deriving stock (Eq, Show) data UserIdentityRemovedData = UserIdentityRemovedData { eirId :: !UserId, - eirEmail :: !(Maybe Email), + eirEmail :: !(Maybe EmailAddress), eirPhone :: !(Maybe Phone) } deriving stock (Eq, Show) @@ -183,56 +185,23 @@ data LegalHoldClientRequestedData = LegalHoldClientRequestedData } deriving stock (Eq, Show) -emailRemoved :: UserId -> Email -> UserEvent +emailRemoved :: UserId -> EmailAddress -> UserEvent emailRemoved u e = UserIdentityRemoved $ UserIdentityRemovedData u (Just e) Nothing -phoneRemoved :: UserId -> Phone -> UserEvent -phoneRemoved u p = - UserIdentityRemoved $ UserIdentityRemovedData u Nothing (Just p) - -emailUpdated :: UserId -> Email -> UserEvent +emailUpdated :: UserId -> EmailAddress -> UserEvent emailUpdated u e = UserIdentityUpdated $ UserIdentityUpdatedData u (Just e) Nothing -phoneUpdated :: UserId -> Phone -> UserEvent -phoneUpdated u p = - UserIdentityUpdated $ UserIdentityUpdatedData u Nothing (Just p) - -handleUpdated :: UserId -> Handle -> UserEvent -handleUpdated u h = - UserUpdated $ (emptyUserUpdatedData u) {eupHandle = Just h} - -localeUpdate :: UserId -> Locale -> UserEvent -localeUpdate u loc = - UserUpdated $ (emptyUserUpdatedData u) {eupLocale = Just loc} - -managedByUpdate :: UserId -> ManagedBy -> UserEvent -managedByUpdate u mb = - UserUpdated $ (emptyUserUpdatedData u) {eupManagedBy = Just mb} - -supportedProtocolUpdate :: UserId -> Set BaseProtocolTag -> UserEvent -supportedProtocolUpdate u prots = - UserUpdated $ (emptyUserUpdatedData u) {eupSupportedProtocols = Just prots} - -profileUpdated :: UserId -> UserUpdate -> UserEvent -profileUpdated u UserUpdate {..} = - UserUpdated $ - (emptyUserUpdatedData u) - { eupName = uupName, - eupPict = uupPict, - eupAccentId = uupAccentId, - eupAssets = uupAssets - } - -emptyUpdate :: UserId -> UserEvent -emptyUpdate = UserUpdated . emptyUserUpdatedData +teamUpdated :: UserId -> TeamId -> UserEvent +teamUpdated u t = UserUpdated (emptyUserUpdatedData u) {eupTeam = Just t} emptyUserUpdatedData :: UserId -> UserUpdatedData emptyUserUpdatedData u = UserUpdatedData { eupId = u, eupName = Nothing, + eupTextStatus = Nothing, eupPict = Nothing, eupAccentId = Nothing, eupAssets = Nothing, @@ -241,7 +210,8 @@ emptyUserUpdatedData u = eupManagedBy = Nothing, eupSSOId = Nothing, eupSSOIdRemoved = False, - eupSupportedProtocols = Nothing + eupSupportedProtocols = Nothing, + eupTeam = Nothing } -- Event schema @@ -273,6 +243,7 @@ eventObjectSchema = ( UserUpdatedData <$> eupId .= field "id" schema <*> eupName .= maybe_ (optField "name" schema) + <*> eupTextStatus .= maybe_ (optField "text_status" schema) <*> eupPict .= maybe_ (optField "picture" schema) -- DEPRECATED <*> eupAccentId .= maybe_ (optField "accent_id" schema) <*> eupAssets .= maybe_ (optField "assets" (array schema)) @@ -281,12 +252,8 @@ eventObjectSchema = <*> eupManagedBy .= maybe_ (optField "managed_by" schema) <*> eupSSOId .= maybe_ (optField "sso_id" genericToSchema) <*> eupSSOIdRemoved .= field "sso_id_deleted" schema - <*> eupSupportedProtocols - .= maybe_ - ( optField - "supported_protocols" - (set schema) - ) + <*> eupSupportedProtocols .= maybe_ (optField "supported_protocols" (set schema)) + <*> eupTeam .= maybe_ (optField "team" schema) ) ) ) @@ -382,7 +349,7 @@ eventObjectSchema = _ClientEvent ( tag _ClientAdded - (field "client" (clientSchema (Just V5))) + (field "client" (clientSchema (Just V6))) ) EventTypeClientRemoved -> tag diff --git a/libs/wire-api/src/Wire/API/VersionInfo.hs b/libs/wire-api/src/Wire/API/VersionInfo.hs index b7267028b60..0978a935d68 100644 --- a/libs/wire-api/src/Wire/API/VersionInfo.hs +++ b/libs/wire-api/src/Wire/API/VersionInfo.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE RecordWildCards #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -55,8 +57,10 @@ versionHeader = CI.mk . B8.pack $ symbolVal (Proxy @VersionHeader) -------------------------------------------------------------------------------- -- Servant combinators +-- | Exclusive range ('Until V5' means '[.. V4]') data Until v +-- | Inclusive range ('From V5' means '[V5 ..]') data From v instance @@ -72,7 +76,7 @@ instance route _ ctx action = route (Proxy @api) ctx $ - fmap const action `addHeaderCheck` withRequest headerCheck + action `addVersionCheck` withRequest headerCheck where headerCheck :: Wai.Request -> DelayedIO () headerCheck req = do @@ -85,6 +89,14 @@ instance when (v >= demote @n) $ delayedFail err404 + -- this hack makes sure that the version check is executed before the method check + addVersionCheck :: Delayed env b -> DelayedIO () -> Delayed env b + addVersionCheck Delayed {..} new = + Delayed + { capturesD = \env -> capturesD env <* new, + .. + } + hoistServerWithContext _ ctx f = hoistServerWithContext (Proxy @api) ctx f diff --git a/libs/wire-api/test/golden/0001-Golden-test-generation-patch-DO-NOT-MERGE.patch b/libs/wire-api/test/golden/0001-Golden-test-generation-patch-DO-NOT-MERGE.patch index 853e11bdf0b..e42d5f15251 100644 --- a/libs/wire-api/test/golden/0001-Golden-test-generation-patch-DO-NOT-MERGE.patch +++ b/libs/wire-api/test/golden/0001-Golden-test-generation-patch-DO-NOT-MERGE.patch @@ -569,19 +569,19 @@ diff --git a/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs b/libs/wire-a index feb034ada..4305a35d5 100644 --- a/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs +++ b/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs -@@ -48,9 +48,12 @@ data RequestNewLegalHoldClient = RequestNewLegalHoldClient +@@ -48,9 +48,12 @@ data RequestNewLegalHoldClientV0 = RequestNewLegalHoldClientV0 { userId :: UserId, teamId :: TeamId } - deriving stock (Show, Eq, Generic) + deriving stock (Eq, Generic) - deriving (Arbitrary) via (GenericUniform RequestNewLegalHoldClient) + deriving (Arbitrary) via (GenericUniform RequestNewLegalHoldClientV0) -+instance Show RequestNewLegalHoldClient where -+ show (RequestNewLegalHoldClient uid tid) = "(RequestNewLegalHoldClient (" <> show uid <> ") (" <> show tid <> "))" ++instance Show RequestNewLegalHoldClientV0 where ++ show (RequestNewLegalHoldClientV0 uid tid) = "(RequestNewLegalHoldClientV0 (" <> show uid <> ") (" <> show tid <> "))" + - instance ToJSON RequestNewLegalHoldClient where - toJSON (RequestNewLegalHoldClient userId teamId) = + instance ToJSON RequestNewLegalHoldClientV0 where + toJSON (RequestNewLegalHoldClientV0 userId teamId) = object $ diff --git a/libs/wire-api/src/Wire/API/Team/Member.hs b/libs/wire-api/src/Wire/API/Team/Member.hs index a106c02cc..9503860ac 100644 diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/FromJSON.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/FromJSON.hs index 1294ca9bd06..7ef64cf58f4 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/FromJSON.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/FromJSON.hs @@ -21,10 +21,12 @@ import Imports import Test.Tasty import Test.Tasty.HUnit import Test.Wire.API.Golden.Generated.Invite_user (testObject_Invite_user_2) +import Test.Wire.API.Golden.Generated.LockableFeature_team import Test.Wire.API.Golden.Generated.MemberUpdateData_user import Test.Wire.API.Golden.Generated.NewOtrMessage_user import Test.Wire.API.Golden.Generated.RmClient_user import Test.Wire.API.Golden.Generated.SimpleMember_user +import Test.Wire.API.Golden.Manual.Presence import Test.Wire.API.Golden.Runner import Wire.API.Conversation (Conversation, MemberUpdate, OtherMemberUpdate) import Wire.API.User (NewUser, NewUserPublic) @@ -88,5 +90,9 @@ tests = testFromJSONFailureWithMsg @NewUserPublic (Just "only managed-by-Wire users can be created here.") "testObject_NewUserPublic_user_1-3.json" - ] + ], + testCase "LockableFeature_ConferenceCallingConfig" $ + testFromJSONObject testObject_LockableFeature_team_14 "testObject_LockableFeature_team_14.json", + testCase "LockableFeature_ConferenceCallingConfig" $ + testFromJSONObject testObject_Presence_3 "testObject_Presence_3.json" ] diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs index 38c2fa673ea..63fbe936877 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs @@ -23,7 +23,6 @@ import Test.Wire.API.Golden.Generated.AccessRoleLegacy_user qualified import Test.Wire.API.Golden.Generated.AccessToken_user qualified import Test.Wire.API.Golden.Generated.Access_user qualified import Test.Wire.API.Golden.Generated.Action_user qualified -import Test.Wire.API.Golden.Generated.Activate_user qualified import Test.Wire.API.Golden.Generated.ActivationCode_user qualified import Test.Wire.API.Golden.Generated.ActivationKey_user qualified import Test.Wire.API.Golden.Generated.ActivationResponse_user qualified @@ -38,7 +37,6 @@ import Test.Wire.API.Golden.Generated.AssetSize_user qualified import Test.Wire.API.Golden.Generated.AssetToken_user qualified import Test.Wire.API.Golden.Generated.Asset_asset qualified import Test.Wire.API.Golden.Generated.BindingNewTeamUser_user qualified -import Test.Wire.API.Golden.Generated.BindingNewTeam_team qualified import Test.Wire.API.Golden.Generated.BotConvView_provider qualified import Test.Wire.API.Golden.Generated.BotUserView_provider qualified import Test.Wire.API.Golden.Generated.CheckHandles_user qualified @@ -87,6 +85,7 @@ import Test.Wire.API.Golden.Generated.Event_conversation qualified import Test.Wire.API.Golden.Generated.Event_featureConfig qualified import Test.Wire.API.Golden.Generated.Event_team qualified import Test.Wire.API.Golden.Generated.Event_user qualified +import Test.Wire.API.Golden.Generated.Feature_team qualified import Test.Wire.API.Golden.Generated.HandleUpdate_user qualified import Test.Wire.API.Golden.Generated.InvitationCode_user qualified import Test.Wire.API.Golden.Generated.InvitationList_team qualified @@ -100,10 +99,10 @@ import Test.Wire.API.Golden.Generated.LimitedQualifiedUserIdList_user qualified import Test.Wire.API.Golden.Generated.ListType_team qualified import Test.Wire.API.Golden.Generated.LocaleUpdate_user qualified import Test.Wire.API.Golden.Generated.Locale_user qualified +import Test.Wire.API.Golden.Generated.LockableFeaturePatch_team qualified +import Test.Wire.API.Golden.Generated.LockableFeature_team qualified import Test.Wire.API.Golden.Generated.LoginCodeTimeout_user qualified import Test.Wire.API.Golden.Generated.LoginCode_user qualified -import Test.Wire.API.Golden.Generated.LoginId_user qualified -import Test.Wire.API.Golden.Generated.Login_user qualified import Test.Wire.API.Golden.Generated.ManagedBy_user qualified import Test.Wire.API.Golden.Generated.MemberUpdateData_user qualified import Test.Wire.API.Golden.Generated.MemberUpdate_user qualified @@ -125,6 +124,7 @@ import Test.Wire.API.Golden.Generated.NewProvider_provider qualified import Test.Wire.API.Golden.Generated.NewServiceResponse_provider qualified import Test.Wire.API.Golden.Generated.NewService_provider qualified import Test.Wire.API.Golden.Generated.NewTeamMember_team qualified +import Test.Wire.API.Golden.Generated.NewTeam_team qualified import Test.Wire.API.Golden.Generated.NewUserPublic_user qualified import Test.Wire.API.Golden.Generated.NewUser_user qualified import Test.Wire.API.Golden.Generated.OtherMemberUpdate_user qualified @@ -176,7 +176,6 @@ import Test.Wire.API.Golden.Generated.Scheme_user qualified import Test.Wire.API.Golden.Generated.SearchResult_20Contact_user qualified import Test.Wire.API.Golden.Generated.SearchResult_20TeamContact_user qualified import Test.Wire.API.Golden.Generated.SelfProfile_user qualified -import Test.Wire.API.Golden.Generated.SendActivationCode_user qualified import Test.Wire.API.Golden.Generated.SendLoginCode_user qualified import Test.Wire.API.Golden.Generated.ServiceKeyPEM_provider qualified import Test.Wire.API.Golden.Generated.ServiceKeyType_provider qualified @@ -231,9 +230,6 @@ import Test.Wire.API.Golden.Generated.VerificationAction_user qualified import Test.Wire.API.Golden.Generated.VerifyDeleteUser_user qualified import Test.Wire.API.Golden.Generated.ViewLegalHoldServiceInfo_team qualified import Test.Wire.API.Golden.Generated.ViewLegalHoldService_team qualified -import Test.Wire.API.Golden.Generated.WithStatusNoLock_team qualified -import Test.Wire.API.Golden.Generated.WithStatusPatch_team qualified -import Test.Wire.API.Golden.Generated.WithStatus_team qualified import Test.Wire.API.Golden.Generated.Wrapped_20_22some_5fint_22_20Int_user qualified import Test.Wire.API.Golden.Runner import Wire.API.Routes.Version @@ -866,9 +862,6 @@ tests = ), ( Test.Wire.API.Golden.Generated.NewUser_user.testObject_NewUser_user_8, "testObject_NewUser_user_8.json" - ), - ( Test.Wire.API.Golden.Generated.NewUser_user.testObject_NewUser_user_9, - "testObject_NewUser_user_9.json" ) ], testGroup "Golden: NewUserPublic_user" $ @@ -984,14 +977,8 @@ tests = testObjects [(Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_1, "testObject_ActivationKey_user_1.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_2, "testObject_ActivationKey_user_2.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_3, "testObject_ActivationKey_user_3.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_4, "testObject_ActivationKey_user_4.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_5, "testObject_ActivationKey_user_5.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_6, "testObject_ActivationKey_user_6.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_7, "testObject_ActivationKey_user_7.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_8, "testObject_ActivationKey_user_8.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_9, "testObject_ActivationKey_user_9.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_10, "testObject_ActivationKey_user_10.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_11, "testObject_ActivationKey_user_11.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_12, "testObject_ActivationKey_user_12.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_13, "testObject_ActivationKey_user_13.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_14, "testObject_ActivationKey_user_14.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_15, "testObject_ActivationKey_user_15.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_16, "testObject_ActivationKey_user_16.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_17, "testObject_ActivationKey_user_17.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_18, "testObject_ActivationKey_user_18.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_19, "testObject_ActivationKey_user_19.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_20, "testObject_ActivationKey_user_20.json")], testGroup "Golden: ActivationCode_user" $ testObjects [(Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_1, "testObject_ActivationCode_user_1.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_2, "testObject_ActivationCode_user_2.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_3, "testObject_ActivationCode_user_3.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_4, "testObject_ActivationCode_user_4.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_5, "testObject_ActivationCode_user_5.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_6, "testObject_ActivationCode_user_6.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_7, "testObject_ActivationCode_user_7.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_8, "testObject_ActivationCode_user_8.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_9, "testObject_ActivationCode_user_9.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_10, "testObject_ActivationCode_user_10.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_11, "testObject_ActivationCode_user_11.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_12, "testObject_ActivationCode_user_12.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_13, "testObject_ActivationCode_user_13.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_14, "testObject_ActivationCode_user_14.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_15, "testObject_ActivationCode_user_15.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_16, "testObject_ActivationCode_user_16.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_17, "testObject_ActivationCode_user_17.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_18, "testObject_ActivationCode_user_18.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_19, "testObject_ActivationCode_user_19.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_20, "testObject_ActivationCode_user_20.json")], - testGroup "Golden: Activate_user" $ - testObjects [(Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_1, "testObject_Activate_user_1.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_2, "testObject_Activate_user_2.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_3, "testObject_Activate_user_3.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_4, "testObject_Activate_user_4.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_5, "testObject_Activate_user_5.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_6, "testObject_Activate_user_6.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_7, "testObject_Activate_user_7.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_8, "testObject_Activate_user_8.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_9, "testObject_Activate_user_9.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_10, "testObject_Activate_user_10.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_11, "testObject_Activate_user_11.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_12, "testObject_Activate_user_12.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_13, "testObject_Activate_user_13.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_14, "testObject_Activate_user_14.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_15, "testObject_Activate_user_15.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_16, "testObject_Activate_user_16.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_17, "testObject_Activate_user_17.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_18, "testObject_Activate_user_18.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_19, "testObject_Activate_user_19.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_20, "testObject_Activate_user_20.json")], testGroup "Golden: ActivationResponse_user" $ testObjects [(Test.Wire.API.Golden.Generated.ActivationResponse_user.testObject_ActivationResponse_user_1, "testObject_ActivationResponse_user_1.json"), (Test.Wire.API.Golden.Generated.ActivationResponse_user.testObject_ActivationResponse_user_2, "testObject_ActivationResponse_user_2.json"), (Test.Wire.API.Golden.Generated.ActivationResponse_user.testObject_ActivationResponse_user_3, "testObject_ActivationResponse_user_3.json"), (Test.Wire.API.Golden.Generated.ActivationResponse_user.testObject_ActivationResponse_user_4, "testObject_ActivationResponse_user_4.json"), (Test.Wire.API.Golden.Generated.ActivationResponse_user.testObject_ActivationResponse_user_5, "testObject_ActivationResponse_user_5.json"), (Test.Wire.API.Golden.Generated.ActivationResponse_user.testObject_ActivationResponse_user_6, "testObject_ActivationResponse_user_6.json"), (Test.Wire.API.Golden.Generated.ActivationResponse_user.testObject_ActivationResponse_user_7, "testObject_ActivationResponse_user_7.json"), (Test.Wire.API.Golden.Generated.ActivationResponse_user.testObject_ActivationResponse_user_8, "testObject_ActivationResponse_user_8.json"), (Test.Wire.API.Golden.Generated.ActivationResponse_user.testObject_ActivationResponse_user_9, "testObject_ActivationResponse_user_9.json"), (Test.Wire.API.Golden.Generated.ActivationResponse_user.testObject_ActivationResponse_user_10, "testObject_ActivationResponse_user_10.json")], - testGroup "Golden: SendActivationCode_user" $ - testObjects [(Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_1, "testObject_SendActivationCode_user_1.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_2, "testObject_SendActivationCode_user_2.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_3, "testObject_SendActivationCode_user_3.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_4, "testObject_SendActivationCode_user_4.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_5, "testObject_SendActivationCode_user_5.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_6, "testObject_SendActivationCode_user_6.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_7, "testObject_SendActivationCode_user_7.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_8, "testObject_SendActivationCode_user_8.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_9, "testObject_SendActivationCode_user_9.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_10, "testObject_SendActivationCode_user_10.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_11, "testObject_SendActivationCode_user_11.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_12, "testObject_SendActivationCode_user_12.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_13, "testObject_SendActivationCode_user_13.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_14, "testObject_SendActivationCode_user_14.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_15, "testObject_SendActivationCode_user_15.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_16, "testObject_SendActivationCode_user_16.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_17, "testObject_SendActivationCode_user_17.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_18, "testObject_SendActivationCode_user_18.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_19, "testObject_SendActivationCode_user_19.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_20, "testObject_SendActivationCode_user_20.json")], - testGroup "Golden: LoginId_user" $ - testObjects [(Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_1, "testObject_LoginId_user_1.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_2, "testObject_LoginId_user_2.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_3, "testObject_LoginId_user_3.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_4, "testObject_LoginId_user_4.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_5, "testObject_LoginId_user_5.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_6, "testObject_LoginId_user_6.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_7, "testObject_LoginId_user_7.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_8, "testObject_LoginId_user_8.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_9, "testObject_LoginId_user_9.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_10, "testObject_LoginId_user_10.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_11, "testObject_LoginId_user_11.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_12, "testObject_LoginId_user_12.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_13, "testObject_LoginId_user_13.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_14, "testObject_LoginId_user_14.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_15, "testObject_LoginId_user_15.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_16, "testObject_LoginId_user_16.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_17, "testObject_LoginId_user_17.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_18, "testObject_LoginId_user_18.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_19, "testObject_LoginId_user_19.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_20, "testObject_LoginId_user_20.json")], testGroup "Golden: LoginCode_user" $ testObjects [(Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_1, "testObject_LoginCode_user_1.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_2, "testObject_LoginCode_user_2.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_3, "testObject_LoginCode_user_3.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_4, "testObject_LoginCode_user_4.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_5, "testObject_LoginCode_user_5.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_6, "testObject_LoginCode_user_6.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_7, "testObject_LoginCode_user_7.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_8, "testObject_LoginCode_user_8.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_9, "testObject_LoginCode_user_9.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_10, "testObject_LoginCode_user_10.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_11, "testObject_LoginCode_user_11.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_12, "testObject_LoginCode_user_12.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_13, "testObject_LoginCode_user_13.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_14, "testObject_LoginCode_user_14.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_15, "testObject_LoginCode_user_15.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_16, "testObject_LoginCode_user_16.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_17, "testObject_LoginCode_user_17.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_18, "testObject_LoginCode_user_18.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_19, "testObject_LoginCode_user_19.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_20, "testObject_LoginCode_user_20.json")], testGroup "Golden: PendingLoginCode_user" $ @@ -1002,8 +989,6 @@ tests = testObjects [(Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_1, "testObject_LoginCodeTimeout_user_1.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_2, "testObject_LoginCodeTimeout_user_2.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_3, "testObject_LoginCodeTimeout_user_3.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_4, "testObject_LoginCodeTimeout_user_4.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_5, "testObject_LoginCodeTimeout_user_5.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_6, "testObject_LoginCodeTimeout_user_6.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_7, "testObject_LoginCodeTimeout_user_7.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_8, "testObject_LoginCodeTimeout_user_8.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_9, "testObject_LoginCodeTimeout_user_9.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_10, "testObject_LoginCodeTimeout_user_10.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_11, "testObject_LoginCodeTimeout_user_11.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_12, "testObject_LoginCodeTimeout_user_12.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_13, "testObject_LoginCodeTimeout_user_13.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_14, "testObject_LoginCodeTimeout_user_14.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_15, "testObject_LoginCodeTimeout_user_15.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_16, "testObject_LoginCodeTimeout_user_16.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_17, "testObject_LoginCodeTimeout_user_17.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_18, "testObject_LoginCodeTimeout_user_18.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_19, "testObject_LoginCodeTimeout_user_19.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_20, "testObject_LoginCodeTimeout_user_20.json")], testGroup "Golden: CookieLabel_user" $ testObjects [(Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_1, "testObject_CookieLabel_user_1.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_2, "testObject_CookieLabel_user_2.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_3, "testObject_CookieLabel_user_3.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_4, "testObject_CookieLabel_user_4.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_5, "testObject_CookieLabel_user_5.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_6, "testObject_CookieLabel_user_6.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_7, "testObject_CookieLabel_user_7.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_8, "testObject_CookieLabel_user_8.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_9, "testObject_CookieLabel_user_9.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_10, "testObject_CookieLabel_user_10.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_11, "testObject_CookieLabel_user_11.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_12, "testObject_CookieLabel_user_12.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_13, "testObject_CookieLabel_user_13.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_14, "testObject_CookieLabel_user_14.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_15, "testObject_CookieLabel_user_15.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_16, "testObject_CookieLabel_user_16.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_17, "testObject_CookieLabel_user_17.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_18, "testObject_CookieLabel_user_18.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_19, "testObject_CookieLabel_user_19.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_20, "testObject_CookieLabel_user_20.json")], - testGroup "Golden: Login_user" $ - testObjects [(Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_1, "testObject_Login_user_1.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_2, "testObject_Login_user_2.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_3, "testObject_Login_user_3.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_4, "testObject_Login_user_4.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_5, "testObject_Login_user_5.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_6, "testObject_Login_user_6.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_7, "testObject_Login_user_7.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_8, "testObject_Login_user_8.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_9, "testObject_Login_user_9.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_10, "testObject_Login_user_10.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_11, "testObject_Login_user_11.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_12, "testObject_Login_user_12.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_13, "testObject_Login_user_13.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_14, "testObject_Login_user_14.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_15, "testObject_Login_user_15.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_16, "testObject_Login_user_16.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_17, "testObject_Login_user_17.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_18, "testObject_Login_user_18.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_19, "testObject_Login_user_19.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_20, "testObject_Login_user_20.json")], testGroup "Golden: CookieId_user" $ testObjects [(Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_1, "testObject_CookieId_user_1.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_2, "testObject_CookieId_user_2.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_3, "testObject_CookieId_user_3.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_4, "testObject_CookieId_user_4.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_5, "testObject_CookieId_user_5.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_6, "testObject_CookieId_user_6.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_7, "testObject_CookieId_user_7.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_8, "testObject_CookieId_user_8.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_9, "testObject_CookieId_user_9.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_10, "testObject_CookieId_user_10.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_11, "testObject_CookieId_user_11.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_12, "testObject_CookieId_user_12.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_13, "testObject_CookieId_user_13.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_14, "testObject_CookieId_user_14.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_15, "testObject_CookieId_user_15.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_16, "testObject_CookieId_user_16.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_17, "testObject_CookieId_user_17.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_18, "testObject_CookieId_user_18.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_19, "testObject_CookieId_user_19.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_20, "testObject_CookieId_user_20.json")], testGroup "Golden: CookieType_user" $ @@ -1029,7 +1014,7 @@ tests = testGroup "Golden: PubClient_user" $ testObjects [(Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_1, "testObject_PubClient_user_1.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_2, "testObject_PubClient_user_2.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_3, "testObject_PubClient_user_3.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_4, "testObject_PubClient_user_4.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_5, "testObject_PubClient_user_5.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_6, "testObject_PubClient_user_6.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_7, "testObject_PubClient_user_7.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_8, "testObject_PubClient_user_8.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_9, "testObject_PubClient_user_9.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_10, "testObject_PubClient_user_10.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_11, "testObject_PubClient_user_11.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_12, "testObject_PubClient_user_12.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_13, "testObject_PubClient_user_13.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_14, "testObject_PubClient_user_14.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_15, "testObject_PubClient_user_15.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_16, "testObject_PubClient_user_16.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_17, "testObject_PubClient_user_17.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_18, "testObject_PubClient_user_18.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_19, "testObject_PubClient_user_19.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_20, "testObject_PubClient_user_20.json")], testGroup "Golden: ClientV5_user" $ - testObjects [(Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_1, "testObject_ClientV5_user_1.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_2, "testObject_ClientV5_user_2.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_3, "testObject_ClientV5_user_3.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_4, "testObject_ClientV5_user_4.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_5, "testObject_ClientV5_user_5.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_6, "testObject_ClientV5_user_6.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_7, "testObject_ClientV5_user_7.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_8, "testObject_ClientV5_user_8.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_9, "testObject_ClientV5_user_9.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_10, "testObject_ClientV5_user_10.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_11, "testObject_ClientV5_user_11.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_12, "testObject_ClientV5_user_12.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_13, "testObject_ClientV5_user_13.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_14, "testObject_ClientV5_user_14.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_15, "testObject_ClientV5_user_15.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_16, "testObject_ClientV5_user_16.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_17, "testObject_ClientV5_user_17.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_18, "testObject_ClientV5_user_18.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_19, "testObject_ClientV5_user_19.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_20, "testObject_ClientV5_user_20.json")], + testObjects [(Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_1, "testObject_ClientV6_user_1.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_2, "testObject_ClientV6_user_2.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_3, "testObject_ClientV6_user_3.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_4, "testObject_ClientV6_user_4.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_5, "testObject_ClientV6_user_5.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_6, "testObject_ClientV6_user_6.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_7, "testObject_ClientV6_user_7.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_8, "testObject_ClientV6_user_8.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_9, "testObject_ClientV6_user_9.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_10, "testObject_ClientV6_user_10.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_11, "testObject_ClientV6_user_11.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_12, "testObject_ClientV6_user_12.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_13, "testObject_ClientV6_user_13.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_14, "testObject_ClientV6_user_14.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_15, "testObject_ClientV6_user_15.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_16, "testObject_ClientV6_user_16.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_17, "testObject_ClientV6_user_17.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_18, "testObject_ClientV6_user_18.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_19, "testObject_ClientV6_user_19.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_20, "testObject_ClientV6_user_20.json")], testGroup "Golden: Client_user" $ testObjects [(Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_1, "testObject_Client_user_1.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_2, "testObject_Client_user_2.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_3, "testObject_Client_user_3.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_4, "testObject_Client_user_4.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_5, "testObject_Client_user_5.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_6, "testObject_Client_user_6.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_7, "testObject_Client_user_7.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_8, "testObject_Client_user_8.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_9, "testObject_Client_user_9.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_10, "testObject_Client_user_10.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_11, "testObject_Client_user_11.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_12, "testObject_Client_user_12.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_13, "testObject_Client_user_13.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_14, "testObject_Client_user_14.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_15, "testObject_Client_user_15.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_16, "testObject_Client_user_16.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_17, "testObject_Client_user_17.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_18, "testObject_Client_user_18.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_19, "testObject_Client_user_19.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_20, "testObject_Client_user_20.json")], testGroup "Golden: NewClient_user" $ @@ -1053,7 +1038,9 @@ tests = testGroup "Golden: CheckHandles_user" $ testObjects [(Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_1, "testObject_CheckHandles_user_1.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_2, "testObject_CheckHandles_user_2.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_3, "testObject_CheckHandles_user_3.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_4, "testObject_CheckHandles_user_4.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_5, "testObject_CheckHandles_user_5.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_6, "testObject_CheckHandles_user_6.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_7, "testObject_CheckHandles_user_7.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_8, "testObject_CheckHandles_user_8.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_9, "testObject_CheckHandles_user_9.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_10, "testObject_CheckHandles_user_10.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_11, "testObject_CheckHandles_user_11.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_12, "testObject_CheckHandles_user_12.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_13, "testObject_CheckHandles_user_13.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_14, "testObject_CheckHandles_user_14.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_15, "testObject_CheckHandles_user_15.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_16, "testObject_CheckHandles_user_16.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_17, "testObject_CheckHandles_user_17.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_18, "testObject_CheckHandles_user_18.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_19, "testObject_CheckHandles_user_19.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_20, "testObject_CheckHandles_user_20.json")], testGroup "Golden: Email_user" $ - testObjects [(Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_1, "testObject_Email_user_1.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_2, "testObject_Email_user_2.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_3, "testObject_Email_user_3.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_4, "testObject_Email_user_4.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_5, "testObject_Email_user_5.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_6, "testObject_Email_user_6.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_7, "testObject_Email_user_7.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_8, "testObject_Email_user_8.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_9, "testObject_Email_user_9.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_10, "testObject_Email_user_10.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_11, "testObject_Email_user_11.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_12, "testObject_Email_user_12.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_13, "testObject_Email_user_13.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_14, "testObject_Email_user_14.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_15, "testObject_Email_user_15.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_16, "testObject_Email_user_16.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_17, "testObject_Email_user_17.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_18, "testObject_Email_user_18.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_19, "testObject_Email_user_19.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_20, "testObject_Email_user_20.json")], + testObjects + [ (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_1, "testObject_Email_user_1.json") + ], testGroup "Golden: Phone_user" $ testObjects [(Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_1, "testObject_Phone_user_1.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_2, "testObject_Phone_user_2.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_3, "testObject_Phone_user_3.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_4, "testObject_Phone_user_4.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_5, "testObject_Phone_user_5.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_6, "testObject_Phone_user_6.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_7, "testObject_Phone_user_7.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_8, "testObject_Phone_user_8.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_9, "testObject_Phone_user_9.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_10, "testObject_Phone_user_10.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_11, "testObject_Phone_user_11.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_12, "testObject_Phone_user_12.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_13, "testObject_Phone_user_13.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_14, "testObject_Phone_user_14.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_15, "testObject_Phone_user_15.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_16, "testObject_Phone_user_16.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_17, "testObject_Phone_user_17.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_18, "testObject_Phone_user_18.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_19, "testObject_Phone_user_19.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_20, "testObject_Phone_user_20.json")], testGroup "Golden: UserSSOId_user" $ @@ -1125,7 +1112,9 @@ tests = testGroup "Golden: PasswordChange_provider" $ testObjects [(Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_1, "testObject_PasswordChange_provider_1.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_2, "testObject_PasswordChange_provider_2.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_3, "testObject_PasswordChange_provider_3.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_4, "testObject_PasswordChange_provider_4.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_5, "testObject_PasswordChange_provider_5.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_6, "testObject_PasswordChange_provider_6.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_7, "testObject_PasswordChange_provider_7.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_8, "testObject_PasswordChange_provider_8.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_9, "testObject_PasswordChange_provider_9.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_10, "testObject_PasswordChange_provider_10.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_11, "testObject_PasswordChange_provider_11.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_12, "testObject_PasswordChange_provider_12.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_13, "testObject_PasswordChange_provider_13.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_14, "testObject_PasswordChange_provider_14.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_15, "testObject_PasswordChange_provider_15.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_16, "testObject_PasswordChange_provider_16.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_17, "testObject_PasswordChange_provider_17.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_18, "testObject_PasswordChange_provider_18.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_19, "testObject_PasswordChange_provider_19.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_20, "testObject_PasswordChange_provider_20.json")], testGroup "Golden: EmailUpdate_provider" $ - testObjects [(Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_1, "testObject_EmailUpdate_provider_1.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_2, "testObject_EmailUpdate_provider_2.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_3, "testObject_EmailUpdate_provider_3.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_4, "testObject_EmailUpdate_provider_4.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_5, "testObject_EmailUpdate_provider_5.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_6, "testObject_EmailUpdate_provider_6.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_7, "testObject_EmailUpdate_provider_7.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_8, "testObject_EmailUpdate_provider_8.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_9, "testObject_EmailUpdate_provider_9.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_10, "testObject_EmailUpdate_provider_10.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_11, "testObject_EmailUpdate_provider_11.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_12, "testObject_EmailUpdate_provider_12.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_13, "testObject_EmailUpdate_provider_13.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_14, "testObject_EmailUpdate_provider_14.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_15, "testObject_EmailUpdate_provider_15.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_16, "testObject_EmailUpdate_provider_16.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_17, "testObject_EmailUpdate_provider_17.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_18, "testObject_EmailUpdate_provider_18.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_19, "testObject_EmailUpdate_provider_19.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_20, "testObject_EmailUpdate_provider_20.json")], + testObjects + [ (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_1, "testObject_EmailUpdate_provider_1.json") + ], testGroup "Golden: BotConvView_provider" $ testObjects [(Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_1, "testObject_BotConvView_provider_1.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_2, "testObject_BotConvView_provider_2.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_3, "testObject_BotConvView_provider_3.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_4, "testObject_BotConvView_provider_4.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_5, "testObject_BotConvView_provider_5.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_6, "testObject_BotConvView_provider_6.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_7, "testObject_BotConvView_provider_7.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_8, "testObject_BotConvView_provider_8.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_9, "testObject_BotConvView_provider_9.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_10, "testObject_BotConvView_provider_10.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_11, "testObject_BotConvView_provider_11.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_12, "testObject_BotConvView_provider_12.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_13, "testObject_BotConvView_provider_13.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_14, "testObject_BotConvView_provider_14.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_15, "testObject_BotConvView_provider_15.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_16, "testObject_BotConvView_provider_16.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_17, "testObject_BotConvView_provider_17.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_18, "testObject_BotConvView_provider_18.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_19, "testObject_BotConvView_provider_19.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_20, "testObject_BotConvView_provider_20.json")], testGroup "Golden: BotUserView_provider" $ @@ -1167,7 +1156,7 @@ tests = testGroup "Golden: ServiceTagList_provider" $ testObjects [(Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_1, "testObject_ServiceTagList_provider_1.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_2, "testObject_ServiceTagList_provider_2.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_3, "testObject_ServiceTagList_provider_3.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_4, "testObject_ServiceTagList_provider_4.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_5, "testObject_ServiceTagList_provider_5.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_6, "testObject_ServiceTagList_provider_6.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_7, "testObject_ServiceTagList_provider_7.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_8, "testObject_ServiceTagList_provider_8.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_9, "testObject_ServiceTagList_provider_9.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_10, "testObject_ServiceTagList_provider_10.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_11, "testObject_ServiceTagList_provider_11.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_12, "testObject_ServiceTagList_provider_12.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_13, "testObject_ServiceTagList_provider_13.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_14, "testObject_ServiceTagList_provider_14.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_15, "testObject_ServiceTagList_provider_15.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_16, "testObject_ServiceTagList_provider_16.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_17, "testObject_ServiceTagList_provider_17.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_18, "testObject_ServiceTagList_provider_18.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_19, "testObject_ServiceTagList_provider_19.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_20, "testObject_ServiceTagList_provider_20.json")], testGroup "Golden: BindingNewTeam_team" $ - testObjects [(Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_1, "testObject_BindingNewTeam_team_1.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_2, "testObject_BindingNewTeam_team_2.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_3, "testObject_BindingNewTeam_team_3.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_4, "testObject_BindingNewTeam_team_4.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_5, "testObject_BindingNewTeam_team_5.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_6, "testObject_BindingNewTeam_team_6.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_7, "testObject_BindingNewTeam_team_7.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_8, "testObject_BindingNewTeam_team_8.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_9, "testObject_BindingNewTeam_team_9.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_10, "testObject_BindingNewTeam_team_10.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_11, "testObject_BindingNewTeam_team_11.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_12, "testObject_BindingNewTeam_team_12.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_13, "testObject_BindingNewTeam_team_13.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_14, "testObject_BindingNewTeam_team_14.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_15, "testObject_BindingNewTeam_team_15.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_16, "testObject_BindingNewTeam_team_16.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_17, "testObject_BindingNewTeam_team_17.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_18, "testObject_BindingNewTeam_team_18.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_19, "testObject_BindingNewTeam_team_19.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_20, "testObject_BindingNewTeam_team_20.json")], + testObjects [(Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_1, "testObject_NewTeam_team_1.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_2, "testObject_NewTeam_team_2.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_3, "testObject_NewTeam_team_3.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_4, "testObject_NewTeam_team_4.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_5, "testObject_NewTeam_team_5.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_6, "testObject_NewTeam_team_6.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_7, "testObject_NewTeam_team_7.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_8, "testObject_NewTeam_team_8.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_9, "testObject_NewTeam_team_9.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_10, "testObject_NewTeam_team_10.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_11, "testObject_NewTeam_team_11.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_12, "testObject_NewTeam_team_12.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_13, "testObject_NewTeam_team_13.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_14, "testObject_NewTeam_team_14.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_15, "testObject_NewTeam_team_15.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_16, "testObject_NewTeam_team_16.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_17, "testObject_NewTeam_team_17.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_18, "testObject_NewTeam_team_18.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_19, "testObject_NewTeam_team_19.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_20, "testObject_NewTeam_team_20.json")], testGroup "Golden: TeamBinding_team" $ testObjects [(Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_1, "testObject_TeamBinding_team_1.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_2, "testObject_TeamBinding_team_2.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_3, "testObject_TeamBinding_team_3.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_4, "testObject_TeamBinding_team_4.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_5, "testObject_TeamBinding_team_5.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_6, "testObject_TeamBinding_team_6.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_7, "testObject_TeamBinding_team_7.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_8, "testObject_TeamBinding_team_8.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_9, "testObject_TeamBinding_team_9.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_10, "testObject_TeamBinding_team_10.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_11, "testObject_TeamBinding_team_11.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_12, "testObject_TeamBinding_team_12.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_13, "testObject_TeamBinding_team_13.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_14, "testObject_TeamBinding_team_14.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_15, "testObject_TeamBinding_team_15.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_16, "testObject_TeamBinding_team_16.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_17, "testObject_TeamBinding_team_17.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_18, "testObject_TeamBinding_team_18.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_19, "testObject_TeamBinding_team_19.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_20, "testObject_TeamBinding_team_20.json")], testGroup "Golden: Team_team" $ @@ -1196,110 +1185,110 @@ tests = "testObject_TeamConversationList_team_2.json" ) ], - testGroup "Golden: WithStatusNoLock_team 1" $ + testGroup "Golden: Feature_team 1" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_1, "testObject_WithStatusNoLock_team_1.json"), - (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_2, "testObject_WithStatusNoLock_team_2.json"), - (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_3, "testObject_WithStatusNoLock_team_3.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_1, "testObject_Feature_team_1.json"), + (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_2, "testObject_Feature_team_2.json"), + (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_3, "testObject_Feature_team_3.json") ], - testGroup "Golden: WithStatusNoLock_team 2" $ + testGroup "Golden: Feature_team 2" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_4, "testObject_WithStatusNoLock_team_4.json"), - (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_5, "testObject_WithStatusNoLock_team_5.json"), - (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_6, "testObject_WithStatusNoLock_team_6.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_4, "testObject_Feature_team_4.json"), + (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_5, "testObject_Feature_team_5.json"), + (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_6, "testObject_Feature_team_6.json") ], - testGroup "Golden: WithStatusNoLock_team 3" $ + testGroup "Golden: Feature_team 3" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_7, "testObject_WithStatusNoLock_team_7.json"), - (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_8, "testObject_WithStatusNoLock_team_8.json"), - (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_9, "testObject_WithStatusNoLock_team_9.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_7, "testObject_Feature_team_7.json"), + (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_8, "testObject_Feature_team_8.json"), + (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_9, "testObject_Feature_team_9.json") ], - testGroup "Golden: WithStatusNoLock_team 4" $ + testGroup "Golden: Feature_team 4" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_10, "testObject_WithStatusNoLock_team_10.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_10, "testObject_Feature_team_10.json") ], - testGroup "Golden: WithStatusNoLock_team 5" $ + testGroup "Golden: Feature_team 5" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_11, "testObject_WithStatusNoLock_team_11.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_11, "testObject_Feature_team_11.json") ], - testGroup "Golden: WithStatusNoLock_team 6" $ + testGroup "Golden: Feature_team 6" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_12, "testObject_WithStatusNoLock_team_12.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_12, "testObject_Feature_team_12.json") ], - testGroup "Golden: WithStatusNoLock_team 7" $ + testGroup "Golden: Feature_team 7" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_13, "testObject_WithStatusNoLock_team_13.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_13, "testObject_Feature_team_13.json") ], - testGroup "Golden: WithStatusNoLock_team 8" $ + testGroup "Golden: Feature_team 8" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_14, "testObject_WithStatusNoLock_team_14.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_14, "testObject_Feature_team_14.json") ], - testGroup "Golden: WithStatusNoLock_team 9" $ + testGroup "Golden: Feature_team 9" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_15, "testObject_WithStatusNoLock_team_15.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_15, "testObject_Feature_team_15.json") ], - testGroup "Golden: WithStatusNoLock_team 10" $ + testGroup "Golden: Feature_team 10" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_16, "testObject_WithStatusNoLock_team_16.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_16, "testObject_Feature_team_16.json") ], - testGroup "Golden: WithStatusNoLock_team 11" $ + testGroup "Golden: Feature_team 11" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_17, "testObject_WithStatusNoLock_team_17.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_17, "testObject_Feature_team_17.json") ], - testGroup "Golden: WithStatus_team 1" $ + testGroup "Golden: LockableFeature_team 1" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_1, "testObject_WithStatus_team_1.json"), - (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_2, "testObject_WithStatus_team_2.json"), - (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_3, "testObject_WithStatus_team_3.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_1, "testObject_LockableFeature_team_1.json"), + (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_2, "testObject_LockableFeature_team_2.json"), + (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_3, "testObject_LockableFeature_team_3.json") ], - testGroup "Golden: WithStatus_team 2" $ + testGroup "Golden: LockableFeature_team 2" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_4, "testObject_WithStatus_team_4.json"), - (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_5, "testObject_WithStatus_team_5.json"), - (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_6, "testObject_WithStatus_team_6.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_4, "testObject_LockableFeature_team_4.json"), + (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_5, "testObject_LockableFeature_team_5.json"), + (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_6, "testObject_LockableFeature_team_6.json") ], - testGroup "Golden: WithStatus_team 3" $ + testGroup "Golden: LockableFeature_team 3" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_7, "testObject_WithStatus_team_7.json"), - (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_8, "testObject_WithStatus_team_8.json"), - (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_9, "testObject_WithStatus_team_9.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_7, "testObject_LockableFeature_team_7.json"), + (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_8, "testObject_LockableFeature_team_8.json"), + (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_9, "testObject_LockableFeature_team_9.json") ], - testGroup "Golden: WithStatus_team 4" $ + testGroup "Golden: LockableFeature_team 4" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_10, "testObject_WithStatus_team_10.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_10, "testObject_LockableFeature_team_10.json") ], - testGroup "Golden: WithStatus_team 5" $ + testGroup "Golden: LockableFeature_team 5" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_11, "testObject_WithStatus_team_11.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_11, "testObject_LockableFeature_team_11.json") ], - testGroup "Golden: WithStatus_team 6" $ + testGroup "Golden: LockableFeature_team 6" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_12, "testObject_WithStatus_team_12.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_12, "testObject_LockableFeature_team_12.json") ], - testGroup "Golden: WithStatus_team 7" $ + testGroup "Golden: LockableFeature_team 7" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_13, "testObject_WithStatus_team_13.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_13, "testObject_LockableFeature_team_13.json") ], - testGroup "Golden: WithStatus_team 8" $ + testGroup "Golden: LockableFeature_team 8" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_14, "testObject_WithStatus_team_14.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_14, "testObject_LockableFeature_team_14.json") ], - testGroup "Golden: WithStatus_team 9" $ + testGroup "Golden: LockableFeature_team 9" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_15, "testObject_WithStatus_team_15.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_15, "testObject_LockableFeature_team_15.json") ], - testGroup "Golden: WithStatus_team 10" $ + testGroup "Golden: LockableFeature_team 10" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_16, "testObject_WithStatus_team_16.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_16, "testObject_LockableFeature_team_16.json") ], - testGroup "Golden: WithStatus_team 11" $ + testGroup "Golden: LockableFeature_team 11" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_17, "testObject_WithStatus_team_17.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_17, "testObject_LockableFeature_team_17.json") ], - testGroup "Golden: WithStatus_team 12" $ + testGroup "Golden: LockableFeature_team 12" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_18, "testObject_WithStatus_team_18.json"), - (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_19, "testObject_WithStatus_team_19.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_18, "testObject_LockableFeature_team_18.json"), + (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_19, "testObject_LockableFeature_team_19.json") ], testGroup "Golden: InvitationRequest_team" $ testObjects [(Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_1, "testObject_InvitationRequest_team_1.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_2, "testObject_InvitationRequest_team_2.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_3, "testObject_InvitationRequest_team_3.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_4, "testObject_InvitationRequest_team_4.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_5, "testObject_InvitationRequest_team_5.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_6, "testObject_InvitationRequest_team_6.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_7, "testObject_InvitationRequest_team_7.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_8, "testObject_InvitationRequest_team_8.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_9, "testObject_InvitationRequest_team_9.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_10, "testObject_InvitationRequest_team_10.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_11, "testObject_InvitationRequest_team_11.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_12, "testObject_InvitationRequest_team_12.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_13, "testObject_InvitationRequest_team_13.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_14, "testObject_InvitationRequest_team_14.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_15, "testObject_InvitationRequest_team_15.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_16, "testObject_InvitationRequest_team_16.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_17, "testObject_InvitationRequest_team_17.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_18, "testObject_InvitationRequest_team_18.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_19, "testObject_InvitationRequest_team_19.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_20, "testObject_InvitationRequest_team_20.json")], @@ -1321,14 +1310,38 @@ tests = testObjects [(Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_1, "testObject_DisableLegalHoldForUserRequest_team_1.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_2, "testObject_DisableLegalHoldForUserRequest_team_2.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_3, "testObject_DisableLegalHoldForUserRequest_team_3.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_4, "testObject_DisableLegalHoldForUserRequest_team_4.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_5, "testObject_DisableLegalHoldForUserRequest_team_5.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_6, "testObject_DisableLegalHoldForUserRequest_team_6.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_7, "testObject_DisableLegalHoldForUserRequest_team_7.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_8, "testObject_DisableLegalHoldForUserRequest_team_8.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_9, "testObject_DisableLegalHoldForUserRequest_team_9.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_10, "testObject_DisableLegalHoldForUserRequest_team_10.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_11, "testObject_DisableLegalHoldForUserRequest_team_11.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_12, "testObject_DisableLegalHoldForUserRequest_team_12.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_13, "testObject_DisableLegalHoldForUserRequest_team_13.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_14, "testObject_DisableLegalHoldForUserRequest_team_14.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_15, "testObject_DisableLegalHoldForUserRequest_team_15.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_16, "testObject_DisableLegalHoldForUserRequest_team_16.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_17, "testObject_DisableLegalHoldForUserRequest_team_17.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_18, "testObject_DisableLegalHoldForUserRequest_team_18.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_19, "testObject_DisableLegalHoldForUserRequest_team_19.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_20, "testObject_DisableLegalHoldForUserRequest_team_20.json")], testGroup "Golden: ApproveLegalHoldForUserRequest_team" $ testObjects [(Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_1, "testObject_ApproveLegalHoldForUserRequest_team_1.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_2, "testObject_ApproveLegalHoldForUserRequest_team_2.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_3, "testObject_ApproveLegalHoldForUserRequest_team_3.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_4, "testObject_ApproveLegalHoldForUserRequest_team_4.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_5, "testObject_ApproveLegalHoldForUserRequest_team_5.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_6, "testObject_ApproveLegalHoldForUserRequest_team_6.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_7, "testObject_ApproveLegalHoldForUserRequest_team_7.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_8, "testObject_ApproveLegalHoldForUserRequest_team_8.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_9, "testObject_ApproveLegalHoldForUserRequest_team_9.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_10, "testObject_ApproveLegalHoldForUserRequest_team_10.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_11, "testObject_ApproveLegalHoldForUserRequest_team_11.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_12, "testObject_ApproveLegalHoldForUserRequest_team_12.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_13, "testObject_ApproveLegalHoldForUserRequest_team_13.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_14, "testObject_ApproveLegalHoldForUserRequest_team_14.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_15, "testObject_ApproveLegalHoldForUserRequest_team_15.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_16, "testObject_ApproveLegalHoldForUserRequest_team_16.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_17, "testObject_ApproveLegalHoldForUserRequest_team_17.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_18, "testObject_ApproveLegalHoldForUserRequest_team_18.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_19, "testObject_ApproveLegalHoldForUserRequest_team_19.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_20, "testObject_ApproveLegalHoldForUserRequest_team_20.json")], + testGroup "Golden: RequestNewLegalHoldClientV0_team" $ + testObjects + [ (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClientV0_team_1, "testObject_RequestNewLegalHoldClientV0_team_1.json"), + (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClientV0_team_2, "testObject_RequestNewLegalHoldClientV0_team_2.json") + ], testGroup "Golden: RequestNewLegalHoldClient_team" $ - testObjects [(Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_1, "testObject_RequestNewLegalHoldClient_team_1.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_2, "testObject_RequestNewLegalHoldClient_team_2.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_3, "testObject_RequestNewLegalHoldClient_team_3.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_4, "testObject_RequestNewLegalHoldClient_team_4.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_5, "testObject_RequestNewLegalHoldClient_team_5.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_6, "testObject_RequestNewLegalHoldClient_team_6.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_7, "testObject_RequestNewLegalHoldClient_team_7.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_8, "testObject_RequestNewLegalHoldClient_team_8.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_9, "testObject_RequestNewLegalHoldClient_team_9.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_10, "testObject_RequestNewLegalHoldClient_team_10.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_11, "testObject_RequestNewLegalHoldClient_team_11.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_12, "testObject_RequestNewLegalHoldClient_team_12.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_13, "testObject_RequestNewLegalHoldClient_team_13.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_14, "testObject_RequestNewLegalHoldClient_team_14.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_15, "testObject_RequestNewLegalHoldClient_team_15.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_16, "testObject_RequestNewLegalHoldClient_team_16.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_17, "testObject_RequestNewLegalHoldClient_team_17.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_18, "testObject_RequestNewLegalHoldClient_team_18.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_19, "testObject_RequestNewLegalHoldClient_team_19.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_20, "testObject_RequestNewLegalHoldClient_team_20.json")], + testObjects + [ (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_1, "testObject_RequestNewLegalHoldClient_team_1.json"), + (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_2, "testObject_RequestNewLegalHoldClient_team_2.json") + ], testGroup "Golden: NewLegalHoldClient_team" $ testObjects [(Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_1, "testObject_NewLegalHoldClient_team_1.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_2, "testObject_NewLegalHoldClient_team_2.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_3, "testObject_NewLegalHoldClient_team_3.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_4, "testObject_NewLegalHoldClient_team_4.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_5, "testObject_NewLegalHoldClient_team_5.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_6, "testObject_NewLegalHoldClient_team_6.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_7, "testObject_NewLegalHoldClient_team_7.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_8, "testObject_NewLegalHoldClient_team_8.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_9, "testObject_NewLegalHoldClient_team_9.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_10, "testObject_NewLegalHoldClient_team_10.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_11, "testObject_NewLegalHoldClient_team_11.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_12, "testObject_NewLegalHoldClient_team_12.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_13, "testObject_NewLegalHoldClient_team_13.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_14, "testObject_NewLegalHoldClient_team_14.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_15, "testObject_NewLegalHoldClient_team_15.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_16, "testObject_NewLegalHoldClient_team_16.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_17, "testObject_NewLegalHoldClient_team_17.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_18, "testObject_NewLegalHoldClient_team_18.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_19, "testObject_NewLegalHoldClient_team_19.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_20, "testObject_NewLegalHoldClient_team_20.json")], + testGroup "Golden: LegalHoldServiceConfirmV0_team" $ + testObjects + [ (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirmV0_team_1, "testObject_LegalHoldServiceConfirmV0_team_1.json"), + (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirmV0_team_2, "testObject_LegalHoldServiceConfirmV0_team_2.json") + ], testGroup "Golden: LegalHoldServiceConfirm_team" $ - testObjects [(Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_1, "testObject_LegalHoldServiceConfirm_team_1.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_2, "testObject_LegalHoldServiceConfirm_team_2.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_3, "testObject_LegalHoldServiceConfirm_team_3.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_4, "testObject_LegalHoldServiceConfirm_team_4.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_5, "testObject_LegalHoldServiceConfirm_team_5.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_6, "testObject_LegalHoldServiceConfirm_team_6.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_7, "testObject_LegalHoldServiceConfirm_team_7.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_8, "testObject_LegalHoldServiceConfirm_team_8.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_9, "testObject_LegalHoldServiceConfirm_team_9.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_10, "testObject_LegalHoldServiceConfirm_team_10.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_11, "testObject_LegalHoldServiceConfirm_team_11.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_12, "testObject_LegalHoldServiceConfirm_team_12.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_13, "testObject_LegalHoldServiceConfirm_team_13.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_14, "testObject_LegalHoldServiceConfirm_team_14.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_15, "testObject_LegalHoldServiceConfirm_team_15.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_16, "testObject_LegalHoldServiceConfirm_team_16.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_17, "testObject_LegalHoldServiceConfirm_team_17.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_18, "testObject_LegalHoldServiceConfirm_team_18.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_19, "testObject_LegalHoldServiceConfirm_team_19.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_20, "testObject_LegalHoldServiceConfirm_team_20.json")], + testObjects + [ (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_1, "testObject_LegalHoldServiceConfirm_team_1.json"), + (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_2, "testObject_LegalHoldServiceConfirm_team_2.json") + ], + testGroup "Golden: LegalHoldServiceRemoveV0_team" $ + testObjects + [ (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemoveV0_team_1, "testObject_LegalHoldServiceRemoveV0_team_1.json"), + (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemoveV0_team_2, "testObject_LegalHoldServiceRemoveV0_team_2.json") + ], testGroup "Golden: LegalHoldServiceRemove_team" $ - testObjects [(Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_1, "testObject_LegalHoldServiceRemove_team_1.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_2, "testObject_LegalHoldServiceRemove_team_2.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_3, "testObject_LegalHoldServiceRemove_team_3.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_4, "testObject_LegalHoldServiceRemove_team_4.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_5, "testObject_LegalHoldServiceRemove_team_5.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_6, "testObject_LegalHoldServiceRemove_team_6.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_7, "testObject_LegalHoldServiceRemove_team_7.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_8, "testObject_LegalHoldServiceRemove_team_8.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_9, "testObject_LegalHoldServiceRemove_team_9.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_10, "testObject_LegalHoldServiceRemove_team_10.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_11, "testObject_LegalHoldServiceRemove_team_11.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_12, "testObject_LegalHoldServiceRemove_team_12.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_13, "testObject_LegalHoldServiceRemove_team_13.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_14, "testObject_LegalHoldServiceRemove_team_14.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_15, "testObject_LegalHoldServiceRemove_team_15.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_16, "testObject_LegalHoldServiceRemove_team_16.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_17, "testObject_LegalHoldServiceRemove_team_17.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_18, "testObject_LegalHoldServiceRemove_team_18.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_19, "testObject_LegalHoldServiceRemove_team_19.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_20, "testObject_LegalHoldServiceRemove_team_20.json")], + testObjects + [ (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_1, "testObject_LegalHoldServiceRemove_team_1.json"), + (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_2, "testObject_LegalHoldServiceRemove_team_2.json") + ], testGroup "Golden: TeamMember_team" $ testObjects [(Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_1, "testObject_TeamMember_team_1.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_2, "testObject_TeamMember_team_2.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_3, "testObject_TeamMember_team_3.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_4, "testObject_TeamMember_team_4.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_5, "testObject_TeamMember_team_5.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_6, "testObject_TeamMember_team_6.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_7, "testObject_TeamMember_team_7.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_8, "testObject_TeamMember_team_8.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_9, "testObject_TeamMember_team_9.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_10, "testObject_TeamMember_team_10.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_11, "testObject_TeamMember_team_11.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_12, "testObject_TeamMember_team_12.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_13, "testObject_TeamMember_team_13.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_14, "testObject_TeamMember_team_14.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_15, "testObject_TeamMember_team_15.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_16, "testObject_TeamMember_team_16.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_17, "testObject_TeamMember_team_17.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_18, "testObject_TeamMember_team_18.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_19, "testObject_TeamMember_team_19.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_20, "testObject_TeamMember_team_20.json")], testGroup "Golden: ListType_team" $ @@ -1354,81 +1367,81 @@ tests = (Test.Wire.API.Golden.Generated.VerificationAction_user.testObject_VerificationAction_user_3, "testObject_VerificationAction_user_3") ], testGroup - "Golden: WithStatusPatch_team 1" + "Golden: LockableFeaturePatch_team 1" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_1, "testObject_WithStatusPatch_team_1.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_1, "testObject_LockableFeaturePatch_team_1.json")], testGroup - "Golden: WithStatusPatch_team 2" + "Golden: LockableFeaturePatch_team 2" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_2, "testObject_WithStatusPatch_team_2.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_2, "testObject_LockableFeaturePatch_team_2.json")], testGroup - "Golden: WithStatusPatch_team 3" + "Golden: LockableFeaturePatch_team 3" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_3, "testObject_WithStatusPatch_team_3.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_3, "testObject_LockableFeaturePatch_team_3.json")], testGroup - "Golden: WithStatusPatch_team 4" + "Golden: LockableFeaturePatch_team 4" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_4, "testObject_WithStatusPatch_team_4.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_4, "testObject_LockableFeaturePatch_team_4.json")], testGroup - "Golden: WithStatusPatch_team 5" + "Golden: LockableFeaturePatch_team 5" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_5, "testObject_WithStatusPatch_team_5.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_5, "testObject_LockableFeaturePatch_team_5.json")], testGroup - "Golden: WithStatusPatch_team 6" + "Golden: LockableFeaturePatch_team 6" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_6, "testObject_WithStatusPatch_team_6.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_6, "testObject_LockableFeaturePatch_team_6.json")], testGroup - "Golden: WithStatusPatch_team 7" + "Golden: LockableFeaturePatch_team 7" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_7, "testObject_WithStatusPatch_team_7.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_7, "testObject_LockableFeaturePatch_team_7.json")], testGroup - "Golden: WithStatusPatch_team 8" + "Golden: LockableFeaturePatch_team 8" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_8, "testObject_WithStatusPatch_team_8.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_8, "testObject_LockableFeaturePatch_team_8.json")], testGroup - "Golden: WithStatusPatch_team 9" + "Golden: LockableFeaturePatch_team 9" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_9, "testObject_WithStatusPatch_team_9.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_9, "testObject_LockableFeaturePatch_team_9.json")], testGroup - "Golden: WithStatusPatch_team 10" + "Golden: LockableFeaturePatch_team 10" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_10, "testObject_WithStatusPatch_team_10.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_10, "testObject_LockableFeaturePatch_team_10.json")], testGroup - "Golden: WithStatusPatch_team 11" + "Golden: LockableFeaturePatch_team 11" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_11, "testObject_WithStatusPatch_team_11.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_11, "testObject_LockableFeaturePatch_team_11.json")], testGroup - "Golden: WithStatusPatch_team 12" + "Golden: LockableFeaturePatch_team 12" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_12, "testObject_WithStatusPatch_team_12.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_12, "testObject_LockableFeaturePatch_team_12.json")], testGroup - "Golden: WithStatusPatch_team 13" + "Golden: LockableFeaturePatch_team 13" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_13, "testObject_WithStatusPatch_team_13.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_13, "testObject_LockableFeaturePatch_team_13.json")], testGroup - "Golden: WithStatusPatch_team 14" + "Golden: LockableFeaturePatch_team 14" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_14, "testObject_WithStatusPatch_team_14.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_14, "testObject_LockableFeaturePatch_team_14.json")], testGroup - "Golden: WithStatusPatch_team 15" + "Golden: LockableFeaturePatch_team 15" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_15, "testObject_WithStatusPatch_team_15.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_15, "testObject_LockableFeaturePatch_team_15.json")], testGroup - "Golden: WithStatusPatch_team 16" + "Golden: LockableFeaturePatch_team 16" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_16, "testObject_WithStatusPatch_team_16.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_16, "testObject_LockableFeaturePatch_team_16.json")], testGroup - "Golden: WithStatusPatch_team 17" + "Golden: LockableFeaturePatch_team 17" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_17, "testObject_WithStatusPatch_team_17.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_17, "testObject_LockableFeaturePatch_team_17.json")], testGroup - "Golden: WithStatusPatch_team 18" + "Golden: LockableFeaturePatch_team 18" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_18, "testObject_WithStatusPatch_team_18.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_18, "testObject_LockableFeaturePatch_team_18.json")], testGroup - "Golden: WithStatusPatch_team 19" + "Golden: LockableFeaturePatch_team 19" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_19, "testObject_WithStatusPatch_team_19.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_19, "testObject_LockableFeaturePatch_team_19.json")], testGroup "Golden: Event_FeatureConfig" $ testObjects diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Activate_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Activate_user.hs deleted file mode 100644 index b6200b6fb76..00000000000 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Activate_user.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 Test.Wire.API.Golden.Generated.Activate_user where - -import Data.Text.Ascii (AsciiChars (validate)) -import Imports (Bool (False, True), fromRight, undefined) -import Wire.API.User (Email (Email, emailDomain, emailLocal), Phone (Phone, fromPhone)) -import Wire.API.User.Activation - ( Activate (..), - ActivationCode (ActivationCode, fromActivationCode), - ActivationKey (ActivationKey, fromActivationKey), - ActivationTarget (ActivateEmail, ActivateKey, ActivatePhone), - ) - -testObject_Activate_user_1 :: Activate -testObject_Activate_user_1 = - Activate - { activateTarget = ActivatePhone (Phone {fromPhone = "+45520903"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "HUUpJQ==")}, - activateDryrun = True - } - -testObject_Activate_user_2 :: Activate -testObject_Activate_user_2 = - Activate - { activateTarget = - ActivateKey (ActivationKey {fromActivationKey = fromRight undefined (validate "e3sm9EjNmzA=")}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "fg==")}, - activateDryrun = False - } - -testObject_Activate_user_3 :: Activate -testObject_Activate_user_3 = - Activate - { activateTarget = ActivatePhone (Phone {fromPhone = "+44508058"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "OAbwDkw=")}, - activateDryrun = True - } - -testObject_Activate_user_4 :: Activate -testObject_Activate_user_4 = - Activate - { activateTarget = ActivatePhone (Phone {fromPhone = "+97751884"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "811p-743Gvpi")}, - activateDryrun = False - } - -testObject_Activate_user_5 :: Activate -testObject_Activate_user_5 = - Activate - { activateTarget = ActivateEmail (Email {emailLocal = "\1002810\NUL\1075125", emailDomain = "k\\\SOHa\SYN*\176499"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "")}, - activateDryrun = False - } - -testObject_Activate_user_6 :: Activate -testObject_Activate_user_6 = - Activate - { activateTarget = ActivateEmail (Email {emailLocal = "\1104323i>\1007870Ha!", emailDomain = ""}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "FXrNll0Kqg==")}, - activateDryrun = False - } - -testObject_Activate_user_7 :: Activate -testObject_Activate_user_7 = - Activate - { activateTarget = ActivateKey (ActivationKey {fromActivationKey = fromRight undefined (validate "jQ==")}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "8yl3qERc")}, - activateDryrun = False - } - -testObject_Activate_user_8 :: Activate -testObject_Activate_user_8 = - Activate - { activateTarget = ActivatePhone (Phone {fromPhone = "+3276478697350"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "NF20Avw=")}, - activateDryrun = True - } - -testObject_Activate_user_9 :: Activate -testObject_Activate_user_9 = - Activate - { activateTarget = ActivateKey (ActivationKey {fromActivationKey = fromRight undefined (validate "DkV9xQ==")}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "61wG")}, - activateDryrun = True - } - -testObject_Activate_user_10 :: Activate -testObject_Activate_user_10 = - Activate - { activateTarget = ActivateKey (ActivationKey {fromActivationKey = fromRight undefined (validate "1szizA==")}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "kcvCq2A=")}, - activateDryrun = False - } - -testObject_Activate_user_11 :: Activate -testObject_Activate_user_11 = - Activate - { activateTarget = ActivateEmail (Email {emailLocal = "\ETX4\SUB", emailDomain = ""}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "MZpmmg==")}, - activateDryrun = False - } - -testObject_Activate_user_12 :: Activate -testObject_Activate_user_12 = - Activate - { activateTarget = ActivateKey (ActivationKey {fromActivationKey = fromRight undefined (validate "V3mr5D4=")}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "sScBopoNTb0=")}, - activateDryrun = True - } - -testObject_Activate_user_13 :: Activate -testObject_Activate_user_13 = - Activate - { activateTarget = - ActivateKey (ActivationKey {fromActivationKey = fromRight undefined (validate "haH9_sUNFw==")}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "ysvb")}, - activateDryrun = False - } - -testObject_Activate_user_14 :: Activate -testObject_Activate_user_14 = - Activate - { activateTarget = ActivatePhone (Phone {fromPhone = "+13340815619"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "hQ==")}, - activateDryrun = True - } - -testObject_Activate_user_15 :: Activate -testObject_Activate_user_15 = - Activate - { activateTarget = - ActivateEmail (Email {emailLocal = "\22308W[\1041599G\996204]{\n", emailDomain = " V8\992253\NAK*"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "biTZ")}, - activateDryrun = False - } - -testObject_Activate_user_16 :: Activate -testObject_Activate_user_16 = - Activate - { activateTarget = ActivatePhone (Phone {fromPhone = "+77635104433"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "5W4=")}, - activateDryrun = True - } - -testObject_Activate_user_17 :: Activate -testObject_Activate_user_17 = - Activate - { activateTarget = ActivatePhone (Phone {fromPhone = "+556856857856"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "ShjEcgx6P0Hs")}, - activateDryrun = False - } - -testObject_Activate_user_18 :: Activate -testObject_Activate_user_18 = - Activate - { activateTarget = - ActivateEmail (Email {emailLocal = "2\1107376B\1099134\ETX2\US\1080331", emailDomain = "v\SOH\SO\1007855/e"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "xRvktQ==")}, - activateDryrun = False - } - -testObject_Activate_user_19 :: Activate -testObject_Activate_user_19 = - Activate - { activateTarget = ActivateKey (ActivationKey {fromActivationKey = fromRight undefined (validate "1fCrdg==")}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "")}, - activateDryrun = False - } - -testObject_Activate_user_20 :: Activate -testObject_Activate_user_20 = - Activate - { activateTarget = ActivatePhone (Phone {fromPhone = "+893051142276"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "7PtclAevMzA=")}, - activateDryrun = False - } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ActivationResponse_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ActivationResponse_user.hs index 020c4119ddd..05e0dc390d1 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ActivationResponse_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ActivationResponse_user.hs @@ -27,19 +27,19 @@ testObject_ActivationResponse_user_1 = { activatedIdentity = SSOIdentity (UserSSOId mkSimpleSampleUref) - (Just (Email {emailLocal = "\165918\rZ\a\ESC", emailDomain = "p\131777\62344"})), + (Just (unsafeEmailAddress "some" "example")), activatedFirst = False } testObject_ActivationResponse_user_2 :: ActivationResponse testObject_ActivationResponse_user_2 = - ActivationResponse {activatedIdentity = EmailIdentity (Email "foo" "example.com"), activatedFirst = False} + ActivationResponse {activatedIdentity = EmailIdentity (unsafeEmailAddress "some" "example"), activatedFirst = False} testObject_ActivationResponse_user_3 :: ActivationResponse testObject_ActivationResponse_user_3 = ActivationResponse { activatedIdentity = - EmailIdentity (Email {emailLocal = "\10031*;'R\EM\SI\1032685\1041167", emailDomain = "Gw:[T8\34437"}), + EmailIdentity (unsafeEmailAddress "some" "example"), activatedFirst = False } @@ -47,7 +47,7 @@ testObject_ActivationResponse_user_4 :: ActivationResponse testObject_ActivationResponse_user_4 = ActivationResponse { activatedIdentity = - EmailIdentity (Email {emailLocal = "h\nPr3", emailDomain = ""}), + EmailIdentity (unsafeEmailAddress "some" "example"), activatedFirst = True } @@ -55,7 +55,7 @@ testObject_ActivationResponse_user_5 :: ActivationResponse testObject_ActivationResponse_user_5 = ActivationResponse { activatedIdentity = - EmailIdentity (Email {emailLocal = "7\1042098m\95296\b\1098765", emailDomain = "AJX*s&\173117\988870p"}), + EmailIdentity (unsafeEmailAddress "some" "example"), activatedFirst = False } @@ -69,19 +69,19 @@ testObject_ActivationResponse_user_6 = testObject_ActivationResponse_user_7 :: ActivationResponse testObject_ActivationResponse_user_7 = ActivationResponse - { activatedIdentity = EmailIdentity (Email {emailLocal = "\98670", emailDomain = ""}), + { activatedIdentity = EmailIdentity (unsafeEmailAddress "some" "example"), activatedFirst = True } testObject_ActivationResponse_user_8 :: ActivationResponse testObject_ActivationResponse_user_8 = - ActivationResponse {activatedIdentity = EmailIdentity (Email "bar" "example.com"), activatedFirst = True} + ActivationResponse {activatedIdentity = EmailIdentity (unsafeEmailAddress "some" "example"), activatedFirst = True} testObject_ActivationResponse_user_9 :: ActivationResponse testObject_ActivationResponse_user_9 = ActivationResponse { activatedIdentity = - EmailIdentity (Email {emailLocal = "\ENQ?", emailDomain = ""}), + EmailIdentity (unsafeEmailAddress "some" "example"), activatedFirst = False } @@ -89,6 +89,6 @@ testObject_ActivationResponse_user_10 :: ActivationResponse testObject_ActivationResponse_user_10 = ActivationResponse { activatedIdentity = - EmailIdentity (Email {emailLocal = "\ACK3", emailDomain = "\f\1040847\1071035\EOT\1003280P\DEL"}), + EmailIdentity (unsafeEmailAddress "some" "example"), activatedFirst = False } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/BindingNewTeamUser_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/BindingNewTeamUser_user.hs index 37dc8807bf7..d8151e07736 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/BindingNewTeamUser_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/BindingNewTeamUser_user.hs @@ -24,36 +24,23 @@ import Data.UUID as UUID import Imports (Maybe (Just, Nothing), fromJust) import Wire.API.Asset import Wire.API.Team - ( BindingNewTeam (BindingNewTeam), - Icon (..), - NewTeam - ( NewTeam, - _newTeamIcon, - _newTeamIconKey, - _newTeamMembers, - _newTeamName - ), - ) import Wire.API.User (BindingNewTeamUser (..)) testObject_BindingNewTeamUser_user_1 :: BindingNewTeamUser testObject_BindingNewTeamUser_user_1 = BindingNewTeamUser { bnuTeam = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "\fe\ENQ\1011760zm\166331\&6+)g;5\989956Z\8196\&41\DC1\n\STX\ETX%|\NULM\996272S=`I\59956UK1\1003466]X\r\SUBa\EM!\74407+\ETXepRw\ACK\ENQ#\127835\1061771\1036174\1018930UX\66821]>i&r\137805\1055913Z\1070413\&6\DC4\DC4\1024114\1058863\1044802\ESC\SYNa4\NUL\1059602\1015948\123628\tLZ\ACKw$=\SYNu\ETXE1\63200C'\ENQ\151764\47003\134542$\100516\1112326\&9;#\1044763\1015439&\ESC\1026916k/\tu\\pk\NUL\STX\1083510)\FS/Lni]Q\NUL\SIZ|=\DC1V]]\FS5\156475U6>(\17233'\CAN\179678%'I1-D\"\1098303\n\78699\npkHY#\NUL\1014868u]\1078674\147414\STX\USj'\993967'\CAN\1042144&\35396E\37802=\135058Da\STX\v\1100351=\1083565V#\993183\RS\FSN#`uny\1003178\1094898\&53#\DEL/|,+\243pW\44721i4j", - _newTeamIcon = DefaultIcon, - _newTeamIconKey = - Just - ( unsafeRange - "\ACKc\151665L ,\STX\NAK[\SUB\DC1\63043\GSxe\1000559c\US\DC4<`|\29113\147003Q\1028347\987929<{\NUL^\FST\141040J\1071963U\EOT\SYN\65033\DC3G\1003198+\EM\181213xr\v\32449\ESCyTD@>Ou\70496j\43574E\STX6e\983711\SO\ESC\135327\&34\1063210\41000\1018151\&8\1057958\163400uxW\41951\1080957Y\ACK\141633(\CAN\FS$D\1055410\148196\36291\SI3\1082544#\SYN?\ETX\ACK0*W3\ACK\1085759i\35231h\NAK-\42529\1034909\ACKH?\\Tv\1098776\54330Q\46933\DLE-@k%{=4\SUB!w&\1042435D\DC2cuT^\DC4\GSH\b\137953^]\985924jXA\1010085\133569@fV,OA\185077\38677F\154006Az^g7\177712),C\1020911}.\72736\996321~V\1077077\1024186(9^z\1014725\67354\&3}Gj\1078379\fd>\57781\1088153Y\177269p#^\1054503L`S~\1101440\DC23\EOT\145319\24591\92747\13418as:F\ETX" - ), - _newTeamMembers = Nothing - } - ), + NewTeam + { newTeamName = + unsafeRange + "\fe\ENQ\1011760zm\166331\&6+)g;5\989956Z\8196\&41\DC1\n\STX\ETX%|\NULM\996272S=`I\59956UK1\1003466]X\r\SUBa\EM!\74407+\ETXepRw\ACK\ENQ#\127835\1061771\1036174\1018930UX\66821]>i&r\137805\1055913Z\1070413\&6\DC4\DC4\1024114\1058863\1044802\ESC\SYNa4\NUL\1059602\1015948\123628\tLZ\ACKw$=\SYNu\ETXE1\63200C'\ENQ\151764\47003\134542$\100516\1112326\&9;#\1044763\1015439&\ESC\1026916k/\tu\\pk\NUL\STX\1083510)\FS/Lni]Q\NUL\SIZ|=\DC1V]]\FS5\156475U6>(\17233'\CAN\179678%'I1-D\"\1098303\n\78699\npkHY#\NUL\1014868u]\1078674\147414\STX\USj'\993967'\CAN\1042144&\35396E\37802=\135058Da\STX\v\1100351=\1083565V#\993183\RS\FSN#`uny\1003178\1094898\&53#\DEL/|,+\243pW\44721i4j", + newTeamIcon = DefaultIcon, + newTeamIconKey = + Just + ( unsafeRange + "\ACKc\151665L ,\STX\NAK[\SUB\DC1\63043\GSxe\1000559c\US\DC4<`|\29113\147003Q\1028347\987929<{\NUL^\FST\141040J\1071963U\EOT\SYN\65033\DC3G\1003198+\EM\181213xr\v\32449\ESCyTD@>Ou\70496j\43574E\STX6e\983711\SO\ESC\135327\&34\1063210\41000\1018151\&8\1057958\163400uxW\41951\1080957Y\ACK\141633(\CAN\FS$D\1055410\148196\36291\SI3\1082544#\SYN?\ETX\ACK0*W3\ACK\1085759i\35231h\NAK-\42529\1034909\ACKH?\\Tv\1098776\54330Q\46933\DLE-@k%{=4\SUB!w&\1042435D\DC2cuT^\DC4\GSH\b\137953^]\985924jXA\1010085\133569@fV,OA\185077\38677F\154006Az^g7\177712),C\1020911}.\72736\996321~V\1077077\1024186(9^z\1014725\67354\&3}Gj\1078379\fd>\57781\1088153Y\177269p#^\1054503L`S~\1101440\DC23\EOT\145319\24591\92747\13418as:F\ETX" + ) + }, bnuCurrency = Just XUA } @@ -61,19 +48,16 @@ testObject_BindingNewTeamUser_user_2 :: BindingNewTeamUser testObject_BindingNewTeamUser_user_2 = BindingNewTeamUser { bnuTeam = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "G\EOT\DC47\1030077bCy\83226&5\"\96437B$\STX\DC2QJb_\15727\1104659Y \156055\1044397Y\1004994g\v\991186xkJUi\1028168.=-\1054839\&2\1113630U\ESC]\SUB\1091929\DLE}R\157290\DC1\1111740\1096562+R/\1083774\170894p(M\ENQ5Fw<\144133E\1005699R\DLE44\1060383\SO%@FPG\986135JJ\vE\GSz\RS_\tb]0t_Ax}\rt\1057458h\DC3O\ACK\991050`\1038022vm-?$!)~\152722bh\RS\1011653\1007510\&0x \1092001\1078327+)A&mRfL\1109449\ENQ\1049319>K@\US\1006511\ab\vPDWG,\1062888/J~)%7?aRr\989765\&4*^\1035118K*\996771\EM\"\SO\987994\186383l\n\tE\136474\1037228\NAK\a\n\78251c?\\\ENQj\"\ESCpe\98450\NUL=\EM>J", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = - Just - ( unsafeRange - "-\ACK\59597v^\SOH_>p\13939\ETX\SYN\EOT\ENQ\2922\1080262]\45888\917616\SI;v}q\47502\190968\a\SI\1113366&~\51980<\GS\1024632`,\1033586sn\2651H\160130\1100746\176758:qNi]\1051932'\1000100#\a#T\171243}\990743\DC2\1008291M_\FS\DC4\988716\1091854\EM,\SO\CAN^]\77867\&9\1112574-\a\SOHID. FAp\EOT\1033411\1004852(S\1052010\68416\129120\DLEsI\ETXe|Mv-\"q\49103zM\14348$H\SOH\139130\1004399D]\SUB\1056469\ESC\151220qW2\ENQ\1104272\RSy\1018323gg\1018839 /\1079527\98975\18928~&y\b\ACK\1084334\1047493\36198\SO\FS\SYN\RSt\\a.V\SO\&Hy8k\US$O\699Xu/=" - ), - _newTeamMembers = Nothing - } - ), + NewTeam + { newTeamName = + unsafeRange + "G\EOT\DC47\1030077bCy\83226&5\"\96437B$\STX\DC2QJb_\15727\1104659Y \156055\1044397Y\1004994g\v\991186xkJUi\1028168.=-\1054839\&2\1113630U\ESC]\SUB\1091929\DLE}R\157290\DC1\1111740\1096562+R/\1083774\170894p(M\ENQ5Fw<\144133E\1005699R\DLE44\1060383\SO%@FPG\986135JJ\vE\GSz\RS_\tb]0t_Ax}\rt\1057458h\DC3O\ACK\991050`\1038022vm-?$!)~\152722bh\RS\1011653\1007510\&0x \1092001\1078327+)A&mRfL\1109449\ENQ\1049319>K@\US\1006511\ab\vPDWG,\1062888/J~)%7?aRr\989765\&4*^\1035118K*\996771\EM\"\SO\987994\186383l\n\tE\136474\1037228\NAK\a\n\78251c?\\\ENQj\"\ESCpe\98450\NUL=\EM>J", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = + Just + ( unsafeRange + "-\ACK\59597v^\SOH_>p\13939\ETX\SYN\EOT\ENQ\2922\1080262]\45888\917616\SI;v}q\47502\190968\a\SI\1113366&~\51980<\GS\1024632`,\1033586sn\2651H\160130\1100746\176758:qNi]\1051932'\1000100#\a#T\171243}\990743\DC2\1008291M_\FS\DC4\988716\1091854\EM,\SO\CAN^]\77867\&9\1112574-\a\SOHID. FAp\EOT\1033411\1004852(S\1052010\68416\129120\DLEsI\ETXe|Mv-\"q\49103zM\14348$H\SOH\139130\1004399D]\SUB\1056469\ESC\151220qW2\ENQ\1104272\RSy\1018323gg\1018839 /\1079527\98975\18928~&y\b\ACK\1084334\1047493\36198\SO\FS\SYN\RSt\\a.V\SO\&Hy8k\US$O\699Xu/=" + ) + }, bnuCurrency = Nothing } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/BindingNewTeam_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/BindingNewTeam_team.hs deleted file mode 100644 index 8f97737dfab..00000000000 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/BindingNewTeam_team.hs +++ /dev/null @@ -1,353 +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 Test.Wire.API.Golden.Generated.BindingNewTeam_team where - -import Data.Id (Id (Id)) -import Data.Range (unsafeRange) -import Data.UUID qualified as UUID (fromString) -import Imports (Maybe (Just, Nothing), fromJust) -import Wire.API.Asset (AssetKey (..), AssetRetention (..)) -import Wire.API.Team - ( BindingNewTeam (..), - Icon (..), - NewTeam - ( NewTeam, - _newTeamIcon, - _newTeamIconKey, - _newTeamMembers, - _newTeamName - ), - ) - -testObject_BindingNewTeam_team_1 :: BindingNewTeam -testObject_BindingNewTeam_team_1 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "UivH&\54922\98185p\USz\11724\r$\DC4j9P\r\"\1070851\3254\986624aF>E\1078807\139041B\EM&\1088459\DC4\174923+'\1103890R;!\GS\1017122\SIvv|\rmbGHz\1005234\95057\&3h\120904\\U|'\ETX;^&G\CAN\f\41076\&42\teq\1049559\SOV1}\RSaT\1014212aO7<;o\179606\f\1111896m)$PC\ESC7;f{\STXt\9533>\EOTX@4|/\tH\ENQ/D\144082\EM\121436C\99696Q\ENQT\1096609?d\ACK\1073806#H\127523\139127*\166004jo4wa\95243leQ*\1000542\1034344>@,\1045947\190894RF4QcNY96\168531\1051528G\1069460&J\\TzHUiG.C\SUB&\FSx\52616\167921\&3\1105098A\1054008B)\29142\31346r\1004296\ENQ&VCPa{\SOH\EMW\DEL\43500\97305\DLE/\1078579\SIc:b\SOH\132266)\35144\1100498\37490@5\983688I02g%%1bJl} :\1021555\SYN\64090\158870\143049" - ), - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_3 :: BindingNewTeam -testObject_BindingNewTeam_team_3 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "\SUB_F\n\65091\140672\DC2>\1079041\74636t\n)1/% hL\DC2Ad\SOHXq6\DC1)\NUL\f6\fV\DC4r\1097128\DC1n\1107359,@\171217\118996\n\SUB%N\176824\ACK\33856Xv)\SYNz?\DC4\EMY\162050\&2\95792um8}\51420\DC2yW\NULHQ\ENQD[Fe\nk\999106\EM\25079Yk@##u}j\169850\153342\STXq\ESCir7) \27756%\1016104~\993971\&8\1085984je\1099724\&0*Gi3\120829je\CANQr>\1033571k1\63774c\1031586L\1015084\93833t\EOTW\999363\SUBo\fgh\ACK\172057C2\38697c\SUB)uW\r\fB\1042942Sf\SUB\SOH*5l\38586\SI\25991\EMB(\ENQ\133758/)!{\1006380\&9\STXA\DEL\16077fx&\180089T&\187029\DC4\52222[\r\v\n\1071241j2\166180/\1086576\ENQQo\fj\134496\129296\nb6\CAN3\RS9\EM\1000086ub\ETB3CY\GSsIz", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "3d4b563b-016c-49da-bad2-876ad0a5ecd2"))) AssetExpiring), - _newTeamIconKey = - Just - ( unsafeRange - "\FS\RSP\988567Gt\SYN-\47148nJ\1010840g^\n\r\177791\GSR\1010061q\988754\nQ\RS\1054014\GS#w\147936\171735\1064959\136621B\DC4\SUBLv\"S>\121093!]sB+6\DC1oc\ETB7\34513lR\95866\EMr%E\1077999B\98708A\1067109N\ETB?{\1065508/|cU\60733\141259]\92896\1102284\DLE\147332\1075446+\991438\t$F\96714he4\166964|k/!5Z~\83246\ETB\1017589\SOH\ENQ\1056989\&3E!{^\33558\&4fh\1029576N\1111705v\f\GS\998029mde!5\1027807y&\1062155xo,\STXrk\1071672\ENQ\SOHJoS\986695X\18929\994879a\991047\RS\1046020\EM\SOH3j\3901Z4\DC4\1068579l\52972n\ESC@ve#\SYN\GS\183587P4\1077298\ESC\170211:\157706z1*\USs\vd`\1059621/\39172\165682" - ), - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_6 :: BindingNewTeam -testObject_BindingNewTeam_team_6 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "v\188076hEWefuu\1006804jPx\158137k#\SOH\986725\STX\ETX^\ESC\n\CAN\8325p1D|S1\1064991\1102106\29079\SYN`\t0g\1034469,t\FSw\fDT\RS#H\SOH\145176\US{\1091499\1025650\984364lW\a,uil\SIN`5e:\SYN Y!\SYN\1025115tb\1085213", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "d7a467c6-8cd4-40cb-9e30-99b64bb11307"))) AssetEternal), - _newTeamIconKey = - Just - ( unsafeRange - "+&heN\1091941K\f_k\DLE(\33970\DC3\9833M\f\1029853\1098178\SI^s\1101855Ga,$\38078\SIb\DC3\f\"s{\ACK5\1025293\5649\US\DLE\SUB\1085641\70123\CAN,\1036517\158007\DC4 \1109215P\95245|f.>hEa\DLE^\ENQ\b]`\1112948<\GSZG\1004098\SOH\190360\24273*8p\FSF@OLpnXTmW\96553f\68110\1076109\25954Ze1 \SYNEm\27765f\ACK\987143" - ), - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_7 :: BindingNewTeam -testObject_BindingNewTeam_team_7 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "\145552\1042892iz\1057971FT\14964;\1108369}\188917\1113471\&9\SO\991633\&7>hAC\NULH2O\177259m\187711\&2R(?W,=,\990725M\992456\aM\194790\SUB\47600q\SOlj\EOTj^.s~\rY%5lM,\26492=\ACK\1016899\188843>{\CAN\DLE\15878f=X9\SYN9\51145\159419TI4\17599\v\NAK6\1014936/\DLE\NAK\ACK\23564H<\ENQ\1029703e\ENQz\1017528:\6137\"rS\a\167660\FS\ETX\1059289\1031786\49012\DC4\DC4Q\"\1065200\&1:\1097556\UST.;\1042663\18380}", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "b199431c-e2ee-48c6-8f1b-56726626b493"))) AssetEternal), - _newTeamIconKey = - Just - ( unsafeRange - "D\RS\168552\SOH\1033444\128689Ll\GS\tW\1056953o\CAN\47716b\ETX|\US*=\1011088\1066392\988391\&6\999812" - ), - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_8 :: BindingNewTeam -testObject_BindingNewTeam_team_8 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "YwD\1023517r\NAK}\1083947\ACK\1047823\29742\EOT\1071030iI5g\1012255\t\"r\150087O\DC4?\53005\1100290\1108960\NUL\1060304qgg\DC1X)\NULL\1054528\CAN{\v4\NUL\93999\bvD#\1035811$aYFk\b\1102040\1089491\1042733\47133:1\179810S7\66745V)\1072087\v\96989\&3#\b\1104899c\27119Q/jPy\1015620P@Df\997914\51756H\1113361Xr\SO\ETB3%\1108760aF@3A\SI\ETB\STX mj9T=\DC3'XI\DC2?0\1093231\156858VHp?\1066163YU\42092\33083\72810,)\1113424\ETX96\153338z\42445/4T\136162\ESC\60427\1086321&\ETBS\1098748\14578z[\54638Z\DC2\"e\SUB\173931&rQ\fJG\100066\180037\155435s$\SUB$\50544S\162554E\ETX*\t+\63443WU*\144654\1042128\&8\NAK\999184a\t\EM\1097907_\DELOD\1006385/\23998\1100140SmfX", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = - Just - ( unsafeRange - "v\70188\46459h\SOH_\991979\DC3\ACKi\1000164\DC1\ETXW\72785\35679\DC2\23266\1026390\EOT\f%_\1064553\GS\SYN\ETB N\NULF\1005467\ENQLUua3\1089232M\8605\"\94879\SOH\RS\n-='\DC1B#\FS\136881>\DC3\132340\SI\GS\1088106G7v6w Z\4678\1051054\182628\170805\ESCP>\131111\1051383\1076729\v}?\5316Jg\SOH\SUB^pl\1101671\&2.\SOV\57380\DC3\22371\64509\ENQB\1045499\1076733\139492<\f\DEL2\19252Tz@6\DC3\71851x?\150161\36913\b\DLE\CANp\1081584\SYN\ETXN\1099776C\SI\SUB\DC1l]R\NULvL\1027446Nz\f-bf}f>\STXH\EM\136484+Zo\1034706\1062880\NAK}\adb\171356-\\-1\DC42\1046344\DC2\78894\&1/\33084b:\ENQ\1038950;Mw\FS\183866\1113547ITuy\1050264`SP\SOH\SO\GS\NAK\a\r7M\1069326\1064150\18615\n\SYN3V\ETXR\n1$e.\1096261B~yd_z\1047817\rV\1091351\RS\SYN\165050l\DC3\47200u\1058674u\"\aTc|sEw\1011190wTC|F\4735B\t\DC4&\bUEN(+M\SOF;\1099746\134573\EM20\nrPW\1017058$\1064809", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = - Just - ( unsafeRange - "X\1019453;\ENQW\ACKLk\996110\144662\ETB\n]\58553[~\10280&U\20125v`I\ETB\USl\983659\t\1090302?\17227KM3c\1067581\1030643= \ETBt5vKOg\NAK/NC2~i'\1062772Ojb\b\ETX\62742\1090035\DC1\SOH\NULFWc\1014613sU>P\SOH~\EMwUHU\SO#\55006\1081711!Nwn\1005601e\SOH\SUB\f\ETX\ETBT\DELl\110629BYU;a\1012448K7?,m\154276Xpa\48825\138301\EM ,M!~^g6}(\60133\36369\RS\8075gX}\161019)c\n\SOH2E" - ), - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_10 :: BindingNewTeam -testObject_BindingNewTeam_team_10 = - BindingNewTeam - ( NewTeam - { _newTeamName = unsafeRange "\b \SOH+\1056054;\t095\42390\n\STX2J\1002251\DC1UzD_\1110746\FS", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = - Just - ( unsafeRange - "\EOT\131569\ETB:\984737HL\SOH^bs\vG\157476{I\1096053]-J\FS\1107927\vs9\DLE\1000765vI`N\48159MZz" - ), - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_11 :: BindingNewTeam -testObject_BindingNewTeam_team_11 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "\48005H\1082536\132304\157763\&5\RS\986337-\NAK\ESCR\nL\63954&bD\139428\SUBH\US\1040918\f\t;e\1064224\47101\tc\1087740e\1099415\DLE\ETX\DELI\65746\ETB\133884\SUB \SI\43795~FE\CAN6\162836\DEL\46062u\"\135684\1041611\FSFYI\t/{\ENQ\RS]j\1076782\US22\15884l\42366$\ETB\US\180023kL{\STX*\131382RMj\ESC\1091332W3H\1020399\FS\NAK^\"5\29653\32539*\1099111", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = - Just - ( unsafeRange - "\1109507I\ACK.\158786@y0\DLE\1083101n\\#skj\1019405Y_\1037580&x\1007219\GS\SIy\1104457B\SYN0\DC3VP1\1086698q\1024822\1081753\28211R\1100307*+\RS,MP\27076*;\n\NAK\47211\t\160463\nGj.\41290\1104539l\12622\FS\61112~\1076042\NUL.\1083842&\SOH}\SI\1080986\DC1+f^ZC\a'T\SOH\n\1020923\1097319U\1107987`W\r\\fX\n\1095366TF\1108756`h\97424[\46315ERdP5<<\1024109;\r\1095899\NULDy\28422\&5N/^\136134(\DC3\1045067\1061604\&6e\f:\SIB\DLEF-\1110200\17393\1064949Rfb\44582\aDrB\987948\13740\26738\NUL+\60859\&2.\a\a}\NAKpsFw\ETB\DC3 \186007\151693k~" - ), - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_12 :: BindingNewTeam -testObject_BindingNewTeam_team_12 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - ";\110872M\EOT\164161P]'\1041089\1094514\4118\1054714iFnRQV\43238@\992926\59902l\1099067\aKZ{\51124S\190890\fg*\n,`!V\STX\991695e'\1039967\SO0\37019p4d\STXs\1020471uK(c'\52929hjB\144953\SOt'h^\SYN\SYN0\1009487_\12064\166805thH\SI\1073479:\1019934l; n4c\1101781D[\1014388\&8Y+\1092407\EOTE\1058506\\0\168273KKTc)P1K\1042475\990753W\ETX<|\24888\&0|5{Y\986771M\DC4\vK\DLE\1089150\SOH\DC4\1013653.\ETBg\991717\DLE\"W\NUL9&0yYZ\1094524\v\11606\58174", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = - Just - ( unsafeRange - "\"C\ESC\SI0\ETB\69608p\12616|/O]\53852\SO \55172C\SYNN\SUB8\NUL\62584BxtH\SO*\1077819\&3.\1061851(\1100810w\GS\152525R{q\990825\&4\180037\150457:\187092\134288>\ETB\nl\1061158g\"\996841,6K\28384\1054272[\1019005\1016209N\24221eB!\188918C\EOT\STXX#El\ETB`\61337e \1096702\ACK\ETXPB\DELC\1111118fa\178975" - ), - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_13 :: BindingNewTeam -testObject_BindingNewTeam_team_13 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "G\DEL\51831\70681rLb<\1056047!\RS|RD\161793\ACK\82958\164863\45602Ag\22680 \vy`\v\1045283K\13763e\18467,\144933DQEO\RS|\SI\1076051\1063435gr\1113276\NUL\n*1\47081R\SO\66829-Y\1037937n\1085668]])\1086075C\DC3\146455\"M@(K\15234\RS1\35575\FS\SUB\1025798T?}\SO=*\184770\n\69897\v_\"7\1064561?Lk\150200x\DC4bu:\146992\14577\1036009<\1015572\&6\SO`\1071314U\51409yp\183322\&7%", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = - Just - ( unsafeRange - "o\64661\1052808\SI[aoM\GS\1110611}q\36535\&4^\ETB-*%\148361\&8\1067531`\1070936#pH}\DC3?w`A/\94009\1108569\995072 \1104313\nX\40987\997490\DC3u\RS\SOH(\1041586\1006481\&6\STX]t{\DC4\";*\r\12492q\1066003\12213\63338+w&\31533(3#\180761PY]\RSf\\?F4\SUB\UST\1108579Rnfq%\66873p\154120\182326j\127981\&0P\bn\SO\FS\t\19400\nN.aGx" - ), - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_14 :: BindingNewTeam -testObject_BindingNewTeam_team_14 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "2#\DC2N\b9&A\1030886ZL{f\1011542M\1101172\23517\a\DELv\164961\32470\ACKT7\DC3\DC4\1009557O\1103393C\152202\t\DC4l\RS\SOH]\ESC\ACK\95718X;\149660* &\97401}\1111236T\ESCCLkx,\DLE\63803\nbT\1049269fWJ\992800\136973a\US`\DC3\139728\28948\&8r2']\NAK\DC2\133094\nl\DC2NXB\ENQia\1068046]B\989632\DLE\ENQdf#\64677\t6g\FS\SOH\1029760Fp(\GSQTZ\1015396\8630\153801dUJt\SI\EM\194705`\\#g0Qed@a${=Q.\1048388Ld`\35027 \173216sV\SUB\SO5\150360\41997\1107813i\EM\DC3\988956\1049486\SOH\1030355>\1044179\DC3w\1001979Y}\21603\&1q\NAKY:\25626q \ETB=*#\74975\EM\61277\\\21887y9Tfc\DC1\49327k\1096646\\Oxxn&6NtaZ?k:5G@\46350\DC3H\1097149hu4\178807\995883\USR\161801\1024517v\26381\23905\72161\12881\ACKD\985152[bb<\1111873", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = Nothing, - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_15 :: BindingNewTeam -testObject_BindingNewTeam_team_15 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - ":\44335R_.\4189\v;\t\1039296-\5484PN\r[\32934\SUBY\1102645<\60542\1083602\aW\1099269@\183771\162143\172579\biU\1005268b\DLE=\t8+\993285\1090143\1018670\1107684>\ACK1\bZQ7fmQOQ\986711l!\DC3\44018\27476*\43689*1\f\1097293\&8nk|\NAK\1005998~\fO\162989\100863!:3\ETXn{%\6663\182700if/!\29917] <\1056176Y\1078680\b\DC4~\t\EM\SOH<*\NAK\143397bx4 {\96203\CANVs;g\98929\144388\STXqkI!QJ\1072302J\189512\DC4\64545?_\STX\t\1082190iB3YdKA7@>Q\995699\987049]\1094644\133325>D\1026819wD\ESC|\SI'^\136789\120874Q#q,\"", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = - Just - ( unsafeRange - "\SOH]rj\1053405eA\1046358\tbj\EMk\DC1l\n\988481H~]u\42907\1029099!kjVS{42\NULE?\EMh\61474\35112B!:\DLEX\DC1T\DEL3W\avimhK\1078443\DC1to*P*\DC1}\986362\1081249H\r\1034017B", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = Nothing, - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_17 :: BindingNewTeam -testObject_BindingNewTeam_team_17 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "|\36324P\US\1040589\159812Y\SOHj\RSYrr\49743\&0m\ENQ\1027954*'\72098\1105368P6\SYN\15236\f\DC2\125109e\1031690\RS\1026891\1003083\69946\rA'\GSA\NAK\53778\1067566J\1016490'T\1037603R2? \FS\US\1032454$\NAKGr(\1008673{\ENQ\62451\&0mJ\SID\STX-\CAN_I\132366\f\147665\FSR\1080205hp\143954B6W2\b\f6\1104867\DC2\180998\b1'7-T-#\3953D\1076345\1082129T]v$Gl\1042148\1032818\&5yg\1025280\nQc.`i\14819\24538}\FS&k4\99627\ACK>#\32013\1036954\EM\131987[vBOPu\1108963@\ACK\NUL\1087882\147841\SO\NAK\98755\31702\EOT\ETX&\1032348?z\989374i\fz\n\1029119\ETB3\a\1108955W\1113557E^\1043345\986117S3'4\ACK\74144*m-\ESC4\USj\ETX__6\1046371\6580M\48069\ESC]\EOTDq\DLEuo\28030$\vUWp1=/o\ETBY\173686\&9\DC2\nQ\177317\1051037)\1102455\1010761\NAKaR\145135;\52151\SOH\EM\na\nvt\133143\ETXa\140630 J\134658uX\1077113?Wz&<\DC4C\fx`\1038161#\SI\194737\37045\43620\RS\STX#\SYN\DC4-Oj\EOTd\1037772'FoHqexoh\SUBx\1106683\184912\bi\998453yr\SI\1064751w\1104226\n8T\1008339\&2'\1024124\1110758\1103037\RSnxW[\26817\993050\96723\153423i\13589\&4\1008403YHZ\48771VZ\DLE^0\STXC\1057595\1037144" - ), - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_20 :: BindingNewTeam -testObject_BindingNewTeam_team_20 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "\SOHW+\a#\151172iN6\GS/#mrj4'\rTV]\ETXg>\"br\SOH\NUL\158808+\47718c^\1003405<`\1111751\149060\STX\986585\ETX\162139D\ENQ\30356nqp\1095539\988368c\RSt\1081319G", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = Nothing, - _newTeamMembers = Nothing - } - ) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/CompletePasswordReset_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/CompletePasswordReset_user.hs index 5881ee95222..2f6d25d9db2 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/CompletePasswordReset_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/CompletePasswordReset_user.hs @@ -26,7 +26,7 @@ import Wire.API.User.Password testObject_CompletePasswordReset_user_1 :: CompletePasswordReset testObject_CompletePasswordReset_user_1 = CompletePasswordReset - { cpwrIdent = PasswordResetEmailIdentity (Email {emailLocal = "\STXQ=\33841k", emailDomain = ""}), + { cpwrIdent = PasswordResetEmailIdentity (unsafeEmailAddress "some" "example"), cpwrCode = PasswordResetCode { fromPasswordResetCode = @@ -179,7 +179,7 @@ testObject_CompletePasswordReset_user_8 = testObject_CompletePasswordReset_user_9 :: CompletePasswordReset testObject_CompletePasswordReset_user_9 = CompletePasswordReset - { cpwrIdent = PasswordResetEmailIdentity (Email {emailLocal = "A", emailDomain = "9L\b\1021106\37856"}), + { cpwrIdent = PasswordResetEmailIdentity (unsafeEmailAddress "some" "example"), cpwrCode = PasswordResetCode { fromPasswordResetCode = @@ -237,7 +237,7 @@ testObject_CompletePasswordReset_user_12 = CompletePasswordReset { cpwrIdent = PasswordResetEmailIdentity - (Email {emailLocal = "(\142728\EM\DEL=]=\a", emailDomain = "\175673\SYN\b\n\64411\v&\1083262"}), + (unsafeEmailAddress "some" "example"), cpwrCode = PasswordResetCode { fromPasswordResetCode = @@ -277,7 +277,7 @@ testObject_CompletePasswordReset_user_14 = CompletePasswordReset { cpwrIdent = PasswordResetEmailIdentity - (Email {emailLocal = "\1046936Q?\1079889\1101745", emailDomain = "\178846\1002100\18704"}), + (unsafeEmailAddress "some" "example"), cpwrCode = PasswordResetCode { fromPasswordResetCode = @@ -297,7 +297,7 @@ testObject_CompletePasswordReset_user_15 = CompletePasswordReset { cpwrIdent = PasswordResetEmailIdentity - (Email {emailLocal = "6\vF\EOT]\ESC\1087604.'", emailDomain = "JEe\1090620\1085217\&2dK\996913"}), + (unsafeEmailAddress "some" "example"), cpwrCode = PasswordResetCode { fromPasswordResetCode = @@ -335,7 +335,7 @@ testObject_CompletePasswordReset_user_17 :: CompletePasswordReset testObject_CompletePasswordReset_user_17 = CompletePasswordReset { cpwrIdent = - PasswordResetEmailIdentity (Email {emailLocal = "\53825[\20709", emailDomain = "\38742wC\SUBE\17763\179609"}), + PasswordResetEmailIdentity (unsafeEmailAddress "some" "example"), cpwrCode = PasswordResetCode { fromPasswordResetCode = diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/EmailUpdate_provider.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/EmailUpdate_provider.hs index 8673f2ba821..59fd227553a 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/EmailUpdate_provider.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/EmailUpdate_provider.hs @@ -18,148 +18,7 @@ module Test.Wire.API.Golden.Generated.EmailUpdate_provider where import Wire.API.Provider (EmailUpdate (..)) -import Wire.API.User.Identity (Email (Email, emailDomain, emailLocal)) +import Wire.API.User.Identity testObject_EmailUpdate_provider_1 :: EmailUpdate -testObject_EmailUpdate_provider_1 = EmailUpdate {email = Email {emailLocal = "sL\98765", emailDomain = "%"}} - -testObject_EmailUpdate_provider_2 :: EmailUpdate -testObject_EmailUpdate_provider_2 = - EmailUpdate - { email = - Email - { emailLocal = "7\160957>t\21165\ACK\69619n9\b\USskT.\"\1106936\r\DC4`", - emailDomain = "^/>1Rp<\EM\1110261\1087553\STX#\a[E\ETX#\30865\162265\3392eJ " - } - } - -testObject_EmailUpdate_provider_3 :: EmailUpdate -testObject_EmailUpdate_provider_3 = - EmailUpdate - { email = - Email - { emailLocal = "1[Z\68778\r\35821\&3\1087344|u\996796\167850\GS \1071086" - } - } - -testObject_EmailUpdate_provider_20 :: EmailUpdate -testObject_EmailUpdate_provider_20 = - EmailUpdate - { email = - Email - { emailLocal = "o\SOH\1002138\aLL$\SO\65490\1099895l*p\984607\SUB", - emailDomain = "q\30683\DC3\12589\1001477\1015970q\1002402\145416\1056480&^\176848Z" - } - } +testObject_EmailUpdate_provider_1 = EmailUpdate {email = unsafeEmailAddress "some" "example"} diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/EmailUpdate_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/EmailUpdate_user.hs index c6507d1ebd3..71e78154c88 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/EmailUpdate_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/EmailUpdate_user.hs @@ -17,181 +17,133 @@ module Test.Wire.API.Golden.Generated.EmailUpdate_user where -import Wire.API.User (Email (Email, emailDomain, emailLocal), EmailUpdate (..)) +import Wire.API.User testObject_EmailUpdate_user_1 :: EmailUpdate testObject_EmailUpdate_user_1 = EmailUpdate { euEmail = - Email - { emailLocal = "<&\DELaW1q|0.n\EM", - emailDomain = "p\1107865\1021976l_R\141868l=;\1049523\&7u\"\DLE1}wm{\CAN}" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_2 :: EmailUpdate -testObject_EmailUpdate_user_2 = EmailUpdate {euEmail = Email {emailLocal = "C\78599|g\1035896(4", emailDomain = ""}} +testObject_EmailUpdate_user_2 = EmailUpdate {euEmail = unsafeEmailAddress "some" "example"} testObject_EmailUpdate_user_3 :: EmailUpdate testObject_EmailUpdate_user_3 = EmailUpdate { euEmail = - Email - { emailLocal = "uA76\1057701c\136605\DC3\148218\SOHU0]Ds$L", - emailDomain = "/\16026 u\1112080\DC3Pq\GSev\25066\1029859\16008" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_4 :: EmailUpdate testObject_EmailUpdate_user_4 = EmailUpdate { euEmail = - Email - { emailLocal = ":|\172071WYA\a`OS\DC3\NAK\1060128\1109387u\v-\DC3F2B\1009753'z\ENQ}4[", - emailDomain = "6\147383C\153603\1016221V\1091182\&8\"\SOHM\168763\58271l" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_5 :: EmailUpdate testObject_EmailUpdate_user_5 = EmailUpdate { euEmail = - Email - { emailLocal = "0a\10920\DC2n\FS!a;*l\55139Z\b\EM\NUL\NUL\1060546\RSj\\\95672_;\STX", - emailDomain = "I\65075j\1014141byd\155419K\129140\74591\1098637mwP" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_6 :: EmailUpdate testObject_EmailUpdate_user_6 = EmailUpdate { euEmail = - Email - { emailLocal = "\DELcom0$p\50570/\FS\1044616\1015174\SIN\1072010,", - emailDomain = - "\GS\1004969\155070,\41398/qeT&\152655\a\45871}\ETB\45684\1113465\1002232#\183342\&0\20887\&9),4F" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_7 :: EmailUpdate testObject_EmailUpdate_user_7 = EmailUpdate { euEmail = - Email {emailLocal = "-\53892\62061", emailDomain = "\a$\12768Be\1072209\fS7.\12322\NUL\2873\r+k>Z:E\ETXhX$?"} + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_8 :: EmailUpdate testObject_EmailUpdate_user_8 = EmailUpdate { euEmail = - Email - { emailLocal = "\EMf\\\SOdD9#XfnL!\995008\ACK\FSZ\53254U", - emailDomain = - ")\34765\1018468x9~t)Dd;P\ESC\1024361.M(p\1050395pCz\1103678\1001284\SI\ENQ\ACK{\1016539\1101104" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_9 :: EmailUpdate -testObject_EmailUpdate_user_9 = EmailUpdate {euEmail = Email {emailLocal = "\FS\SI,n}\13385", emailDomain = "x8\GS"}} +testObject_EmailUpdate_user_9 = EmailUpdate {euEmail = unsafeEmailAddress "some" "example"} testObject_EmailUpdate_user_10 :: EmailUpdate testObject_EmailUpdate_user_10 = EmailUpdate { euEmail = - Email - { emailLocal = "r)\158517\SI\DEL\ETB\STX\1072857\DC4*$", - emailDomain = "\27680\1111520h\1022893\27692\1014774|=Bb\177401X" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_11 :: EmailUpdate testObject_EmailUpdate_user_11 = - EmailUpdate {euEmail = Email {emailLocal = ".", emailDomain = "i4N\1006864f'\GSh\132316\189403\29546x54\48183h"}} + EmailUpdate {euEmail = unsafeEmailAddress "some" "example"} testObject_EmailUpdate_user_12 :: EmailUpdate testObject_EmailUpdate_user_12 = EmailUpdate { euEmail = - Email - { emailLocal = "\1066242\ENQo`\ENQebt*\119006!", - emailDomain = "\152953\169628Fk\DC3\DC1Dq\SYN|0 c\fY\1088003\988616" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_13 :: EmailUpdate testObject_EmailUpdate_user_13 = EmailUpdate { euEmail = - Email - { emailLocal = "5\1109085SV'\7023\169487fR\SOHa:L\184444\SOH`\CANY", - emailDomain = "Yt3l5\145133\1054884j\1087288\1103021&" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_14 :: EmailUpdate testObject_EmailUpdate_user_14 = EmailUpdate { euEmail = - Email {emailLocal = "\140912\993263r.", emailDomain = "\EM\54387\176848q\CANT:`]a$J\DC3'\179878\1010553"} + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_15 :: EmailUpdate testObject_EmailUpdate_user_15 = EmailUpdate { euEmail = - Email - { emailLocal = "8ao\5201Q", - emailDomain = "8T\110875\FS\1001671\1104097\NUL\ETX\5639\ENQ\1078168HZ\185913[rr27\1037003\5689" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_16 :: EmailUpdate testObject_EmailUpdate_user_16 = EmailUpdate { euEmail = - Email - { emailLocal = "\SI\20925\"\DC2\rn\GS\1082759J(?]\US\1002518$7\136749\&7J\1019807p\EMi", - emailDomain = "Q`6_\SYN\DC3\1055256\&5hv\23871\n\SI\171070)\64498\b\ENQ\nA\1450T\94210" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_17 :: EmailUpdate testObject_EmailUpdate_user_17 = EmailUpdate { euEmail = - Email - { emailLocal = "64A\999241\GS\DC1\DLE\7404\GSj", - emailDomain = - "\136875=\156122\f\ENQr\DC2Ga\25747\ETX/\55110G\NULk=\NAKq\1073443R}Ts\ETX\f\1027779\1088335\&2" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_18 :: EmailUpdate testObject_EmailUpdate_user_18 = EmailUpdate { euEmail = - Email - { emailLocal = "9L\ESC\DC3\32248/F\154604O\1061945>\bx;2\148788\&0\US", - emailDomain = " {\"_\v\1092033\1041960\1066771\1088769\EMJ%\1005251yy\SUB\1040487t>xoC" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_19 :: EmailUpdate testObject_EmailUpdate_user_19 = EmailUpdate { euEmail = - Email - { emailLocal = "\178973\1040124E\FS\SUB\126215\NAKC%\988145\ACK\US\DC1a8\r\64887E\990883%\178650\185749;|\GS", - emailDomain = "1+,\135308\&83" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_20 :: EmailUpdate testObject_EmailUpdate_user_20 = EmailUpdate { euEmail = - Email - { emailLocal = "e\NAKV\bD\SOH88Kh\FS\169565D4\1089993\36544zg\RS", - emailDomain = ":\174380D\ENQy+\DC4k>]\60696\ETB\FSr\1010033aWSw\a\6023\&6\RS\99409f" - } + unsafeEmailAddress "some" "example" } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Email_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Email_user.hs index a6205acf031..d73a4d3a218 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Email_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Email_user.hs @@ -17,120 +17,8 @@ module Test.Wire.API.Golden.Generated.Email_user where -import Wire.API.User (Email (..)) +import Wire.API.User -testObject_Email_user_1 :: Email +testObject_Email_user_1 :: EmailAddress testObject_Email_user_1 = - Email - { emailLocal = "\151983fa\49426\SOH2\v.\FS<\ESC\ETB#\t-\1105186`2", - emailDomain = "\EOT\"\27565\DLEn\GS]\ahDzP\CANp\15102T\133424\DC4d" - } - -testObject_Email_user_2 :: Email -testObject_Email_user_2 = Email {emailLocal = "\985610\DC3F\186317\1084807n:", emailDomain = "8\1072891\3215\SYNs"} - -testObject_Email_user_3 :: Email -testObject_Email_user_3 = - Email - { emailLocal = "\1107928o #\168176G\8169%/]\FSk\1056490t\1103662)\DC3\NAK*\ESCE}>\SUB\SOHEn==8", - emailDomain = "\26168\CANN\a;_l$*\16881P\STX\bK" - } - -testObject_Email_user_4 :: Email -testObject_Email_user_4 = - Email - { emailLocal = "!\141500\SI&\ao88i\989736(\SUBl1_\1078316-\f\t&\FS9\1090163", - emailDomain = "ef&\1030389\180990W(\999136cS\SYN\EOT\97583GLi" - } - -testObject_Email_user_5 :: Email -testObject_Email_user_5 = - Email {emailLocal = "", emailDomain = "\27960\DC1\1089303\1027305jh\1015732o\GSH0bW7^\17877\DELB"} - -testObject_Email_user_6 :: Email -testObject_Email_user_6 = - Email {emailLocal = "eM'5\br>\92509\996616\&4\133072\61444r\t", emailDomain = "\996839[n\ENQ )&D=\1020297\ACK\b"} - -testObject_Email_user_7 :: Email -testObject_Email_user_7 = - Email - { emailLocal = "\1108398\169243a\ETX'\94588L\SYN\37261\991394Q\1001290\998959Fc\1094805T\191410\SOTD", - emailDomain = "\176912?1\1100840DT" - } - -testObject_Email_user_8 :: Email -testObject_Email_user_8 = - Email - { emailLocal = " \ESC{\1106829EZ_\t+E\vE", - emailDomain = "h\92250%\54205g\14627Lu\DC2\178534J} Aq\"#f\ESC \EOTO\DC2" - } - -testObject_Email_user_9 :: Email -testObject_Email_user_9 = - Email {emailLocal = "\DEL\1009982\1032817\&5M6d*~-\DC3\ETB?\32582", emailDomain = "g\111007R\1093154|\986636\1030500"} - -testObject_Email_user_10 :: Email -testObject_Email_user_10 = - Email - { emailLocal = "\"K\1062412\1070216$s\988180\1078655V389V\a\ETB\FSH\1055625)\28401Dg\ETB", - emailDomain = "\1113745\1057450k\fi\n\1046406\139820{\GSl\14339YPbV\DEL-ZZ\1060246LK\36307\1053861Y" - } - -testObject_Email_user_11 :: Email -testObject_Email_user_11 = - Email {emailLocal = "", emailDomain = "+zGJ\b_t/N\NUL3S\1061013M\146321\1076256z\1099407\1106566SJD"} - -testObject_Email_user_12 :: Email -testObject_Email_user_12 = - Email - { emailLocal = "\EOT1[G\1014638\983349\&9\1086491:uJ\144560\FSMF\123165\985853\187923\US6|\996879\NAK\1075664", - emailDomain = "\96735_\1064048" - } - -testObject_Email_user_13 :: Email -testObject_Email_user_13 = - Email - { emailLocal = "\ENQ\rt\FSA#}\RSn\176776OA\SYN\SO\1040173\t2q\DC3n\161371\185193\f+", - emailDomain = "]\29293\159214na[\US'h\134423\DC2\1007180\147811\1110187" - } - -testObject_Email_user_14 :: Email -testObject_Email_user_14 = - Email - { emailLocal = "X\DLEE\DC4\1013278\1045648\1107074YMU[\n}\991766\r7\1010192\CAN\\", - emailDomain = "\165099\1036143M\GS!\142750T%F]" - } - -testObject_Email_user_15 :: Email -testObject_Email_user_15 = Email {emailLocal = "{C\174982\1042320eU\DC4w", emailDomain = "`\DC3>"} - -testObject_Email_user_16 :: Email -testObject_Email_user_16 = Email {emailLocal = "IO\EM2>\1053560+~", emailDomain = "\SO"} - -testObject_Email_user_17 :: Email -testObject_Email_user_17 = - Email - { emailLocal = "\RS\1097381\SYN\SOH>\51458V7C-asF\1055340IfrYTM;\1014918\1059325*l(d", - emailDomain = ".53Q\1097431\&26bfw\175553\73861~\165507\131884m\GS\NAK\SO}\152927~\1051259R" - } - -testObject_Email_user_18 :: Email -testObject_Email_user_18 = - Email - { emailLocal = "\181378|\NUL\STX3\DC2\1099608,:\ETBJuF\DLE*\15790\DC1\"\SYNkU!\989789\&8T\EM2", - emailDomain = "T\"q\DC1\71908}\DC3Z~\128415)" - } - -testObject_Email_user_19 :: Email -testObject_Email_user_19 = - Email - { emailLocal = "\t\ACKN~\RSWy5'\CANq:_K\1022684\"+WM\29811S.\DC2D\DEL`\CAN", - emailDomain = "DG\136157A\59646E=W\1075924" - } - -testObject_Email_user_20 :: Email -testObject_Email_user_20 = - Email - { emailLocal = "/\138192ZF\1003769\1027227", - emailDomain = "\187749I\1109889~FN\1016516\ENQ3PH\DC4k\1036543\131674{\1046142\ETBH\1020386\DC2IR" - } + unsafeEmailAddress "some" "example" diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_team.hs index c80d19bea0e..7a9dbb790c1 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_team.hs @@ -230,7 +230,7 @@ testObject_Event_team_18 = (Id (fromJust (UUID.fromString "00007783-0000-7d60-0000-00d30000396e"))) ( Just ( Permissions - { _self = + { self = fromList [ CreateConversation, DeleteConversation, @@ -246,7 +246,7 @@ testObject_Event_team_18 = GetTeamConversations, DeleteTeam ], - _copy = + copy = fromList [ CreateConversation, DeleteConversation, @@ -273,7 +273,7 @@ testObject_Event_team_19 = (Id (fromJust (UUID.fromString "0000382c-0000-1ce7-0000-568b00001fe9"))) ( Just ( Permissions - { _self = + { self = fromList [ DeleteConversation, RemoveTeamMember, @@ -284,7 +284,7 @@ testObject_Event_team_19 = GetMemberPermissions, GetTeamConversations ], - _copy = + copy = fromList [ DeleteConversation, RemoveTeamMember, diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/FeatureStatus_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/FeatureStatus_team.hs deleted file mode 100644 index 6c1d40ec4ed..00000000000 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/FeatureStatus_team.hs +++ /dev/null @@ -1,80 +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 Test.Wire.API.Golden.Generated.FeatureStatus_team where - -import Wire.API.Team.Feature (FeatureStatus (..)) - -testObject_FeatureStatus_team_1 :: FeatureStatus -testObject_FeatureStatus_team_1 = FeatureStatusEnabled - -testObject_FeatureStatus_team_2 :: FeatureStatus -testObject_FeatureStatus_team_2 = FeatureStatusDisabled - -testObject_FeatureStatus_team_3 :: FeatureStatus -testObject_FeatureStatus_team_3 = FeatureStatusEnabled - -testObject_FeatureStatus_team_4 :: FeatureStatus -testObject_FeatureStatus_team_4 = FeatureStatusDisabled - -testObject_FeatureStatus_team_5 :: FeatureStatus -testObject_FeatureStatus_team_5 = FeatureStatusEnabled - -testObject_FeatureStatus_team_6 :: FeatureStatus -testObject_FeatureStatus_team_6 = FeatureStatusDisabled - -testObject_FeatureStatus_team_7 :: FeatureStatus -testObject_FeatureStatus_team_7 = FeatureStatusEnabled - -testObject_FeatureStatus_team_8 :: FeatureStatus -testObject_FeatureStatus_team_8 = FeatureStatusEnabled - -testObject_FeatureStatus_team_9 :: FeatureStatus -testObject_FeatureStatus_team_9 = FeatureStatusDisabled - -testObject_FeatureStatus_team_10 :: FeatureStatus -testObject_FeatureStatus_team_10 = FeatureStatusDisabled - -testObject_FeatureStatus_team_11 :: FeatureStatus -testObject_FeatureStatus_team_11 = FeatureStatusEnabled - -testObject_FeatureStatus_team_12 :: FeatureStatus -testObject_FeatureStatus_team_12 = FeatureStatusEnabled - -testObject_FeatureStatus_team_13 :: FeatureStatus -testObject_FeatureStatus_team_13 = FeatureStatusEnabled - -testObject_FeatureStatus_team_14 :: FeatureStatus -testObject_FeatureStatus_team_14 = FeatureStatusDisabled - -testObject_FeatureStatus_team_15 :: FeatureStatus -testObject_FeatureStatus_team_15 = FeatureStatusDisabled - -testObject_FeatureStatus_team_16 :: FeatureStatus -testObject_FeatureStatus_team_16 = FeatureStatusDisabled - -testObject_FeatureStatus_team_17 :: FeatureStatus -testObject_FeatureStatus_team_17 = FeatureStatusDisabled - -testObject_FeatureStatus_team_18 :: FeatureStatus -testObject_FeatureStatus_team_18 = FeatureStatusDisabled - -testObject_FeatureStatus_team_19 :: FeatureStatus -testObject_FeatureStatus_team_19 = FeatureStatusEnabled - -testObject_FeatureStatus_team_20 :: FeatureStatus -testObject_FeatureStatus_team_20 = FeatureStatusDisabled 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 new file mode 100644 index 00000000000..540fa355c3f --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Feature_team.hs @@ -0,0 +1,75 @@ +{-# LANGUAGE OverloadedLists #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Wire.API.Golden.Generated.Feature_team where + +import Data.Domain +import Imports +import Wire.API.Team.Feature + +testObject_Feature_team_1 :: Feature AppLockConfig +testObject_Feature_team_1 = Feature FeatureStatusEnabled (AppLockConfig (EnforceAppLock False) (-98)) + +testObject_Feature_team_2 :: Feature AppLockConfig +testObject_Feature_team_2 = Feature FeatureStatusEnabled (AppLockConfig (EnforceAppLock True) 0) + +testObject_Feature_team_3 :: Feature AppLockConfig +testObject_Feature_team_3 = Feature FeatureStatusEnabled (AppLockConfig (EnforceAppLock True) 111) + +testObject_Feature_team_4 :: Feature SelfDeletingMessagesConfig +testObject_Feature_team_4 = Feature FeatureStatusEnabled (SelfDeletingMessagesConfig (-97)) + +testObject_Feature_team_5 :: Feature SelfDeletingMessagesConfig +testObject_Feature_team_5 = Feature FeatureStatusEnabled (SelfDeletingMessagesConfig 0) + +testObject_Feature_team_6 :: Feature SelfDeletingMessagesConfig +testObject_Feature_team_6 = Feature FeatureStatusEnabled (SelfDeletingMessagesConfig 77) + +testObject_Feature_team_7 :: Feature ClassifiedDomainsConfig +testObject_Feature_team_7 = Feature FeatureStatusEnabled (ClassifiedDomainsConfig []) + +testObject_Feature_team_8 :: Feature ClassifiedDomainsConfig +testObject_Feature_team_8 = Feature FeatureStatusEnabled (ClassifiedDomainsConfig [Domain "example.com", Domain "test.foobar"]) + +testObject_Feature_team_9 :: Feature ClassifiedDomainsConfig +testObject_Feature_team_9 = Feature FeatureStatusEnabled (ClassifiedDomainsConfig [Domain "test.foobar"]) + +testObject_Feature_team_10 :: Feature SSOConfig +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_13 :: Feature DigitalSignaturesConfig +testObject_Feature_team_13 = Feature FeatureStatusEnabled DigitalSignaturesConfig + +testObject_Feature_team_14 :: Feature ConferenceCallingConfig +testObject_Feature_team_14 = Feature FeatureStatusDisabled (ConferenceCallingConfig One2OneCallsSft) + +testObject_Feature_team_15 :: Feature GuestLinksConfig +testObject_Feature_team_15 = Feature FeatureStatusEnabled GuestLinksConfig + +testObject_Feature_team_16 :: Feature SndFactorPasswordChallengeConfig +testObject_Feature_team_16 = Feature FeatureStatusDisabled SndFactorPasswordChallengeConfig + +testObject_Feature_team_17 :: Feature SearchVisibilityInboundConfig +testObject_Feature_team_17 = Feature FeatureStatusEnabled SearchVisibilityInboundConfig diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationList_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationList_team.hs index d95f54bd302..1463b0d1136 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationList_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationList_team.hs @@ -19,29 +19,14 @@ module Test.Wire.API.Golden.Generated.InvitationList_team where -import Data.Either.Combinators import Data.Id (Id (Id)) import Data.Json.Util (readUTCTimeMillis) import Data.UUID qualified as UUID (fromString) -import Imports (Bool (False, True), Maybe (Just, Nothing), fromJust) +import Imports (Bool (False, True), Maybe (Just, Nothing), fromJust, fromRight') import URI.ByteString (parseURI, strictURIParserOptions) import Wire.API.Team.Invitation - ( Invitation - ( Invitation, - inCreatedAt, - inCreatedBy, - inInvitation, - inInviteeEmail, - inInviteeName, - inInviteePhone, - inInviteeUrl, - inRole, - inTeam - ), - InvitationList (..), - ) import Wire.API.Team.Role (Role (RoleAdmin, RoleExternalPartner, RoleMember, RoleOwner)) -import Wire.API.User.Identity (Email (Email, emailDomain, emailLocal), Phone (Phone, fromPhone)) +import Wire.API.User.Identity import Wire.API.User.Profile (Name (Name, fromName)) testObject_InvitationList_team_1 :: InvitationList @@ -52,21 +37,20 @@ testObject_InvitationList_team_2 = InvitationList { ilInvitations = [ Invitation - { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001")), - inRole = RoleOwner, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-08T09:28:36.729Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), - inInviteeEmail = Email {emailLocal = "\153442", emailDomain = "w"}, - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001")), + role = RoleOwner, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000")), + createdAt = fromJust (readUTCTimeMillis "1864-05-08T09:28:36.729Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "fuC9p\1098501A\163554\f\ENQ\SO\21027N\47326_?oCX.U\r\163744W\33096\58996\1038685\DC3\t[\37667\SYN/\8408A\145025\173325\DC4H\135001\STX\166880\EOT\165028o\DC3" } ), - inInviteePhone = Just (Phone {fromPhone = "+851333011"}), - inInviteeUrl = Just (fromRight' (parseURI strictURIParserOptions "https://example.com/inv14")) + inviteeUrl = Just (fromRight' (parseURI strictURIParserOptions "https://example.com/inv14")) } ], ilHasMore = True @@ -80,134 +64,126 @@ testObject_InvitationList_team_4 = InvitationList { ilInvitations = [ Invitation - { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), - inRole = RoleAdmin, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T19:46:50.121Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), + role = RoleAdmin, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000")), + createdAt = fromJust (readUTCTimeMillis "1864-05-09T19:46:50.121Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "R6\133444\134053VQ\187682\SUB\SOH\180538\&0C\1088909\ESCR\185800\125002@\38857Z?\STX\169387\1067878e}\SOH\ETB\EOTm\184898\US]\986782\189015\1059374\986508\b\DC1zfw-5\120662\CAN\1064450 \EMe\DC4|\14426Vo{\1076439\DC3#\USS\45051&zz\160719\&9\142411,\SI\f\SOHp\1025840\DLE\163178\1060369.&\997544kZ\50431u\b\50764\1109279n:\1103691D$.Q" } ), - inInviteePhone = Just (Phone {fromPhone = "+60506387292"}), - inInviteeUrl = Nothing + inviteeUrl = Nothing }, Invitation - { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), - inRole = RoleAdmin, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T09:00:02.901Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), + role = RoleAdmin, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000")), + createdAt = fromJust (readUTCTimeMillis "1864-05-09T09:00:02.901Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "\DC2}q\CAN=SA\ETXx\t\ETX\\\v[\b)(\ESC]\135875Y\v@p\41515l\45065\157388\NUL\t\1100066\SOH1\DC1\ENQ\1021763\"i\29460\EM\b\ACK\SI\DC2v\ACK" } ), - inInviteePhone = Just (Phone {fromPhone = "+913945015"}), - inInviteeUrl = Nothing + inviteeUrl = Nothing }, Invitation - { inTeam = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), - inRole = RoleMember, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T11:10:31.203Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), + role = RoleMember, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), + createdAt = fromJust (readUTCTimeMillis "1864-05-09T11:10:31.203Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "\58076&\1059325Ec\NUL\16147}k\1036184l\172911\USJ\EM0^.+F\DEL\NUL\f$'`!\ETB[p\1041609}>E0y\96440#4I\a\66593jc\ESCgt\22473\1093208P\DC4!\1095909E93'Y$YL\46886b\r:,\181790\SO\153247y\ETX;\1064633\1099478z4z-D\1096755a\139100\&6\164829r\1033640\987906J\DLE\48134" } ), - inInviteePhone = Just (Phone {fromPhone = "+17046334"}), - inInviteeUrl = Nothing + inviteeUrl = Nothing }, Invitation - { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), - inRole = RoleOwner, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T23:41:34.529Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), + role = RoleOwner, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), + createdAt = fromJust (readUTCTimeMillis "1864-05-09T23:41:34.529Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "Ft*O1\b&\SO\CAN<\72219\1092619m\n\DC4\DC2; \ETX\988837\DC1\1059627\"k.T\1023249[[\FS\EOT{j`\GS\997342c\1066411{\SUB\GSQY\182805\t\NAKy\t\132339j\1036225W " } ), - inInviteePhone = Nothing, - inInviteeUrl = Nothing - }, - Invitation - { inTeam = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000")), - inRole = RoleAdmin, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T00:29:17.658Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, - inInviteeName = Nothing, - inInviteePhone = Just (Phone {fromPhone = "+918848647685283"}), - inInviteeUrl = Nothing - }, - Invitation - { inTeam = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001")), - inRole = RoleOwner, - inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T13:34:37.117Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, - inInviteeName = + inviteeUrl = Nothing + }, + Invitation + { team = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000")), + role = RoleAdmin, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), + createdAt = fromJust (readUTCTimeMillis "1864-05-09T00:29:17.658Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Nothing, + inviteeUrl = Nothing + }, + Invitation + { team = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001")), + role = RoleOwner, + invitationId = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001")), + createdAt = fromJust (readUTCTimeMillis "1864-05-09T13:34:37.117Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "Lo\r\1107113\1111565\1042998\1027480g\"\1055088\SUB\SUB\180703\43419\EOTv\188258,\171408(\GSQT\150160;\1063450\ENQ\ETBB\1106414H\170195\\\1040638,Y" } ), - inInviteePhone = Just (Phone {fromPhone = "+45207005641274"}), - inInviteeUrl = Nothing + inviteeUrl = Nothing } testObject_Invitation_team_6 :: Invitation testObject_Invitation_team_6 = Invitation - { inTeam = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), - inRole = RoleAdmin, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000100000000")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T08:56:40.919Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000000"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = "OC"}, - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), + role = RoleAdmin, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000100000000")), + createdAt = fromJust (readUTCTimeMillis "1864-05-09T08:56:40.919Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000000"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "O~\DC4U\RS?V3_\191280Slh\1072236Q1\1011443j|~M7\1092762\1097596\94632\DC1K\1078140Afs\178951lGV\1113159]`o\EMf\34020InvfDDy\\DI\163761\1091945\ETBB\159212F*X\SOH\SUB\50580\ETX\DLE<\ETX\SYNc\DEL\DLE,p\v*\1005720Vn\fI\70201xS\STXV\ESC$\EMu\1002390xl>\aZ\DC44e\DC4aZ" } ), - inInviteePhone = Just (Phone {fromPhone = "+75547625285"}), - inInviteeUrl = Nothing + inviteeUrl = Nothing } testObject_Invitation_team_7 :: Invitation testObject_Invitation_team_7 = Invitation - { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000200000001")), - inRole = RoleExternalPartner, - inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000002")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-07T18:46:22.786Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000100000000"))), - inInviteeEmail = Email {emailLocal = "oj", emailDomain = ""}, - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000200000001")), + role = RoleExternalPartner, + invitationId = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000002")), + createdAt = fromJust (readUTCTimeMillis "1864-05-07T18:46:22.786Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000100000000"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "\CAN.\110967\1085214\DLE\f\DLE\CAN\150564o;Yay:yY $\ETX<\879%@\USre>5L'R\DC3\178035oy#]c4!\99741U\54858\26279\1042232\1062242p_>f\SO\DEL\175240\1077738\995735_Vm\US}\STXPz\r\ENQK\SO+>\991648\NUL\153467?pu?r\ESC\SUB!?\168405;\6533S\18757\a\1071148\b\1023581\996567\17385\120022\b\SUB\FS\SIF%<\125113\SIh\ESC\ETX\SI\994739\USO\NULg_\151272\47274\1026399\EOT\1058084\1089771z~%IA'R\b\1011572Hv^\1043633wrjb\t\166747\ETX" } ), - inInviteePhone = Just (Phone {fromPhone = "+518729615781"}), - inInviteeUrl = Nothing + inviteeUrl = Nothing } testObject_Invitation_team_12 :: Invitation testObject_Invitation_team_12 = Invitation - { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000002")), - inRole = RoleAdmin, - inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000002")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-12T22:47:35.829Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000002-0000-0002-0000-000000000000"))), - inInviteeEmail = Email {emailLocal = "\1016862\141073\RS", emailDomain = ""}, - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000002")), + role = RoleAdmin, + invitationId = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000002")), + createdAt = fromJust (readUTCTimeMillis "1864-05-12T22:47:35.829Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000002-0000-0002-0000-000000000000"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "\DLEZ+wd^\67082\1073384\&1\STXYdXt>\1081020LSB7F9\\\135148\ENQ\n\987295\"\127009|\a\61724\157754\DEL'\ESCTygU\1106772R\52822\1071584O4\1035713E9\"\1016016\DC2Re\ENQD}\1051112\161959\1104733\bV\176894%98'\RS9\ACK4yP\83405\14400\345\aw\t\1098022\v\1078003xv/Yl\1005740\158703" } ), - inInviteePhone = Just (Phone {fromPhone = "+68945103783764"}), - inInviteeUrl = Nothing + inviteeUrl = Nothing } testObject_Invitation_team_13 :: Invitation testObject_Invitation_team_13 = Invitation - { inTeam = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000000000001")), - inRole = RoleMember, - inInvitation = Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000200000002")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-08T01:18:31.982Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000100000002"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = "\DELr"}, - inInviteeName = Just (Name {fromName = "U"}), - inInviteePhone = Just (Phone {fromPhone = "+549940856897515"}), - inInviteeUrl = Nothing + { team = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000000000001")), + role = RoleMember, + invitationId = Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000200000002")), + createdAt = fromJust (readUTCTimeMillis "1864-05-08T01:18:31.982Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000100000002"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just (Name {fromName = "U"}), + inviteeUrl = Nothing } testObject_Invitation_team_14 :: Invitation testObject_Invitation_team_14 = Invitation - { inTeam = Id (fromJust (UUID.fromString "00000002-0000-0002-0000-000100000000")), - inRole = RoleOwner, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000200000002")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-12T23:54:25.090Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000002-0000-0002-0000-000200000000"))), - inInviteeEmail = Email {emailLocal = "EI", emailDomain = "{"}, - inInviteeName = Nothing, - inInviteePhone = Just (Phone {fromPhone = "+89058877371"}), - inInviteeUrl = Nothing + { team = Id (fromJust (UUID.fromString "00000002-0000-0002-0000-000100000000")), + role = RoleOwner, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000200000002")), + createdAt = fromJust (readUTCTimeMillis "1864-05-12T23:54:25.090Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000002-0000-0002-0000-000200000000"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Nothing, + inviteeUrl = Nothing } testObject_Invitation_team_15 :: Invitation testObject_Invitation_team_15 = Invitation - { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000100000001")), - inRole = RoleOwner, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-08T22:22:28.568Z"), - inCreatedBy = Nothing, - inInviteeEmail = Email {emailLocal = ".", emailDomain = "\DEL"}, - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000100000001")), + role = RoleOwner, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001")), + createdAt = fromJust (readUTCTimeMillis "1864-05-08T22:22:28.568Z"), + createdBy = Nothing, + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "\71448\US&KIL\DC3\1086159![\n6\1111661HEj4E\12136UL\US>2\1070931_\nJ\53410Pv\SO\SIR\30897\&8\bmS\45510mE\ag\SYN\ENQ%\14545\f!\v\US\119306\ENQ\184817\1044744\SO83!j\73854\GS\1071331,\RS\CANF\1062795\1110535U\EMJb\DC1j\EMY\92304O\1007855" } ), - inInviteePhone = Just (Phone {fromPhone = "+57741900390998"}), - inInviteeUrl = Nothing + inviteeUrl = Nothing } testObject_Invitation_team_16 :: Invitation testObject_Invitation_team_16 = Invitation - { inTeam = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000002")), - inRole = RoleExternalPartner, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000200000001")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T09:56:33.113Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))), - inInviteeEmail = Email {emailLocal = "\\", emailDomain = "\"\DEL{"}, - inInviteeName = Just (Name {fromName = "\GS\DC4Q;6/_f*7\1093966\SI+\1092810\41698\&9"}), - inInviteePhone = Nothing, - inInviteeUrl = Nothing + { team = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000002")), + role = RoleExternalPartner, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000200000001")), + createdAt = fromJust (readUTCTimeMillis "1864-05-09T09:56:33.113Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just (Name {fromName = "\GS\DC4Q;6/_f*7\1093966\SI+\1092810\41698\&9"}), + inviteeUrl = Nothing } testObject_Invitation_team_17 :: Invitation testObject_Invitation_team_17 = Invitation - { inTeam = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000000000002")), - inRole = RoleAdmin, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-08T06:30:23.239Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = "\SOH[\97119"}, - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000000000002")), + role = RoleAdmin, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), + createdAt = fromJust (readUTCTimeMillis "1864-05-08T06:30:23.239Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "Z\ESC9E\DEL\NAK\37708\83413}(3m\97177\97764'\1072786.WY;\RS8?v-\1100720\DC2\1015859" } ), - inInviteePhone = Nothing, - inInviteeUrl = Nothing + inviteeUrl = Nothing } testObject_Invitation_team_19 :: Invitation testObject_Invitation_team_19 = Invitation - { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000000")), - inRole = RoleMember, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000000000001")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-07T15:08:06.796Z"), - inCreatedBy = Nothing, - inInviteeEmail = Email {emailLocal = "\1019726\96050\DEL", emailDomain = "(S\ETB"}, - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000000")), + role = RoleMember, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000000000001")), + createdAt = fromJust (readUTCTimeMillis "1864-05-07T15:08:06.796Z"), + createdBy = Nothing, + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "\38776r\111317\ETXQi\1000087\1097943\EM\170747\74323+\1067948Q?H=G-\RS;\1103719\SOq^K;a\1052250W\EM X\83384\1073320>M\980\26387jjbU-&\1040136v\NULy\181884\a|\SYNUfJCHjP\SO\1111555\27981DNA:~s" } ), - inInviteePhone = Just (Phone {fromPhone = "+05787228893"}), - inInviteeUrl = Nothing + inviteeUrl = Nothing } testObject_Invitation_team_20 :: Invitation testObject_Invitation_team_20 = Invitation - { inTeam = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), - inRole = RoleExternalPartner, - inInvitation = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000000000001")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-12T08:07:17.747Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))), - inInviteeEmail = Email {emailLocal = "b", emailDomain = "u9T"}, - inInviteeName = Nothing, - inInviteePhone = Just (Phone {fromPhone = "+27259486019"}), - inInviteeUrl = Nothing + { team = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), + role = RoleExternalPartner, + invitationId = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000000000001")), + createdAt = fromJust (readUTCTimeMillis "1864-05-12T08:07:17.747Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Nothing, + inviteeUrl = Nothing } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LegalHoldServiceConfirm_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LegalHoldServiceConfirm_team.hs index 70d77b0edbf..4e0a5d13558 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LegalHoldServiceConfirm_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LegalHoldServiceConfirm_team.hs @@ -17,187 +17,45 @@ module Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team where +import Data.Domain (Domain (Domain)) import Data.Id +import Data.Qualified (Qualified (Qualified)) import Data.UUID qualified as UUID (fromString) import Imports (fromJust) -import Wire.API.Team.LegalHold.External (LegalHoldServiceConfirm (..)) +import Wire.API.Team.LegalHold.External (LegalHoldServiceConfirm (..), LegalHoldServiceConfirmV0 (..)) -testObject_LegalHoldServiceConfirm_team_1 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_1 = - LegalHoldServiceConfirm +testObject_LegalHoldServiceConfirmV0_team_1 :: LegalHoldServiceConfirmV0 +testObject_LegalHoldServiceConfirmV0_team_1 = + LegalHoldServiceConfirmV0 { lhcClientId = ClientId 0x1d, lhcUserId = Id (fromJust (UUID.fromString "00000003-0000-0004-0000-000100000000")), lhcTeamId = Id (fromJust (UUID.fromString "00000007-0000-0000-0000-000600000005")), lhcRefreshToken = "i>\ACKO" } -testObject_LegalHoldServiceConfirm_team_2 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_2 = - LegalHoldServiceConfirm +testObject_LegalHoldServiceConfirmV0_team_2 :: LegalHoldServiceConfirmV0 +testObject_LegalHoldServiceConfirmV0_team_2 = + LegalHoldServiceConfirmV0 { lhcClientId = ClientId 0x15, lhcUserId = Id (fromJust (UUID.fromString "00000002-0000-0008-0000-000200000007")), lhcTeamId = Id (fromJust (UUID.fromString "00000007-0000-0004-0000-000600000002")), lhcRefreshToken = "\\i" } -testObject_LegalHoldServiceConfirm_team_3 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_3 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 4, - lhcUserId = Id (fromJust (UUID.fromString "00000001-0000-0004-0000-000600000005")), - lhcTeamId = Id (fromJust (UUID.fromString "00000003-0000-0005-0000-000100000001")), - lhcRefreshToken = ")" - } - -testObject_LegalHoldServiceConfirm_team_4 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_4 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0x1b, - lhcUserId = Id (fromJust (UUID.fromString "00000008-0000-0002-0000-000300000001")), - lhcTeamId = Id (fromJust (UUID.fromString "00000004-0000-0008-0000-000300000004")), - lhcRefreshToken = "W" - } - -testObject_LegalHoldServiceConfirm_team_5 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_5 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0x12, - lhcUserId = Id (fromJust (UUID.fromString "00000000-0000-0005-0000-000300000006")), - lhcTeamId = Id (fromJust (UUID.fromString "00000002-0000-0008-0000-000400000007")), - lhcRefreshToken = "\1021908hL\1101997\23856\180103" - } - -testObject_LegalHoldServiceConfirm_team_6 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_6 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 1, - lhcUserId = Id (fromJust (UUID.fromString "00000005-0000-0002-0000-000300000003")), - lhcTeamId = Id (fromJust (UUID.fromString "00000004-0000-0008-0000-000200000006")), - lhcRefreshToken = "\1089885\983521b" - } - -testObject_LegalHoldServiceConfirm_team_7 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_7 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0x1c, - lhcUserId = Id (fromJust (UUID.fromString "00000005-0000-0001-0000-000600000001")), - lhcTeamId = Id (fromJust (UUID.fromString "00000006-0000-0004-0000-000500000003")), - lhcRefreshToken = "\1048812[\ETBu\r" - } - -testObject_LegalHoldServiceConfirm_team_8 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_8 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0x1f, - lhcUserId = Id (fromJust (UUID.fromString "00000003-0000-0008-0000-000200000001")), - lhcTeamId = Id (fromJust (UUID.fromString "00000004-0000-0004-0000-000500000004")), - lhcRefreshToken = "ZU\990363;\US\ESC" - } - -testObject_LegalHoldServiceConfirm_team_9 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_9 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 3, - lhcUserId = Id (fromJust (UUID.fromString "00000003-0000-0008-0000-000100000003")), - lhcTeamId = Id (fromJust (UUID.fromString "00000008-0000-0006-0000-000000000006")), - lhcRefreshToken = "Y\1088702" - } - -testObject_LegalHoldServiceConfirm_team_10 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_10 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0x20, - lhcUserId = Id (fromJust (UUID.fromString "00000006-0000-0005-0000-000500000006")), - lhcTeamId = Id (fromJust (UUID.fromString "00000001-0000-0005-0000-000700000001")), - lhcRefreshToken = "" - } - -testObject_LegalHoldServiceConfirm_team_11 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_11 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0, - lhcUserId = Id (fromJust (UUID.fromString "00000006-0000-0002-0000-000700000007")), - lhcTeamId = Id (fromJust (UUID.fromString "00000002-0000-0005-0000-000400000007")), - lhcRefreshToken = "\153567@-c\ENQ" - } - -testObject_LegalHoldServiceConfirm_team_12 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_12 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0, - lhcUserId = Id (fromJust (UUID.fromString "00000005-0000-0006-0000-000500000004")), - lhcTeamId = Id (fromJust (UUID.fromString "00000007-0000-0008-0000-000600000006")), - lhcRefreshToken = "" - } - -testObject_LegalHoldServiceConfirm_team_13 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_13 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0xc, - lhcUserId = Id (fromJust (UUID.fromString "00000002-0000-0005-0000-000600000005")), - lhcTeamId = Id (fromJust (UUID.fromString "00000004-0000-0000-0000-000100000007")), - lhcRefreshToken = "DXD[" - } - -testObject_LegalHoldServiceConfirm_team_14 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_14 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 2, - lhcUserId = Id (fromJust (UUID.fromString "00000007-0000-0003-0000-000200000003")), - lhcTeamId = Id (fromJust (UUID.fromString "00000004-0000-0001-0000-000400000003")), - lhcRefreshToken = "T\1068224\DC3\177787\STX" - } - -testObject_LegalHoldServiceConfirm_team_15 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_15 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0x1a, - lhcUserId = Id (fromJust (UUID.fromString "00000005-0000-0005-0000-000300000007")), - lhcTeamId = Id (fromJust (UUID.fromString "00000004-0000-0003-0000-000100000004")), - lhcRefreshToken = "\n' \FS~\137351)" - } - -testObject_LegalHoldServiceConfirm_team_16 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_16 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0xe, - lhcUserId = Id (fromJust (UUID.fromString "00000003-0000-0002-0000-000000000000")), - lhcTeamId = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000300000000")), - lhcRefreshToken = "\65915\163144\n" - } - -testObject_LegalHoldServiceConfirm_team_17 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_17 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0xe, - lhcUserId = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000600000004")), - lhcTeamId = Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000400000008")), - lhcRefreshToken = "" - } - -testObject_LegalHoldServiceConfirm_team_18 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_18 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0x11, - lhcUserId = Id (fromJust (UUID.fromString "00000006-0000-0000-0000-000800000004")), - lhcTeamId = Id (fromJust (UUID.fromString "00000006-0000-0003-0000-000100000005")), - lhcRefreshToken = "Y\1029262" - } - -testObject_LegalHoldServiceConfirm_team_19 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_19 = +testObject_LegalHoldServiceConfirm_team_1 :: LegalHoldServiceConfirm +testObject_LegalHoldServiceConfirm_team_1 = LegalHoldServiceConfirm - { lhcClientId = ClientId 0x1c, - lhcUserId = Id (fromJust (UUID.fromString "00000003-0000-0006-0000-000700000002")), - lhcTeamId = Id (fromJust (UUID.fromString "00000001-0000-0003-0000-000600000000")), - lhcRefreshToken = "[" + { clientId = ClientId 4, + userId = Qualified (Id (fromJust (UUID.fromString "00000001-0000-0004-0000-000600000005"))) (Domain "example.com"), + teamId = Id (fromJust (UUID.fromString "00000003-0000-0005-0000-000100000001")), + refreshToken = ")" } -testObject_LegalHoldServiceConfirm_team_20 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_20 = +testObject_LegalHoldServiceConfirm_team_2 :: LegalHoldServiceConfirm +testObject_LegalHoldServiceConfirm_team_2 = LegalHoldServiceConfirm - { lhcClientId = ClientId 1, - lhcUserId = Id (fromJust (UUID.fromString "00000001-0000-0004-0000-000600000005")), - lhcTeamId = Id (fromJust (UUID.fromString "00000006-0000-0001-0000-000500000008")), - lhcRefreshToken = "i\FS" + { clientId = ClientId 0x1b, + userId = Qualified (Id (fromJust (UUID.fromString "00000008-0000-0002-0000-000300000001"))) (Domain "example.com"), + teamId = Id (fromJust (UUID.fromString "00000004-0000-0008-0000-000300000004")), + refreshToken = "W" } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LegalHoldServiceRemove_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LegalHoldServiceRemove_team.hs index 9506233523a..20d979d1dc3 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LegalHoldServiceRemove_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LegalHoldServiceRemove_team.hs @@ -17,147 +17,37 @@ module Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team where +import Data.Domain (Domain (Domain)) import Data.Id (Id (Id)) +import Data.Qualified (Qualified (Qualified)) import Data.UUID qualified as UUID (fromString) import Imports (fromJust) -import Wire.API.Team.LegalHold.External (LegalHoldServiceRemove (..)) +import Wire.API.Team.LegalHold.External -testObject_LegalHoldServiceRemove_team_1 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_1 = - LegalHoldServiceRemove +testObject_LegalHoldServiceRemoveV0_team_1 :: LegalHoldServiceRemoveV0 +testObject_LegalHoldServiceRemoveV0_team_1 = + LegalHoldServiceRemoveV0 { lhrUserId = Id (fromJust (UUID.fromString "00000034-0000-0016-0000-003c00000024")), lhrTeamId = Id (fromJust (UUID.fromString "0000001e-0000-000f-0000-007100000079")) } -testObject_LegalHoldServiceRemove_team_2 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_2 = - LegalHoldServiceRemove +testObject_LegalHoldServiceRemoveV0_team_2 :: LegalHoldServiceRemoveV0 +testObject_LegalHoldServiceRemoveV0_team_2 = + LegalHoldServiceRemoveV0 { lhrUserId = Id (fromJust (UUID.fromString "0000004f-0000-0076-0000-001f00000019")), lhrTeamId = Id (fromJust (UUID.fromString "00000050-0000-0059-0000-004d00000067")) } -testObject_LegalHoldServiceRemove_team_3 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_3 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "0000001a-0000-0072-0000-003e00000008")), - lhrTeamId = Id (fromJust (UUID.fromString "0000006c-0000-005c-0000-002100000019")) - } - -testObject_LegalHoldServiceRemove_team_4 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_4 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "0000003c-0000-0013-0000-003b00000001")), - lhrTeamId = Id (fromJust (UUID.fromString "0000007c-0000-0060-0000-007400000077")) - } - -testObject_LegalHoldServiceRemove_team_5 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_5 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "00000000-0000-005e-0000-00680000007c")), - lhrTeamId = Id (fromJust (UUID.fromString "0000003f-0000-002e-0000-003900000032")) - } - -testObject_LegalHoldServiceRemove_team_6 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_6 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "0000004b-0000-0014-0000-007e00000010")), - lhrTeamId = Id (fromJust (UUID.fromString "0000005d-0000-0053-0000-005f00000044")) - } - -testObject_LegalHoldServiceRemove_team_7 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_7 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "0000002c-0000-0020-0000-003900000073")), - lhrTeamId = Id (fromJust (UUID.fromString "0000002d-0000-002b-0000-005c0000003c")) - } - -testObject_LegalHoldServiceRemove_team_8 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_8 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "0000003a-0000-0066-0000-001a0000001e")), - lhrTeamId = Id (fromJust (UUID.fromString "00000060-0000-007d-0000-002c00000059")) - } - -testObject_LegalHoldServiceRemove_team_9 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_9 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "00000037-0000-0024-0000-005e00000067")), - lhrTeamId = Id (fromJust (UUID.fromString "0000006e-0000-0072-0000-00260000000a")) - } - -testObject_LegalHoldServiceRemove_team_10 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_10 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "00000077-0000-0003-0000-001b00000033")), - lhrTeamId = Id (fromJust (UUID.fromString "0000000d-0000-0013-0000-007100000063")) - } - -testObject_LegalHoldServiceRemove_team_11 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_11 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "00000062-0000-0018-0000-007b0000002e")), - lhrTeamId = Id (fromJust (UUID.fromString "00000009-0000-007b-0000-00050000004b")) - } - -testObject_LegalHoldServiceRemove_team_12 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_12 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "00000017-0000-0030-0000-002d0000002b")), - lhrTeamId = Id (fromJust (UUID.fromString "00000023-0000-0000-0000-004100000061")) - } - -testObject_LegalHoldServiceRemove_team_13 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_13 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "00000055-0000-005d-0000-00140000001a")), - lhrTeamId = Id (fromJust (UUID.fromString "00000055-0000-0050-0000-000600000019")) - } - -testObject_LegalHoldServiceRemove_team_14 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_14 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "00000015-0000-0061-0000-003e00000067")), - lhrTeamId = Id (fromJust (UUID.fromString "0000001b-0000-005f-0000-006b00000040")) - } - -testObject_LegalHoldServiceRemove_team_15 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_15 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "0000006a-0000-005d-0000-005d00000072")), - lhrTeamId = Id (fromJust (UUID.fromString "0000004e-0000-0066-0000-002c00000021")) - } - -testObject_LegalHoldServiceRemove_team_16 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_16 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "0000005c-0000-0064-0000-00120000002a")), - lhrTeamId = Id (fromJust (UUID.fromString "0000000d-0000-0001-0000-000500000049")) - } - -testObject_LegalHoldServiceRemove_team_17 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_17 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "00000068-0000-001b-0000-006a0000005a")), - lhrTeamId = Id (fromJust (UUID.fromString "00000019-0000-002e-0000-005c00000010")) - } - -testObject_LegalHoldServiceRemove_team_18 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_18 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "0000007d-0000-0044-0000-004d00000004")), - lhrTeamId = Id (fromJust (UUID.fromString "00000019-0000-003f-0000-007000000071")) - } - -testObject_LegalHoldServiceRemove_team_19 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_19 = +testObject_LegalHoldServiceRemove_team_1 :: LegalHoldServiceRemove +testObject_LegalHoldServiceRemove_team_1 = LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "00000040-0000-0053-0000-00060000001b")), - lhrTeamId = Id (fromJust (UUID.fromString "00000014-0000-0022-0000-005a00000075")) + { userId = Qualified (Id (fromJust (UUID.fromString "00000034-0000-0016-0000-003c00000024"))) (Domain "example.com"), + teamId = Id (fromJust (UUID.fromString "0000001e-0000-000f-0000-007100000079")) } -testObject_LegalHoldServiceRemove_team_20 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_20 = +testObject_LegalHoldServiceRemove_team_2 :: LegalHoldServiceRemove +testObject_LegalHoldServiceRemove_team_2 = LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "00000012-0000-005d-0000-00790000003e")), - lhrTeamId = Id (fromJust (UUID.fromString "0000006d-0000-006f-0000-007c0000006e")) + { userId = Qualified (Id (fromJust (UUID.fromString "0000004f-0000-0076-0000-001f00000019"))) (Domain "example.com"), + teamId = Id (fromJust (UUID.fromString "00000050-0000-0059-0000-004d00000067")) } 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 new file mode 100644 index 00000000000..478398eb383 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeaturePatch_team.hs @@ -0,0 +1,81 @@ +{-# LANGUAGE OverloadedLists #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Wire.API.Golden.Generated.LockableFeaturePatch_team where + +import Data.Domain +import Imports +import Wire.API.Team.Feature + +testObject_LockableFeaturePatch_team_1 :: LockableFeaturePatch AppLockConfig +testObject_LockableFeaturePatch_team_1 = LockableFeaturePatch (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just (AppLockConfig (EnforceAppLock False) (-98))) + +testObject_LockableFeaturePatch_team_2 :: LockableFeaturePatch AppLockConfig +testObject_LockableFeaturePatch_team_2 = LockableFeaturePatch Nothing Nothing (Just (AppLockConfig (EnforceAppLock True) 0)) + +testObject_LockableFeaturePatch_team_3 :: LockableFeaturePatch AppLockConfig +testObject_LockableFeaturePatch_team_3 = LockableFeaturePatch (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just (AppLockConfig (EnforceAppLock True) 111)) + +testObject_LockableFeaturePatch_team_4 :: LockableFeaturePatch SelfDeletingMessagesConfig +testObject_LockableFeaturePatch_team_4 = LockableFeaturePatch (Just FeatureStatusEnabled) Nothing (Just (SelfDeletingMessagesConfig (-97))) + +testObject_LockableFeaturePatch_team_5 :: LockableFeaturePatch SelfDeletingMessagesConfig +testObject_LockableFeaturePatch_team_5 = LockableFeaturePatch (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just (SelfDeletingMessagesConfig 0)) + +testObject_LockableFeaturePatch_team_6 :: LockableFeaturePatch SelfDeletingMessagesConfig +testObject_LockableFeaturePatch_team_6 = LockableFeaturePatch (Just FeatureStatusEnabled) Nothing (Just (SelfDeletingMessagesConfig 77)) + +testObject_LockableFeaturePatch_team_7 :: LockableFeaturePatch ClassifiedDomainsConfig +testObject_LockableFeaturePatch_team_7 = LockableFeaturePatch (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just (ClassifiedDomainsConfig [])) + +testObject_LockableFeaturePatch_team_8 :: LockableFeaturePatch ClassifiedDomainsConfig +testObject_LockableFeaturePatch_team_8 = LockableFeaturePatch Nothing (Just LockStatusLocked) (Just (ClassifiedDomainsConfig [Domain "example.com", Domain "test.foobar"])) + +testObject_LockableFeaturePatch_team_9 :: LockableFeaturePatch ClassifiedDomainsConfig +testObject_LockableFeaturePatch_team_9 = LockableFeaturePatch (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just (ClassifiedDomainsConfig [Domain "test.foobar"])) + +testObject_LockableFeaturePatch_team_10 :: LockableFeaturePatch SSOConfig +testObject_LockableFeaturePatch_team_10 = LockableFeaturePatch (Just FeatureStatusDisabled) (Just LockStatusLocked) (Just SSOConfig) + +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_13 :: LockableFeaturePatch DigitalSignaturesConfig +testObject_LockableFeaturePatch_team_13 = LockableFeaturePatch (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just DigitalSignaturesConfig) + +testObject_LockableFeaturePatch_team_14 :: LockableFeaturePatch ConferenceCallingConfig +testObject_LockableFeaturePatch_team_14 = LockableFeaturePatch Nothing (Just LockStatusUnlocked) (Just (ConferenceCallingConfig One2OneCallsSft)) + +testObject_LockableFeaturePatch_team_15 :: LockableFeaturePatch GuestLinksConfig +testObject_LockableFeaturePatch_team_15 = LockableFeaturePatch (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just GuestLinksConfig) + +testObject_LockableFeaturePatch_team_16 :: LockableFeaturePatch SndFactorPasswordChallengeConfig +testObject_LockableFeaturePatch_team_16 = LockableFeaturePatch (Just FeatureStatusDisabled) (Just LockStatusUnlocked) (Just SndFactorPasswordChallengeConfig) + +testObject_LockableFeaturePatch_team_17 :: LockableFeaturePatch SearchVisibilityInboundConfig +testObject_LockableFeaturePatch_team_17 = LockableFeaturePatch (Just FeatureStatusEnabled) Nothing (Just SearchVisibilityInboundConfig) + +testObject_LockableFeaturePatch_team_18 :: LockableFeaturePatch GuestLinksConfig +testObject_LockableFeaturePatch_team_18 = LockableFeaturePatch (Just FeatureStatusEnabled) Nothing Nothing + +testObject_LockableFeaturePatch_team_19 :: LockableFeaturePatch SelfDeletingMessagesConfig +testObject_LockableFeaturePatch_team_19 = LockableFeaturePatch Nothing (Just LockStatusUnlocked) Nothing 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 new file mode 100644 index 00000000000..8c4f9562f39 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeature_team.hs @@ -0,0 +1,104 @@ +{-# LANGUAGE OverloadedLists #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Wire.API.Golden.Generated.LockableFeature_team where + +import Data.ByteString.Conversion (parser, runParser) +import Data.Domain +import Data.Misc +import Imports +import Wire.API.Team.Feature + +testObject_LockableFeature_team_1 :: LockableFeature AppLockConfig +testObject_LockableFeature_team_1 = LockableFeature FeatureStatusEnabled LockStatusUnlocked (AppLockConfig (EnforceAppLock False) (-98)) + +testObject_LockableFeature_team_2 :: LockableFeature AppLockConfig +testObject_LockableFeature_team_2 = LockableFeature FeatureStatusEnabled LockStatusUnlocked (AppLockConfig (EnforceAppLock True) 0) + +testObject_LockableFeature_team_3 :: LockableFeature AppLockConfig +testObject_LockableFeature_team_3 = LockableFeature FeatureStatusEnabled LockStatusLocked (AppLockConfig (EnforceAppLock True) 111) + +testObject_LockableFeature_team_4 :: LockableFeature SelfDeletingMessagesConfig +testObject_LockableFeature_team_4 = LockableFeature FeatureStatusEnabled LockStatusUnlocked (SelfDeletingMessagesConfig (-97)) + +testObject_LockableFeature_team_5 :: LockableFeature SelfDeletingMessagesConfig +testObject_LockableFeature_team_5 = LockableFeature FeatureStatusEnabled LockStatusUnlocked (SelfDeletingMessagesConfig 0) + +testObject_LockableFeature_team_6 :: LockableFeature SelfDeletingMessagesConfig +testObject_LockableFeature_team_6 = LockableFeature FeatureStatusEnabled LockStatusLocked (SelfDeletingMessagesConfig 77) + +testObject_LockableFeature_team_7 :: LockableFeature ClassifiedDomainsConfig +testObject_LockableFeature_team_7 = LockableFeature FeatureStatusEnabled LockStatusLocked (ClassifiedDomainsConfig []) + +testObject_LockableFeature_team_8 :: LockableFeature ClassifiedDomainsConfig +testObject_LockableFeature_team_8 = LockableFeature FeatureStatusEnabled LockStatusLocked (ClassifiedDomainsConfig [Domain "example.com", Domain "test.foobar"]) + +testObject_LockableFeature_team_9 :: LockableFeature ClassifiedDomainsConfig +testObject_LockableFeature_team_9 = LockableFeature FeatureStatusEnabled LockStatusUnlocked (ClassifiedDomainsConfig [Domain "test.foobar"]) + +testObject_LockableFeature_team_10 :: LockableFeature SSOConfig +testObject_LockableFeature_team_10 = LockableFeature FeatureStatusDisabled LockStatusLocked SSOConfig + +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_13 :: LockableFeature DigitalSignaturesConfig +testObject_LockableFeature_team_13 = LockableFeature FeatureStatusEnabled LockStatusLocked DigitalSignaturesConfig + +testObject_LockableFeature_team_14 :: LockableFeature ConferenceCallingConfig +testObject_LockableFeature_team_14 = LockableFeature FeatureStatusDisabled LockStatusUnlocked (ConferenceCallingConfig One2OneCallsTurn) + +testObject_LockableFeature_team_15 :: LockableFeature GuestLinksConfig +testObject_LockableFeature_team_15 = LockableFeature FeatureStatusEnabled LockStatusUnlocked GuestLinksConfig + +testObject_LockableFeature_team_16 :: LockableFeature SndFactorPasswordChallengeConfig +testObject_LockableFeature_team_16 = LockableFeature FeatureStatusDisabled LockStatusUnlocked SndFactorPasswordChallengeConfig + +testObject_LockableFeature_team_17 :: LockableFeature SearchVisibilityInboundConfig +testObject_LockableFeature_team_17 = LockableFeature FeatureStatusEnabled LockStatusUnlocked SearchVisibilityInboundConfig + +testObject_LockableFeature_team_18 :: LockableFeature MlsE2EIdConfig +testObject_LockableFeature_team_18 = + LockableFeature + FeatureStatusEnabled + LockStatusLocked + ( MlsE2EIdConfig + (fromIntegral @Int (60 * 60 * 24)) + Nothing + (either (\e -> error (show e)) Just $ parseHttpsUrl "https://example.com") + False + ) + +parseHttpsUrl :: ByteString -> Either String HttpsUrl +parseHttpsUrl url = runParser parser url + +testObject_LockableFeature_team_19 :: LockableFeature MlsE2EIdConfig +testObject_LockableFeature_team_19 = + LockableFeature + FeatureStatusEnabled + LockStatusLocked + ( MlsE2EIdConfig + (fromIntegral @Int (60 * 60 * 24)) + (either (\e -> error (show e)) Just $ parseHttpsUrl "https://example.com") + Nothing + True + ) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LoginId_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LoginId_user.hs deleted file mode 100644 index 117789dfdf7..00000000000 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LoginId_user.hs +++ /dev/null @@ -1,119 +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 Test.Wire.API.Golden.Generated.LoginId_user where - -import Data.Handle (parseHandle) -import Data.Maybe -import Wire.API.User (Email (Email, emailDomain, emailLocal), Phone (Phone, fromPhone)) -import Wire.API.User.Auth (LoginId (..)) - -testObject_LoginId_user_1 :: LoginId -testObject_LoginId_user_1 = - LoginByEmail - (Email {emailLocal = "~]z^?j\NAK\1088399\1112814X{)\1087092t\f", emailDomain = "\1113045\n\vL$\ENQY\NUL\DELUj?H%"}) - -testObject_LoginId_user_2 :: LoginId -testObject_LoginId_user_2 = LoginByPhone (Phone {fromPhone = "+178807168"}) - -testObject_LoginId_user_3 :: LoginId -testObject_LoginId_user_3 = - LoginByEmail - ( Email - { emailLocal = "0\1088863^\1000125\144267\NUL)|\183379:", - emailDomain = "q6e/$\1033221Zb\1050001)\991223\&05i\20077~q\1071660\128584y" - } - ) - -testObject_LoginId_user_4 :: LoginId -testObject_LoginId_user_4 = LoginByHandle (fromJust (parseHandle "7a8gg3v98")) - -testObject_LoginId_user_5 :: LoginId -testObject_LoginId_user_5 = LoginByPhone (Phone {fromPhone = "+041157889572"}) - -testObject_LoginId_user_6 :: LoginId -testObject_LoginId_user_6 = LoginByPhone (Phone {fromPhone = "+2351341820189"}) - -testObject_LoginId_user_7 :: LoginId -testObject_LoginId_user_7 = LoginByHandle (fromJust (parseHandle "lb")) - -testObject_LoginId_user_8 :: LoginId -testObject_LoginId_user_8 = LoginByPhone (Phone {fromPhone = "+2831673805093"}) - -testObject_LoginId_user_9 :: LoginId -testObject_LoginId_user_9 = LoginByPhone (Phone {fromPhone = "+1091378734554"}) - -testObject_LoginId_user_10 :: LoginId -testObject_LoginId_user_10 = - LoginByHandle (fromJust (parseHandle "z58-6fbjhtx11d8t6oplyijpkc2.fp_lf3kpk3_.qle4iecjun2xd0tpcordlg2bwv636v3cthpgwah3undqmuofgzp8ry6gc6g-n-kxnj7sl6771hxou7-t_ps_lu_t3.4ukz6dh6fkjq2i3aggtkbpzbd1162.qv.rbtb6e.90-xpayg65z9t9lk2aur452zcs9a")) - -testObject_LoginId_user_11 :: LoginId -testObject_LoginId_user_11 = - LoginByEmail - ( Email - { emailLocal = "\154036\140469A\1031528ovP Ig\92578t';\6199\SOHC\29188\157632{\n%\1090626\v2\GS\180557\1112803&", - emailDomain = "m\180009U{f&.3\3846\&1?Ew\30701G-" - } - ) - -testObject_LoginId_user_12 :: LoginId -testObject_LoginId_user_12 = - LoginByEmail (Email {emailLocal = "", emailDomain = "\18232\EM+h\ENQ(D\SO\28757\993545 \a\r1"}) - -testObject_LoginId_user_13 :: LoginId -testObject_LoginId_user_13 = - LoginByEmail - ( Email - { emailLocal = "5-h\1094050\1011032&$og\1084464\26226\989383<%\2855\fGF-yJ\f*cK", - emailDomain = "*g\EM\120758\&7$L\CAN\59033\57589\tV\1102330D\a\\yK\1090380T" - } - ) - -testObject_LoginId_user_14 :: LoginId -testObject_LoginId_user_14 = LoginByPhone (Phone {fromPhone = "+8668821360611"}) - -testObject_LoginId_user_15 :: LoginId -testObject_LoginId_user_15 = - LoginByEmail - ( Email - { emailLocal = "\ACK\ENQX\ACK&\94893\&8\1044677\&7E`Y'\DC1TV\ACK\DLE", - emailDomain = "\GS\ESCj\999191,j\994949\1043277#a1)}\DC3Vk\SOHQ7&;" - } - ) - -testObject_LoginId_user_16 :: LoginId -testObject_LoginId_user_16 = - LoginByEmail - ( Email - { emailLocal = "\1013039\&1", - emailDomain = - "\v`\EM\49692v\1082687;F\18618\&0\4155Sgu%>\1076869y\v\1018080\NAK\133308\US\1025555\ACKs\SI\a\US" - } - ) - -testObject_LoginId_user_17 :: LoginId -testObject_LoginId_user_17 = LoginByHandle (fromJust (parseHandle "e3iusdy")) - -testObject_LoginId_user_18 :: LoginId -testObject_LoginId_user_18 = - LoginByHandle (fromJust (parseHandle "8vpices3usz1dfs4u2lf_e3jendod_szl1z111_eoj4b7k7ajj-xo.qzbw4espf3smnz_")) - -testObject_LoginId_user_19 :: LoginId -testObject_LoginId_user_19 = LoginByHandle (fromJust (parseHandle "3jzpp2bo8")) - -testObject_LoginId_user_20 :: LoginId -testObject_LoginId_user_20 = LoginByEmail (Email {emailLocal = "", emailDomain = "\155899"}) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Login_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Login_user.hs deleted file mode 100644 index e0b6a4cf88a..00000000000 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Login_user.hs +++ /dev/null @@ -1,227 +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 Test.Wire.API.Golden.Generated.Login_user where - -import Data.Code -import Data.Handle (parseHandle) -import Data.Maybe -import Data.Misc (plainTextPassword6Unsafe) -import Data.Range (unsafeRange) -import Data.Text.Ascii (AsciiChars (validate)) -import Imports -import Wire.API.User (Email (Email, emailDomain, emailLocal), Phone (Phone, fromPhone)) -import Wire.API.User.Auth - -testObject_Login_user_1 :: Login -testObject_Login_user_1 = - PasswordLogin - ( PasswordLoginData - (LoginByEmail (Email {emailLocal = "4\1069339\vEaP", emailDomain = "\ENQ\n\FS\ESC\997356i03!"})) - ( plainTextPassword6Unsafe - "\b5Ta\61971\150647\186716fa&\1047748o!ov\SI\1100133i\DC4\ETXY\SOR\991323\1086159Ta^s\ETB\SI[\189068\988899\26508\CAN6\STXp\1069462-9\983823&\NAK\1052068]^\13044;>-Z$Z\NAK\r\1101550a\RS%\NUL:\188721\47674\157548?e]\ETX \142608 C\SOH\SIS%8m\1091987V\147131[\1006262\&6\171610\1011219\164656SX\n%\1061259*>\t+\132427Y\989558\993346\GSU\1067541\&6TU!*\40114\&90\1055516\RSV\162483N\t*\EOT{I<\1084278\SOH\183116!c\\\n\1107501\183146\DC1,-xX\EMV?\t\168648\1054239\DC2\DEL1\SOHu\SOH\63459\53061\SO+h\ACK::\RS\21356_g,\SO*\v\DC4\1093710HFF\188918\1081075fF\ESC2\SOHT\DC1)\fc\35905l\1061547\f#~\STX]\1035086/Or)kY\1031423\SOHNCk\1067954\&5\1083470x=H\NUL\23760\1058646\1099097E/$\DELpbi\137522\FSKi\15676\1018134\t7\"OL\54208\7516\&5\43466\NUL(\1030852\166514\SOH\149343\994835\25513C==\GSTV3\DELl6\999006.Z)$\16723|\172732\1090303J;O\GSbw\vI\1101024I\SYN\DC2^\149630\STX3%i\EMW\138614\DC4\1113619tsL5\147087W\96700(_,\1091179*\1041287rckx\SOH\SIs\SOHJd\140574\SYNev.\DC4\DLE\99082.\1106785\996992\143448\US_\ETBf\STX\SO\DC3\1043748\&6O\DC1Q\SOH'\GS,|]W\SIa\62568\151062.\v\aH&-L\DC2+\147179\1095524\EOTm)\19925\181147\183368!\185223\142946m\DC4\DC3\1034282m\GS\185509>>\"NDw\1076877hY\1033831sFKz^ \1108187\&5Qec\NAK}|\1108194.Q\173114imb\1027220 p;\1089082\SYN\1065748kF\1102854r8o\DC1" - ) - (Just (CookieLabel {cookieLabelText = "r"})) - Nothing - ) - -testObject_Login_user_2 :: Login -testObject_Login_user_2 = - SmsLogin - ( SmsLoginData - (Phone {fromPhone = "+956057641851"}) - (LoginCode {fromLoginCode = "\nG\1076650\&8\b"}) - (Just (CookieLabel {cookieLabelText = "G"})) - ) - -testObject_Login_user_3 :: Login -testObject_Login_user_3 = - PasswordLogin - ( PasswordLoginData - (LoginByHandle (fromJust (parseHandle "c2wp.7s5."))) - ( plainTextPassword6Unsafe - "&\RS\DC4\1104052Z\11418n\SO\158691\1010906/\127253'\1063038m\1010345\"\9772\138717\RS(&\996590\SOf1Wf'I\SI\100286\1047270\1033961\DC1Jq\1050673Y\\Bedu@\1014647c\1003986D\53211\1050614S\144414\ETX\ETXW>\1005358\DC4\rSO8FXy\166833a\EM\170017\SUBNF\158145L\RS$5\NULk\RSz*s\148780\157980\v\175417\"SY\DEL\STX\994691\1103514ub5q\ENQ\1014299\vN.\t\183536:l\1105396\RS\1027721\a\168001\SO\vt\1098704W\SYN\1042396\1109979\a'v\ETB\64211\NAK\59538\STX \NAK\STX\49684,\1111630x\1047668^\1067127\27366I;\NAKb\1092049o\162763_\190546MME\1022528\SI\1096252H;\SO\ETBs\SO\1065937{Knlrd;\35750\DC4\SI\1075008TO\1090529\999639U\48787\1099927t\1068680^y\17268u$\DC1Jp\1054308\164905\164446\STX\"\1095399*\SO\1004302\32166\990924X\1098844\ETXsK}\b\143918\NUL0\988724\&12\171116\tM052\189551\EOT0\RS\986138\1084688{ji\ESC\1020800\27259&t \SI\ESCy\aL\136111\131558\994027\r\1054821ga,\DC4do,tx[I&\DC4h\DLE\ETX\DLEBpm\1002292-\a]/ZI\1033117q]w3n\46911e\23692kYo5\1090844'K\1089820}v\146759;\1018792\\=\41264\&8g\DLEg*has\44159\1006118\DC3\USYg?I\19462\NAKaW2\150415m\t}h\155161RbU\STX\ETBlz2!\DC3JW5\ESC\1026156U\SOg,rpO\5857]0\ESC\479\1005443F\SI\1045994\RS\SO\11908rl\1104306~\ACK+Mn{5\993784a\EM2\v{jM\ETBT\1058105$\DC1\1099974\GSj_~Z\1007141P\SOH\EOTo@TJhk\EOT\ETBk:-\96583[p\DLE\DC1\RS'\r\STXQ,,\1016866?H\rh\30225\rj\147982\DC2\\(u\ESCu\154705\1002696o\DC4\988492\1103465\1052034\DC1q\GS-\b\40807\DC1qW>\fys\8130,'\159954<" - ) - (Just (CookieLabel {cookieLabelText = "\1082362\66362>XC"})) - (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))})) - ) - -testObject_Login_user_4 :: Login -testObject_Login_user_4 = - SmsLogin - ( SmsLoginData - (Phone {fromPhone = "+04332691687649"}) - (LoginCode {fromLoginCode = "\94770m"}) - (Just (CookieLabel {cookieLabelText = ":"})) - ) - -testObject_Login_user_5 :: Login -testObject_Login_user_5 = - PasswordLogin - ( PasswordLoginData - ( LoginByHandle - (fromJust (parseHandle "c372iaa_v5onjcck67rlzq4dn5_oxhtx7dpx7v82lp1rhx0e97i26--8r3c6k773bxtlzmkjc20-11_047ydua_o9_5u4sll_fl3ng_0sa.")) - ) - ( plainTextPassword6Unsafe - "\120347\184756DU\1035832hp\1006715t~\DC2\SOH\STX*\1053210y1\1078382H\173223{e\\S\SO?c_7\t\DC4X\135187\&6\172722E\100168j\SUB\t\SYN\1088511>HO]60\990035\ETX\"+w,t\1066040\ak(b%u\151197`>b\1028272e\ACKc\151393\1107996)\12375\&7\1082464`\186313yO+v%\1033664\rc<\65764\&2>8u\1094258\1080669\1113623\75033a\179193\NAK=\EOT\1077021\&8R&j\1042630\ESC\t4sj-\991835\40404n\136765\1064089N\GS\\\1026123\72288\&5\r\97004(P!\DEL\29235\26855\b\1067772Mr~\65123\EMjt>Z\GS~\140732A\1031358\SO\\>\DC16\">%\45860\1084751I@u5\187891\vrY\r;7\1071052#\1078407\1016286\CAN'\63315\1041397\EM_I_zY\987300\149441\EMd\1039844cd\DEL\1061999\136326Cp3\26325\GSXj\n\46305jy\44050\58825\t-\19065\43336d\1046547L\SUBYF\ACKPOL\54766\DC2\DC1\DC1\DC2*\rH\DLE(?\DC3F\25820\DLE\r]\1069451j\170177 @\ENQT\1100685s\FSF2\NAK]8\a\DC3!\NAKW\176469\1110834K\1025058\1112222_%\1001818\1113069'\1098149\70360(#\SOHky\t\ETB!\17570\NAK\DC4\ESC{\119317U2LS'" - ) - (Just (CookieLabel {cookieLabelText = "LGz%\119949j\f\RS/\SOH"})) - (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))})) - ) - -testObject_Login_user_6 :: Login -testObject_Login_user_6 = - PasswordLogin - ( PasswordLoginData - (LoginByPhone (Phone {fromPhone = "+930266260693371"})) - ( plainTextPassword6Unsafe - "K?)V\148106}_\185335\1060952\fJ3!\986581\1062221\51615\166583\1071064\a\1015675\SOH7\\#z9\133503\1081163\985690\1041362\EM\DC3\156174'\r)~Ke9+\175606\175778\994126M\1099049\"h\SOHTh\EOT`;\ACK\1093024\ENQ\1026474'e{\FSv\40757\US\143355*\16236\1076902\52767:E]:R\1093823K}l\1111648Y\51665\1049318S~\EOT#T\1029316\&1hIWn\v`\45455Kb~\ESC\DLEdT\FS\SI\1092141f\ETBY7\DEL\RS\131804\t\998971\13414\48242\GSG\DC3BH#\DEL\\RAd\166099g\1072356\1054332\SIk&\STXE\22217\FS\FS\FS$t\1001957:O\1098769q}_\1039296.\SOH\DC4\STX\157262c`L>\1050744l\1086722m'BtB5\1003280,t\"\1066340\&9(#\ENQ4\SIIy>\1031158\1100542\GSbf\"i\ETB\14367a\1086113C@\1078844\1092137\32415\NAK\999161\23344*N\SYN\ESC:iXibA\136851\169508q\1048663]:9r\63027\73801\NUL\1050763\USCN\US\147710\1048697\1016861eR\RSZbD5!8N\ESCV\7344\ACK\173064\SUBuz\1053950\188308~\ESC\SI%{3I/F\25232/DMS\US>o\187199\63000Z\1108766\GS[K\184801\94661\1088369\995346\ESCO-4\CAN\US\FSZp" - ) - (Just (CookieLabel {cookieLabelText = "\1014596'\998013KW\\\NUL\DC4"})) - (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))})) - ) - -testObject_Login_user_7 :: Login -testObject_Login_user_7 = - PasswordLogin - ( PasswordLoginData - (LoginByEmail (Email {emailLocal = "BG", emailDomain = "\12137c\v}\SIL$_"})) - ( plainTextPassword6Unsafe - "&\991818\1023244\83352\STXJ<-~\STX>\v\74228\151871\&5QN\53968\166184ql\NAK\74290\&3}{\DC3\173242S\22739;\t7\183958_F~D*f\1049940)\1067330-9\20699\&7GK= %\RS@kOF#\179945\1094401\124994\&8_\42309\GSL\37698\ETX\1047946\&0Wl1A`LYz\USy\20728\SUBo\ESC[\DC4\bt\66640a\ETXs~\USF\175140G`$\vG\DC1\1044421\128611/\1014458C>\SI" - ) - (Just (CookieLabel {cookieLabelText = "\SO\NAKeC/"})) - (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))})) - ) - -testObject_Login_user_8 :: Login -testObject_Login_user_8 = - PasswordLogin - ( PasswordLoginData - (LoginByEmail (Email {emailLocal = "", emailDomain = "~^G\1075856\\"})) - ( plainTextPassword6Unsafe - "z>\1088515\1024903/\137135\1092812\b%$\1037736\143620:}\t\CAN\1058585\1044157)\12957\1005180s\1006270\CAN}\40034\EM[\41342\vX#VG,df4\141493\&8m5\46365OTK\144460\37582\DEL\44719\9670Z\"ZS\ESCms|[Q%\1088673\ENQW\\\1000857C\185096+\1070458\4114\17825v\180321\41886){\1028513\DEL\143570f\187156}:X-\b2N\EM\USl\127906\49608Y\1071393\1012763r2.1\49912\EOT+\137561\DC3\145480]'\1028275s\997684\42805.}\185059o\992118X\132901\11013\r\SUBNq6\1019605'\fd\RS\14503\1097628,:%\t\151916\73955QD\1086880\ESC(q4KDQ2zcI\DLE>\EM5\993596\&1\fBkd\DC3\ACK:F:\EOT\100901\11650O N\FS,N\1054390\1000247[h\DEL9\5932:xZ=\f\1085312\DC3u\RS\fe#\SUB^$lkx\32804 \rr\SUBJ\1013606\1017057\FSR][_5\NAK\58351\11748\35779\&5\24821\1055669\996852\37445K!\1052768eRR%\32108+h~1\993198\35871lTzS$\DLE\1060275\"*\1086839pmRE\DC3(\US^\8047Jc\10129\1071815i\n+G$|\993993\156283g\FS\fgU3Y\119068\ACKf)\1093562\SYN\78340\1100638/\NULPi\43622{\1048095j\1083269\FS9\132797\1024684\32713w$\45599\126246)Si\167172\29311FX\1057490j{`\44452`\999383\159809\&4u%\1070378P*\1057403\25422\DELC\RSR\SYN-\51098\1011541g\68666:S>c\15266\132940\DLEY\1066831~a)YW_J\1063076P\a+ U\1084883j\EMk\SOH\1096984\DC1\18679e\172760\175328,\5135g@\DC2\GSHXl.\ETB\153793\&2\DC3mY\1054891\tv?L8L\1074044N\133565\nb1j\1044024\148213xfQ=\\\ENQe\995818\1023862U\DC2p{\SO\1099404jd^@U\994269tP.\DC2Y%R`a\r\160622\&7}HnUf\132856m^7:\NAK=\52348>l\95313hwp27\149950jE\fx=!.\DC3]Ar\tw\DC4&\SUBk\194572s\1042820\4498I\146071\61461\1060645dsY\DLE\181922dX.\146295i]\151113\1028288\rWS\USU\1098732\SUB\49884\1083906\DLE\STXN~-\SO6\190031\1110322\\O\185165Jc\1052359\1071278\NULHSo\DLE-W\DC36\170321I\1068712)\99800={\99796h\27961\61707M\1022570FwJQ\1111976ck\SUB\CAN|UV-\NAK\SOH|\DC4;\f\156907\145795\ENQS\NAK.B\"D\163007#o*\126577\32988m\RS\1049834B3Gg;\DC1\\\180659\1098926\ENQ B^\SI\152630$e\39220\170037>fMgC\187276,o\128488\\?\1033955~/s\SOH?MMc;D18Ne\EOT\CAN)*\STX\GS\162681/\t\NAK \1010386\1013311z\33488Bv\1109131(=<\SOq\1104556?L\6845\1066491\2972c\997644<&!\1103500\999823j~O3USw\DC2\ETX\a\ETB+\1024033Ny\31920(/Sco\STX{3\SIEh\SYN\1032591\1022672\27668-\FS.'\ENQX\98936\150419Ti3\1051250\"%\SYN\b\188444+\EOT\STX^\1108463)2bR\ACK\SIJB[\1045179&O9{w{aV\ENQgZ?3z\1065517\&8\4979\156950\990517`\1063252\"PE)uKq|w\SYN0\ESC. \ETX\73440sxW\160357\1001111m\ENQ7e)\77912\1008764:s\CANYj\9870\16356\ACK\USlTu\1110309I.\1087068O#kQ\RS!g\1062167\CANQ\US\172867\SYN\ACK|\"M\"P\US\ETX@ZPq\1016598gY\148621=\a\1057645l8\1041152\&3\995012\1022626CN<\147876gJ\1038434]\94932mX~\ACKw3\DLE\179764\&8\a6\EOT}\DLEi\DC3L5\1032336PY^|!Vz\ESC4\36208!iLa\12091\DC4\1059706\167964\GS:\1042431\149640h\\dLx\1087701\EM\194900\SUB\134635R%ps7\95168s\1074387fg\nIf\1067199\DC1l\SUB\1022871-n_\6065UY?4d]|c\\[T\ajS\18838\55046\37136aK\1025430\1112672\ETX\FSx+" - ) - (Just (CookieLabel {cookieLabelText = ""})) - (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))})) - ) - -testObject_Login_user_10 :: Login -testObject_Login_user_10 = - SmsLogin - ( SmsLoginData - (Phone {fromPhone = "+4211134144507"}) - (LoginCode {fromLoginCode = "\13379\61834\135400!\ETBi\1050047"}) - (Just (CookieLabel {cookieLabelText = ""})) - ) - -testObject_Login_user_11 :: Login -testObject_Login_user_11 = - SmsLogin (SmsLoginData (Phone {fromPhone = "+338932197597737"}) (LoginCode {fromLoginCode = "\1069411+W\EM3"}) Nothing) - -testObject_Login_user_12 :: Login -testObject_Login_user_12 = - PasswordLogin - ( PasswordLoginData - (LoginByPhone (Phone {fromPhone = "+153353668"})) - ( plainTextPassword6Unsafe - "n\1095465Q\169408\ESC\1003840&Q/\rd\43034\US\EOTw2C\ACK\1056364\178004\EOT\EOTv\1010012\bf,b\DEL\STX\1013552'\175696C]G\46305\1017071\190782\&4\NULY.\173618\SO3sI\194978F\1084606\&5\21073rG/:\"\1013990X\46943\&6\FS:\CAN\aeYwWT\1083802\136913Msbm\NAK@\984540\1013513\EOT^\FS\147032\NAK@\ENQ>\f\RSUc\EOTV9&c\3517\a\986228a'PPG\100445\179638>[\3453\&2\64964Xc\131306[0\1002646\b\99652B\DC1[\1029237\GS\19515\US\EMs-u\ETBs\1067133\1005008\161663n\1072320?\1045643ck\DC48XC\174289\RSI2\2862\STX\DLEM\ESC\n?<\\\DC3E\72219\GS\n$cyS\136198!,\v9\ETB/\DC1\62324?P\ETB\41758\DC2\999537~\1058761W-W4K8.\DC27\EML\1078049h\SI}t+H\SUB\ESCX\120523s\EOTt\177703taa\GS\f\152365(v\1024552M\ESCvg3P1\1032835\57603]g\3933\&4T\NAK$\38212);\\8\1109165\nK\NAK}D'^fJ'\143205e\174052\39597!\EM.\DC2{\\CEp\1045384\ETBk_\1083904\18397\164138\1063468]MG$\187650[E\1112126\b\1073487{b\50650\ESC^b@W\NAK$\FS<\1023895&\155992R\ACKJ\SI\1093108\1101041\41438n\1007134\&8]\148288\ENQ}|k\STX\CANQ\USI\a\CANDZ\1062877\NUL\50197rb\18947\&3G%\FS\162081\EOT\NAK4YB0-i\1018065IM\1073908[\1111554:Cr$\99636)L\136837W\40897.x;\41461\1030711\995525\USkb\CANY9)\SYN4\SI\1103461Av.\r\f\1061861\&9{\SO\ETBP\f\33538u\r-9cB4\1016091G\RS\22817\1014740r\128247HcsPm\59419s\120987!|J<\DLE8\FS[\NAKWYAK\75011^\987050c3\1042176\aC\ETX\ETB\1053739Y\DC4f\ACK\1060945!\1032209:RlQ!BX\f=\1070694f\151362\DEL\113727O\ETX\\\"\53275B<\RSLV4g%3\1098063\ACK`\NAK>\n\44626kp\986102\171479\DEL\60526H\20888lyJ\DC2)\1055149(\1027099A\FSh\EOTj\35251\DC4M\ESCP-q\bn\CAN\143310~\GS\EM\"o\21512%*e2\165597L\1023807sy\152913\&2m\GS\1049046{EG]\DC16B+{\983622IYa\1008153\&5,<\ESCX\f\SI\186613\153744E\134407\1011088L<\EMdUO\ETB\SUBZYm\ACK\1086320R\SUB\991954\DC3^\60967s\fu_g\EM?i~}\DELV2\148681R\FS\EOT3j\45841m\1542\1100884\n7S\SIT5j\170914\SI\1015133\141587h\182480Q\146618\59914\DEL\NAKZM\1110574\&02f\129340l!*\SOH\1027033\SOH\1070384\1094775\t\72805\ESCa:q UKEN\RS-\n\ETXH\22365a\1074707\b\37494\"\1035508\149695\1033139R4\ETX\DLE\FS\STX\1004750%\"@\1009369\&6=/x\NULP\EOT\174871/\190041\f\f\1005146?*\fIcKW\DELQ\"\1001726P*\1095849\&6=d\n\157680\RS\1087962\EOT\DC2I\47501U\b=Pc\DLE" - ) - (Just (CookieLabel {cookieLabelText = "\SI\128787-\125004:\136001\39864\ACK\SO"})) - (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))})) - ) - -testObject_Login_user_13 :: Login -testObject_Login_user_13 = - SmsLogin (SmsLoginData (Phone {fromPhone = "+626804710"}) (LoginCode {fromLoginCode = "&\1040514y"}) Nothing) - -testObject_Login_user_14 :: Login -testObject_Login_user_14 = - SmsLogin - ( SmsLoginData - (Phone {fromPhone = "+5693913858477"}) - (LoginCode {fromLoginCode = ""}) - (Just (CookieLabel {cookieLabelText = "\95804\25610"})) - ) - -testObject_Login_user_15 :: Login -testObject_Login_user_15 = - SmsLogin - ( SmsLoginData - (Phone {fromPhone = "+56208262"}) - (LoginCode {fromLoginCode = ""}) - (Just (CookieLabel {cookieLabelText = "q\ETB(\1086676\187384>8\141442\n6"})) - ) - -testObject_Login_user_16 :: Login -testObject_Login_user_16 = - SmsLogin - ( SmsLoginData - (Phone {fromPhone = "+588058222975"}) - (LoginCode {fromLoginCode = "_\1110666\1003968\1108501-_\ETB"}) - (Just (CookieLabel {cookieLabelText = "\SOL\1079080\1008939\1059848@\FS\DLE$"})) - ) - -testObject_Login_user_17 :: Login -testObject_Login_user_17 = - SmsLogin - ( SmsLoginData - (Phone {fromPhone = "+3649176551364"}) - (LoginCode {fromLoginCode = "\ETB1\1002982n\DLEdV\1030538d\SOH"}) - (Just (CookieLabel {cookieLabelText = "\1112281{/p\100214"})) - ) - -testObject_Login_user_18 :: Login -testObject_Login_user_18 = - SmsLogin - ( SmsLoginData - (Phone {fromPhone = "+478931600"}) - (LoginCode {fromLoginCode = ",\139681\13742,"}) - (Just (CookieLabel {cookieLabelText = "5"})) - ) - -testObject_Login_user_19 :: Login -testObject_Login_user_19 = - SmsLogin - ( SmsLoginData - (Phone {fromPhone = "+92676996582869"}) - (LoginCode {fromLoginCode = "x\27255<"}) - (Just (CookieLabel {cookieLabelText = "w;U\ESCx:"})) - ) - -testObject_Login_user_20 :: Login -testObject_Login_user_20 = - PasswordLogin - ( PasswordLoginData - (LoginByEmail (Email {emailLocal = "[%", emailDomain = ","})) - ( plainTextPassword6Unsafe - "ryzP\DC39\11027-1A)\b,u\8457j~0\1090580\1033743\fI\170254er\DC4V|}'kzG%A;3H\amD\STXU1\NUL^\1043764\DLEO&5u\EOT\SUB\167046\&0A\996223X\DC2\FS7fEt\97366rPvytT\136915!\100713$Q|BI+EM5\NAK\t\DELRKrE\DLE\US\r?.\STX|@1v^\vycpu\n$\DC2\186675\131718-Q\151081\n\r\1033981\68381O\ENQ*\68660Z\USo\EOTn\188565%&\DC3Me*\STX;\DLE034\nv\NAK\140398(\1075494\990138n@\1108345|\48421d\n*\SI\NUL}\NAKA!\1045882\1036527Hx\ETB3\STX{#T|5|GC\1089070z.\USN\1080851\22324\vu\SYN~LP\147583CV\SO q\151952\DC2e8h\USg\1019358;\f\996107\1108688At\1022346)\USG\DC3\166541\39337|\1042043\SI\134073\EOTc~6\DLE:u\165393##^\nn{d\CAN\ng\16237\ESC\US\US~A8};T\RS\NAK)&\b\ACK\1106044\GS(\DC3u;\1094683;=e\1051162\"\40669vCt)o\987006m\43912\78088l1+\1036284[\STXFLx\1080932:\1031973\992752\&71/kE\93787p\DC4Ij\ETB\194985&\SUB^\FSl1\ACK\1019548\ETXW,+3\128058\95671\DLE7\59727\&7rG'\1078914JC9M\1053804\SYN\DC2\44350>~\1016308Y\1062059=i-\fS\172440\156520K2-@\ENQ\f\1108851_1D-&\128386lR\187248/\993988$:\31415:\52267Dg\1015243O\1010173\170117\SO\179807\&2z\NAKq\141547c\FSliJ{\1055925\1060070'BL\168670;\STX\1046844\18443B\NUL\7839b\1072569:w\1108016Ad\SUB6\NAKo\55279\nsPWM{\ETXfW\1018373JT\1021361$\989069\54608\190318\173259u4\1103286\t\34021\1039458\"\153264UM\1084148\1095406\34105\1105325\t\nIn'\1070532\21097\16091\EM\DC1<\v\bW\SI}\141807\b\1072339\1035283\GS`\1094467x\NUL\986937K\FSj\1079287\DC1\SI\168992d\991620k4\SUB\1009876\49943^\58464\1052547\1016875i2=$:[f\1064579\DC2n\NAKJ<=\2028\SI!z\1105364\SON\NAK\EM\180748V\1024876CQ_G\nY#ky\132779k\DC3\ENQ}OC\96566}~M\EMp\ETX\RSx\b\183962\1073008\b8/\DC4?\1081654B\1025870\EOT\SO\DELU\1020905\ESC=%\51062J\168855\ETB\992593\990312\985186\to\1101036X_@@\45111\43952$" - ) - (Just (CookieLabel {cookieLabelText = "\1055424\r9\998420`\NAKx"})) - (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))})) - ) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewClient_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewClient_user.hs index 17b3893bef6..98bb2187a95 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewClient_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewClient_user.hs @@ -66,7 +66,7 @@ testObject_NewClient_user_2 = "I\1065423\995547oIC\by\1045956\&1\13659&w>S~z\35967\a{2Dj\v|Z\"\f\1060612*[\65357V\1086491kS\145031A\1106044\1056321(2\DLE\48205\SOi\SI(\1032525\168748f?q\SO5\146557d\1068952^nI\1103535_?\1019210H\119099\SUBf\995865\n\1004095x\ACKdZ\1053945^N\fa\SYN\SUBb=\1112183SP\128516aTd\EM\186127\DC3\ACK\ETB!\1011808\142127o{uoN\CANqL\NAK\ESCc=\v@o2\1043826\EOT\142486\US\1079334\&5v\STX\GS_k,\DC3mAV>$\1029013\1061276\RS\1089843\n\8980-\60552ea}G`r? \DEL\1004551\SOH\US\132757\&9\brl\155069}u\120967\1080794\1062392@M6M\155107\98552\167588|E5Ud\1051152tLjQ\1022837\6734\RS\v\DC1jE\ACK'~f\SIR\1010717\NAKd}}\1059960q\1031766\DC1\151174\&9\160469\RS\100592\ETX\186780\DEL\r\FS\US\36812\14285\NAK/\GS\25526\1090814\61061\NUL(:\1054313n#m9x \1078109\183480}\1052622\54486\GS\991929\b`\1087609G#T\DC2-8\NAK\18310\134655\tp/!\STX4C\SUB'DP'.\a\1110090\&8<9\SYN\NAKEq\168018Ep]\ajZ%\1025589\4170O\35069>\CAN\ACKw*f<\1102303\SOjzpjY\US\SUB\19086\DC1\DC1\ACK|\SO\1064500;\135633F!f\19971b%\1048714t9\DC2\f\121106X! \133247C\RS\1029038\162320C!\20923H(/\GSV)e\SYN2\NUL#H$BAJy\ETB\162654X\137014\FS\SUB\DEL~\f\ESC;\n<\GSf~{\b_" ), newClientModel = Just "om", - newClientCapabilities = Just (Set.fromList [ClientSupportsLegalholdImplicitConsent]), + newClientCapabilities = Just (ClientCapabilityList (Set.fromList [ClientSupportsLegalholdImplicitConsent])), newClientMLSPublicKeys = mempty, newClientVerificationCode = Nothing } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewConv_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewConv_user.hs index 654575f9943..65d223dcd11 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewConv_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewConv_user.hs @@ -19,7 +19,6 @@ module Test.Wire.API.Golden.Generated.NewConv_user where -import Data.Domain (Domain (Domain)) import Data.Id import Data.Misc (Milliseconds (Ms, ms)) import Data.Set qualified as Set (fromList) @@ -29,9 +28,6 @@ import Wire.API.Conversation import Wire.API.Conversation.Role import Wire.API.User -testDomain :: Domain -testDomain = Domain "testdomain.example.com" - testObject_NewConv_user_1 :: NewConv testObject_NewConv_user_1 = NewConv diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewPasswordReset_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewPasswordReset_user.hs index f21c63bcbe2..d583fc33a6d 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewPasswordReset_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewPasswordReset_user.hs @@ -23,8 +23,7 @@ import Wire.API.User.Password testObject_NewPasswordReset_user_1 :: NewPasswordReset testObject_NewPasswordReset_user_1 = NewPasswordReset - ( Email - { emailLocal = "\1007057b\1098950\&9#\34943\DLEX2o\6661\171973\60563t", - emailDomain = "\1080376\60900\DC1\41907s\f\98453}\CAN\SO\n8\SUBz\169687\n\154344Zdb#\SUB4IM8\67225+" - } + ( unsafeEmailAddress + "some" + "example.com" ) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewProvider_provider.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewProvider_provider.hs index 3524b4c0085..c8367a9dc66 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewProvider_provider.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewProvider_provider.hs @@ -43,14 +43,14 @@ import URI.ByteString ), ) import Wire.API.Provider (NewProvider (..)) -import Wire.API.User.Identity (Email (Email, emailDomain, emailLocal)) +import Wire.API.User.Identity import Wire.API.User.Profile (Name (Name, fromName)) testObject_NewProvider_provider_1 :: NewProvider testObject_NewProvider_provider_1 = NewProvider { newProviderName = Name {fromName = "\1017845\&8\1098296\58272:(\DLE\1000399f\1005296\a\13015\1080816\&4K\42948L"}, - newProviderEmail = Email {emailLocal = "Y", emailDomain = "\1109538>\SI_\SUB0"}, + newProviderEmail = unsafeEmailAddress "some" "example", newProviderUrl = coerce URI @@ -82,7 +82,7 @@ testObject_NewProvider_provider_2 = NewProvider { newProviderName = Name {fromName = "Tx\47851q\1065280@\SUB\1025401\aW\FSG\DC4\DLE$\DEL\63098\DC4$`\vO?\"Z\1046679\1083355\&5K"}, - newProviderEmail = Email {emailLocal = "S\999953k\RS\49099G", emailDomain = "T,"}, + newProviderEmail = unsafeEmailAddress "some" "example", newProviderUrl = coerce URI @@ -117,7 +117,7 @@ testObject_NewProvider_provider_3 = { fromName = "!\STXOF\1038347\1063280\1099707\1025890\162780\97458+|\RS\DC2NYwPo\\%\181456vWF\48577\DC4\FSk\1059188\8605\RSCQv\1054931\1007187b\162434\152999(\139131_\48010\r\1080015\1097027r\ENQ\1051284\997772\1025219A,y%|\10517\USR\"\1008217w*\1067361B8\1001156k!{\19881M|e" }, - newProviderEmail = Email {emailLocal = "y\SO\DEL\120818", emailDomain = " e"}, + newProviderEmail = unsafeEmailAddress "some" "example", newProviderUrl = coerce URI @@ -152,7 +152,7 @@ testObject_NewProvider_provider_4 = { fromName = "\22553\ETB\SOH\r\n\1065785:\DLEr\1198N[9T#p\1074919\1000932=Ltw\170949\fQ\65194,&JG8I#!8\16806'Y0Q\1051894^\"'u2\DC4\EM_\n\48528\USD\ESC\FS9 QF\EMbh;O|\1049800'2\1091629\1091090#D?\SYNK\1112422" }, - newProviderEmail = Email {emailLocal = "\993799'g\n", emailDomain = "\1046285Z\1045773\GS"}, + newProviderEmail = unsafeEmailAddress "some" "example", newProviderUrl = coerce URI @@ -183,7 +183,7 @@ testObject_NewProvider_provider_5 :: NewProvider testObject_NewProvider_provider_5 = NewProvider { newProviderName = Name {fromName = "\169186\DC24)\1066115\ENQv7\r\ETXp\DLEeI\62252\184208\&4SD^\68867;9*"}, - newProviderEmail = Email {emailLocal = "\1086513", emailDomain = ""}, + newProviderEmail = unsafeEmailAddress "some" "example", newProviderUrl = coerce URI @@ -218,7 +218,7 @@ testObject_NewProvider_provider_6 = { fromName = "\nLY-\1041348\1053736`\r\STX\36143`\rBa\DC2jv*m\n\DC2\SUBpp\DLE\EOT+pq=P\1006208\185123c~\42806Qp5\38725\&5\1051474X \993515*%\1058138&QNA\SO6\17381e\"K\20379B\ETX=\1095143\179544\&2\59383a\1060828\1036355\1047984\ACK\159657.\SOH\fP\194727f\ETB\134004\&2h\1111875t\73697\1109301i\984751x]cx\nC\53880\ETB9;\65505${\1002225v\1057050#SNKZ@\95712-\DC3E\ACK{\GS" }, - newProviderEmail = Email {emailLocal = "\1111012\149049\1059090\137222\NAK", emailDomain = "\ETX\1058787\ENQ-H"}, + newProviderEmail = unsafeEmailAddress "some" "example", newProviderUrl = coerce URI @@ -245,7 +245,7 @@ testObject_NewProvider_provider_7 :: NewProvider testObject_NewProvider_provider_7 = NewProvider { newProviderName = Name {fromName = "^\1061540\ENQMSO\1061583\&0\STX"}, - newProviderEmail = Email {emailLocal = "e\DC1\8331c[i", emailDomain = "2CTG\US\r"}, + newProviderEmail = unsafeEmailAddress "some" "example", newProviderUrl = coerce URI @@ -280,7 +280,7 @@ testObject_NewProvider_provider_8 = { fromName = "\14918\RSh\1071695;Io2:\NAK^e3\NAK\152781Vq=\EM\1090805\ETX\1076024\1111879\DC1h5Zqc\1082985vz\bV\DLE\SYNZh-\vZ4\1008751\&5/KI\1017195\14775\NAK\DC39\24480:q5Oe\1084323\&9\v:xx'U\1106866\SUBt|\DC1O-}\SOH\DC4Zz\1021791z[,F{\1074628\v\2561\SUB4\DLE0-\r\b\USw\SI\DC3a\1074460mv\DC1;\39901W&e|\1009415\1081200;\DELo\128316\140382\SOHds\30510\\\1044298\SI.\54580J.1" }, - newProviderEmail = Email {emailLocal = "\SUB#\EMdlb", emailDomain = "\ETB\985485\NAK^"}, + newProviderEmail = unsafeEmailAddress "some" "example", newProviderUrl = coerce URI @@ -315,7 +315,7 @@ testObject_NewProvider_provider_9 = { fromName = "h{k\7076\162133\99501.t\1011685\DC1K\SYN>O\SI\SOH\49104pJ\SOH\nh(\1058570E\29145\&9\18179\NAK\FS\1819\vD\995637\SOg6F\1054281qUD\53666\1028850hn]f\53775$kZg\984559\t~;\1017416\1007006k\989140\&5\161421r\RS\ACK[\RS\1018346\183709\38751\a33\147504\&6\1069673$#{\GS\DLE\EOTX\FS=N\EM\180147\&6<0%\DC2cdm\174481b\14332\ENQ=9F2:\182353~\EM\ETB\47916k!Q\153214\1110656[\NULeld" }, - newProviderEmail = Email {emailLocal = "8-\SYN\\", emailDomain = "\EOT\50704~#"}, + newProviderEmail = unsafeEmailAddress "some" "example", newProviderUrl = coerce URI @@ -346,7 +346,7 @@ testObject_NewProvider_provider_10 = { fromName = "?\990628\58645\50907Ykp*V~z8xa\ETX`6^\SOT\1104197\US\1104107\175563z\995556P\an/\1021466{/O\40639f\993031-OG{\DC3\20273\1025488\DLEz\DLE\US\1043327c\RS\147510j\120360A\1109443\1059885\&2Wc\DEL\120607\1018480P\50798m{\FS'9Hi\1063626hMB\1075646\1050859Nl&\46118\&8}\141343\9870'\ETXg\ENQ\1034698\1006401%Ps)C\178746\US\"G.\1034816I " }, - newProviderEmail = Email {emailLocal = "_e_v\SUBL", emailDomain = "\v4"}, + newProviderEmail = unsafeEmailAddress "some" "example", newProviderUrl = coerce URI @@ -381,7 +381,7 @@ testObject_NewProvider_provider_11 = { fromName = "\983133\999447-\a%~\19798\DC4v4@yim\12033\US37\17431\STX\133604#@\1103392a}mE\DC3\172859Es3C\40960TZaTUy\182080\983082u\SO\159200n\EOT[\ETX\164033\51273_\SOHd\SUB \67980\\#\160768{@\DC1%8s\19295\992711,!D<\92708KM\DC15T\n\CAN@sl\NUL*\a\SYNxZ\ACK\6422 ei:V+y\NAK\SOA\17849dX\991225\991020\r\1057765\EOT\1102945" }, - newProviderEmail = Email {emailLocal = "\a*\RS_", emailDomain = "6\1030075\a\f#9"}, + newProviderEmail = unsafeEmailAddress "some" "example", newProviderUrl = coerce URI @@ -408,7 +408,7 @@ testObject_NewProvider_provider_12 :: NewProvider testObject_NewProvider_provider_12 = NewProvider { newProviderName = Name {fromName = "E\DEL\DC3MNO\33205\EOTZ\1042578\NULI\9109\&5e3"}, - newProviderEmail = Email {emailLocal = "", emailDomain = "d*\1049690jA}"}, + newProviderEmail = unsafeEmailAddress "some" "example", newProviderUrl = coerce URI @@ -439,7 +439,7 @@ testObject_NewProvider_provider_13 = { fromName = "\146808\25949\&7\DC2\1076199Y<\1069016+Babu\r\189492\132668\EOTGo\163930:\1052561z\1061137\167675\6623\1091167tIid\156982CR+\1076183\1035056\DC3h+Wju\SI'\ESCc|\1043334?y4L\147691Z\FS\nQ*\r\24905\ACKan\1096641Sw\53656\996906\SI\SOHZ\17777I\26208Nm\SI>\SO\ENQ\b\SYN\STX\1104514\173928\RS,@+Fm,;(cl" }, - newProviderEmail = Email {emailLocal = "B\RSvq\1063673\&7", emailDomain = "\44409"}, + newProviderEmail = unsafeEmailAddress "some" "example", newProviderUrl = coerce URI @@ -474,7 +474,7 @@ testObject_NewProvider_provider_14 = { fromName = "\a3\1030411\1108909\145052W\DLE\ETXf\SUBW\1041360\DC26\1032094\174641^5]b\SUBS\986449\f\1100960|=\1079519\fYu\"\f\44824V\11068\SUB4S\ETB\983122\1069918\&9DD\DLE\1076534\1055217.M0p)\1036040>\140974.\ACK$z,\127809s\1044091P\1053904 +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Wire.API.Golden.Generated.NewTeam_team where + +import Data.Id (Id (Id)) +import Data.Range (unsafeRange) +import Data.UUID qualified as UUID (fromString) +import Imports (Maybe (Just, Nothing), fromJust) +import Wire.API.Asset (AssetKey (..), AssetRetention (..)) +import Wire.API.Team + +testObject_NewTeam_team_1 :: NewTeam +testObject_NewTeam_team_1 = + NewTeam + { newTeamName = + unsafeRange + "UivH&\54922\98185p\USz\11724\r$\DC4j9P\r\"\1070851\3254\986624aF>E\1078807\139041B\EM&\1088459\DC4\174923+'\1103890R;!\GS\1017122\SIvv|\rmbGHz\1005234\95057\&3h\120904\\U|'\ETX;^&G\CAN\f\41076\&42\teq\1049559\SOV1}\RSaT\1014212aO7<;o\179606\f\1111896m)$PC\ESC7;f{\STXt\9533>\EOTX@4|/\tH\ENQ/D\144082\EM\121436C\99696Q\ENQT\1096609?d\ACK\1073806#H\127523\139127*\166004jo4wa\95243leQ*\1000542\1034344>@,\1045947\190894RF4QcNY96\168531\1051528G\1069460&J\\TzHUiG.C\SUB&\FSx\52616\167921\&3\1105098A\1054008B)\29142\31346r\1004296\ENQ&VCPa{\SOH\EMW\DEL\43500\97305\DLE/\1078579\SIc:b\SOH\132266)\35144\1100498\37490@5\983688I02g%%1bJl} :\1021555\SYN\64090\158870\143049" + ) + } + +testObject_NewTeam_team_3 :: NewTeam +testObject_NewTeam_team_3 = + NewTeam + { newTeamName = + unsafeRange + "\SUB_F\n\65091\140672\DC2>\1079041\74636t\n)1/% hL\DC2Ad\SOHXq6\DC1)\NUL\f6\fV\DC4r\1097128\DC1n\1107359,@\171217\118996\n\SUB%N\176824\ACK\33856Xv)\SYNz?\DC4\EMY\162050\&2\95792um8}\51420\DC2yW\NULHQ\ENQD[Fe\nk\999106\EM\25079Yk@##u}j\169850\153342\STXq\ESCir7) \27756%\1016104~\993971\&8\1085984je\1099724\&0*Gi3\120829je\CANQr>\1033571k1\63774c\1031586L\1015084\93833t\EOTW\999363\SUBo\fgh\ACK\172057C2\38697c\SUB)uW\r\fB\1042942Sf\SUB\SOH*5l\38586\SI\25991\EMB(\ENQ\133758/)!{\1006380\&9\STXA\DEL\16077fx&\180089T&\187029\DC4\52222[\r\v\n\1071241j2\166180/\1086576\ENQQo\fj\134496\129296\nb6\CAN3\RS9\EM\1000086ub\ETB3CY\GSsIz", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "3d4b563b-016c-49da-bad2-876ad0a5ecd2"))) AssetExpiring), + newTeamIconKey = + Just + ( unsafeRange + "\FS\RSP\988567Gt\SYN-\47148nJ\1010840g^\n\r\177791\GSR\1010061q\988754\nQ\RS\1054014\GS#w\147936\171735\1064959\136621B\DC4\SUBLv\"S>\121093!]sB+6\DC1oc\ETB7\34513lR\95866\EMr%E\1077999B\98708A\1067109N\ETB?{\1065508/|cU\60733\141259]\92896\1102284\DLE\147332\1075446+\991438\t$F\96714he4\166964|k/!5Z~\83246\ETB\1017589\SOH\ENQ\1056989\&3E!{^\33558\&4fh\1029576N\1111705v\f\GS\998029mde!5\1027807y&\1062155xo,\STXrk\1071672\ENQ\SOHJoS\986695X\18929\994879a\991047\RS\1046020\EM\SOH3j\3901Z4\DC4\1068579l\52972n\ESC@ve#\SYN\GS\183587P4\1077298\ESC\170211:\157706z1*\USs\vd`\1059621/\39172\165682" + ) + } + +testObject_NewTeam_team_6 :: NewTeam +testObject_NewTeam_team_6 = + NewTeam + { newTeamName = + unsafeRange + "v\188076hEWefuu\1006804jPx\158137k#\SOH\986725\STX\ETX^\ESC\n\CAN\8325p1D|S1\1064991\1102106\29079\SYN`\t0g\1034469,t\FSw\fDT\RS#H\SOH\145176\US{\1091499\1025650\984364lW\a,uil\SIN`5e:\SYN Y!\SYN\1025115tb\1085213", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "d7a467c6-8cd4-40cb-9e30-99b64bb11307"))) AssetEternal), + newTeamIconKey = + Just + ( unsafeRange + "+&heN\1091941K\f_k\DLE(\33970\DC3\9833M\f\1029853\1098178\SI^s\1101855Ga,$\38078\SIb\DC3\f\"s{\ACK5\1025293\5649\US\DLE\SUB\1085641\70123\CAN,\1036517\158007\DC4 \1109215P\95245|f.>hEa\DLE^\ENQ\b]`\1112948<\GSZG\1004098\SOH\190360\24273*8p\FSF@OLpnXTmW\96553f\68110\1076109\25954Ze1 \SYNEm\27765f\ACK\987143" + ) + } + +testObject_NewTeam_team_7 :: NewTeam +testObject_NewTeam_team_7 = + NewTeam + { newTeamName = + unsafeRange + "\145552\1042892iz\1057971FT\14964;\1108369}\188917\1113471\&9\SO\991633\&7>hAC\NULH2O\177259m\187711\&2R(?W,=,\990725M\992456\aM\194790\SUB\47600q\SOlj\EOTj^.s~\rY%5lM,\26492=\ACK\1016899\188843>{\CAN\DLE\15878f=X9\SYN9\51145\159419TI4\17599\v\NAK6\1014936/\DLE\NAK\ACK\23564H<\ENQ\1029703e\ENQz\1017528:\6137\"rS\a\167660\FS\ETX\1059289\1031786\49012\DC4\DC4Q\"\1065200\&1:\1097556\UST.;\1042663\18380}", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "b199431c-e2ee-48c6-8f1b-56726626b493"))) AssetEternal), + newTeamIconKey = + Just + ( unsafeRange + "D\RS\168552\SOH\1033444\128689Ll\GS\tW\1056953o\CAN\47716b\ETX|\US*=\1011088\1066392\988391\&6\999812" + ) + } + +testObject_NewTeam_team_8 :: NewTeam +testObject_NewTeam_team_8 = + NewTeam + { newTeamName = + unsafeRange + "YwD\1023517r\NAK}\1083947\ACK\1047823\29742\EOT\1071030iI5g\1012255\t\"r\150087O\DC4?\53005\1100290\1108960\NUL\1060304qgg\DC1X)\NULL\1054528\CAN{\v4\NUL\93999\bvD#\1035811$aYFk\b\1102040\1089491\1042733\47133:1\179810S7\66745V)\1072087\v\96989\&3#\b\1104899c\27119Q/jPy\1015620P@Df\997914\51756H\1113361Xr\SO\ETB3%\1108760aF@3A\SI\ETB\STX mj9T=\DC3'XI\DC2?0\1093231\156858VHp?\1066163YU\42092\33083\72810,)\1113424\ETX96\153338z\42445/4T\136162\ESC\60427\1086321&\ETBS\1098748\14578z[\54638Z\DC2\"e\SUB\173931&rQ\fJG\100066\180037\155435s$\SUB$\50544S\162554E\ETX*\t+\63443WU*\144654\1042128\&8\NAK\999184a\t\EM\1097907_\DELOD\1006385/\23998\1100140SmfX", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = + Just + ( unsafeRange + "v\70188\46459h\SOH_\991979\DC3\ACKi\1000164\DC1\ETXW\72785\35679\DC2\23266\1026390\EOT\f%_\1064553\GS\SYN\ETB N\NULF\1005467\ENQLUua3\1089232M\8605\"\94879\SOH\RS\n-='\DC1B#\FS\136881>\DC3\132340\SI\GS\1088106G7v6w Z\4678\1051054\182628\170805\ESCP>\131111\1051383\1076729\v}?\5316Jg\SOH\SUB^pl\1101671\&2.\SOV\57380\DC3\22371\64509\ENQB\1045499\1076733\139492<\f\DEL2\19252Tz@6\DC3\71851x?\150161\36913\b\DLE\CANp\1081584\SYN\ETXN\1099776C\SI\SUB\DC1l]R\NULvL\1027446Nz\f-bf}f>\STXH\EM\136484+Zo\1034706\1062880\NAK}\adb\171356-\\-1\DC42\1046344\DC2\78894\&1/\33084b:\ENQ\1038950;Mw\FS\183866\1113547ITuy\1050264`SP\SOH\SO\GS\NAK\a\r7M\1069326\1064150\18615\n\SYN3V\ETXR\n1$e.\1096261B~yd_z\1047817\rV\1091351\RS\SYN\165050l\DC3\47200u\1058674u\"\aTc|sEw\1011190wTC|F\4735B\t\DC4&\bUEN(+M\SOF;\1099746\134573\EM20\nrPW\1017058$\1064809", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = + Just + ( unsafeRange + "X\1019453;\ENQW\ACKLk\996110\144662\ETB\n]\58553[~\10280&U\20125v`I\ETB\USl\983659\t\1090302?\17227KM3c\1067581\1030643= \ETBt5vKOg\NAK/NC2~i'\1062772Ojb\b\ETX\62742\1090035\DC1\SOH\NULFWc\1014613sU>P\SOH~\EMwUHU\SO#\55006\1081711!Nwn\1005601e\SOH\SUB\f\ETX\ETBT\DELl\110629BYU;a\1012448K7?,m\154276Xpa\48825\138301\EM ,M!~^g6}(\60133\36369\RS\8075gX}\161019)c\n\SOH2E" + ) + } + +testObject_NewTeam_team_10 :: NewTeam +testObject_NewTeam_team_10 = + NewTeam + { newTeamName = unsafeRange "\b \SOH+\1056054;\t095\42390\n\STX2J\1002251\DC1UzD_\1110746\FS", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = + Just + ( unsafeRange + "\EOT\131569\ETB:\984737HL\SOH^bs\vG\157476{I\1096053]-J\FS\1107927\vs9\DLE\1000765vI`N\48159MZz" + ) + } + +testObject_NewTeam_team_11 :: NewTeam +testObject_NewTeam_team_11 = + NewTeam + { newTeamName = + unsafeRange + "\48005H\1082536\132304\157763\&5\RS\986337-\NAK\ESCR\nL\63954&bD\139428\SUBH\US\1040918\f\t;e\1064224\47101\tc\1087740e\1099415\DLE\ETX\DELI\65746\ETB\133884\SUB \SI\43795~FE\CAN6\162836\DEL\46062u\"\135684\1041611\FSFYI\t/{\ENQ\RS]j\1076782\US22\15884l\42366$\ETB\US\180023kL{\STX*\131382RMj\ESC\1091332W3H\1020399\FS\NAK^\"5\29653\32539*\1099111", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = + Just + ( unsafeRange + "\1109507I\ACK.\158786@y0\DLE\1083101n\\#skj\1019405Y_\1037580&x\1007219\GS\SIy\1104457B\SYN0\DC3VP1\1086698q\1024822\1081753\28211R\1100307*+\RS,MP\27076*;\n\NAK\47211\t\160463\nGj.\41290\1104539l\12622\FS\61112~\1076042\NUL.\1083842&\SOH}\SI\1080986\DC1+f^ZC\a'T\SOH\n\1020923\1097319U\1107987`W\r\\fX\n\1095366TF\1108756`h\97424[\46315ERdP5<<\1024109;\r\1095899\NULDy\28422\&5N/^\136134(\DC3\1045067\1061604\&6e\f:\SIB\DLEF-\1110200\17393\1064949Rfb\44582\aDrB\987948\13740\26738\NUL+\60859\&2.\a\a}\NAKpsFw\ETB\DC3 \186007\151693k~" + ) + } + +testObject_NewTeam_team_12 :: NewTeam +testObject_NewTeam_team_12 = + NewTeam + { newTeamName = + unsafeRange + ";\110872M\EOT\164161P]'\1041089\1094514\4118\1054714iFnRQV\43238@\992926\59902l\1099067\aKZ{\51124S\190890\fg*\n,`!V\STX\991695e'\1039967\SO0\37019p4d\STXs\1020471uK(c'\52929hjB\144953\SOt'h^\SYN\SYN0\1009487_\12064\166805thH\SI\1073479:\1019934l; n4c\1101781D[\1014388\&8Y+\1092407\EOTE\1058506\\0\168273KKTc)P1K\1042475\990753W\ETX<|\24888\&0|5{Y\986771M\DC4\vK\DLE\1089150\SOH\DC4\1013653.\ETBg\991717\DLE\"W\NUL9&0yYZ\1094524\v\11606\58174", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = + Just + ( unsafeRange + "\"C\ESC\SI0\ETB\69608p\12616|/O]\53852\SO \55172C\SYNN\SUB8\NUL\62584BxtH\SO*\1077819\&3.\1061851(\1100810w\GS\152525R{q\990825\&4\180037\150457:\187092\134288>\ETB\nl\1061158g\"\996841,6K\28384\1054272[\1019005\1016209N\24221eB!\188918C\EOT\STXX#El\ETB`\61337e \1096702\ACK\ETXPB\DELC\1111118fa\178975" + ) + } + +testObject_NewTeam_team_13 :: NewTeam +testObject_NewTeam_team_13 = + NewTeam + { newTeamName = + unsafeRange + "G\DEL\51831\70681rLb<\1056047!\RS|RD\161793\ACK\82958\164863\45602Ag\22680 \vy`\v\1045283K\13763e\18467,\144933DQEO\RS|\SI\1076051\1063435gr\1113276\NUL\n*1\47081R\SO\66829-Y\1037937n\1085668]])\1086075C\DC3\146455\"M@(K\15234\RS1\35575\FS\SUB\1025798T?}\SO=*\184770\n\69897\v_\"7\1064561?Lk\150200x\DC4bu:\146992\14577\1036009<\1015572\&6\SO`\1071314U\51409yp\183322\&7%", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = + Just + ( unsafeRange + "o\64661\1052808\SI[aoM\GS\1110611}q\36535\&4^\ETB-*%\148361\&8\1067531`\1070936#pH}\DC3?w`A/\94009\1108569\995072 \1104313\nX\40987\997490\DC3u\RS\SOH(\1041586\1006481\&6\STX]t{\DC4\";*\r\12492q\1066003\12213\63338+w&\31533(3#\180761PY]\RSf\\?F4\SUB\UST\1108579Rnfq%\66873p\154120\182326j\127981\&0P\bn\SO\FS\t\19400\nN.aGx" + ) + } + +testObject_NewTeam_team_14 :: NewTeam +testObject_NewTeam_team_14 = + NewTeam + { newTeamName = + unsafeRange + "2#\DC2N\b9&A\1030886ZL{f\1011542M\1101172\23517\a\DELv\164961\32470\ACKT7\DC3\DC4\1009557O\1103393C\152202\t\DC4l\RS\SOH]\ESC\ACK\95718X;\149660* &\97401}\1111236T\ESCCLkx,\DLE\63803\nbT\1049269fWJ\992800\136973a\US`\DC3\139728\28948\&8r2']\NAK\DC2\133094\nl\DC2NXB\ENQia\1068046]B\989632\DLE\ENQdf#\64677\t6g\FS\SOH\1029760Fp(\GSQTZ\1015396\8630\153801dUJt\SI\EM\194705`\\#g0Qed@a${=Q.\1048388Ld`\35027 \173216sV\SUB\SO5\150360\41997\1107813i\EM\DC3\988956\1049486\SOH\1030355>\1044179\DC3w\1001979Y}\21603\&1q\NAKY:\25626q \ETB=*#\74975\EM\61277\\\21887y9Tfc\DC1\49327k\1096646\\Oxxn&6NtaZ?k:5G@\46350\DC3H\1097149hu4\178807\995883\USR\161801\1024517v\26381\23905\72161\12881\ACKD\985152[bb<\1111873", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = Nothing + } + +testObject_NewTeam_team_15 :: NewTeam +testObject_NewTeam_team_15 = + NewTeam + { newTeamName = + unsafeRange + ":\44335R_.\4189\v;\t\1039296-\5484PN\r[\32934\SUBY\1102645<\60542\1083602\aW\1099269@\183771\162143\172579\biU\1005268b\DLE=\t8+\993285\1090143\1018670\1107684>\ACK1\bZQ7fmQOQ\986711l!\DC3\44018\27476*\43689*1\f\1097293\&8nk|\NAK\1005998~\fO\162989\100863!:3\ETXn{%\6663\182700if/!\29917] <\1056176Y\1078680\b\DC4~\t\EM\SOH<*\NAK\143397bx4 {\96203\CANVs;g\98929\144388\STXqkI!QJ\1072302J\189512\DC4\64545?_\STX\t\1082190iB3YdKA7@>Q\995699\987049]\1094644\133325>D\1026819wD\ESC|\SI'^\136789\120874Q#q,\"", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = + Just + ( unsafeRange + "\SOH]rj\1053405eA\1046358\tbj\EMk\DC1l\n\988481H~]u\42907\1029099!kjVS{42\NULE?\EMh\61474\35112B!:\DLEX\DC1T\DEL3W\avimhK\1078443\DC1to*P*\DC1}\986362\1081249H\r\1034017B", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = Nothing + } + +testObject_NewTeam_team_17 :: NewTeam +testObject_NewTeam_team_17 = + NewTeam + { newTeamName = + unsafeRange + "|\36324P\US\1040589\159812Y\SOHj\RSYrr\49743\&0m\ENQ\1027954*'\72098\1105368P6\SYN\15236\f\DC2\125109e\1031690\RS\1026891\1003083\69946\rA'\GSA\NAK\53778\1067566J\1016490'T\1037603R2? \FS\US\1032454$\NAKGr(\1008673{\ENQ\62451\&0mJ\SID\STX-\CAN_I\132366\f\147665\FSR\1080205hp\143954B6W2\b\f6\1104867\DC2\180998\b1'7-T-#\3953D\1076345\1082129T]v$Gl\1042148\1032818\&5yg\1025280\nQc.`i\14819\24538}\FS&k4\99627\ACK>#\32013\1036954\EM\131987[vBOPu\1108963@\ACK\NUL\1087882\147841\SO\NAK\98755\31702\EOT\ETX&\1032348?z\989374i\fz\n\1029119\ETB3\a\1108955W\1113557E^\1043345\986117S3'4\ACK\74144*m-\ESC4\USj\ETX__6\1046371\6580M\48069\ESC]\EOTDq\DLEuo\28030$\vUWp1=/o\ETBY\173686\&9\DC2\nQ\177317\1051037)\1102455\1010761\NAKaR\145135;\52151\SOH\EM\na\nvt\133143\ETXa\140630 J\134658uX\1077113?Wz&<\DC4C\fx`\1038161#\SI\194737\37045\43620\RS\STX#\SYN\DC4-Oj\EOTd\1037772'FoHqexoh\SUBx\1106683\184912\bi\998453yr\SI\1064751w\1104226\n8T\1008339\&2'\1024124\1110758\1103037\RSnxW[\26817\993050\96723\153423i\13589\&4\1008403YHZ\48771VZ\DLE^0\STXC\1057595\1037144" + ) + } + +testObject_NewTeam_team_20 :: NewTeam +testObject_NewTeam_team_20 = + NewTeam + { newTeamName = + unsafeRange + "\SOHW+\a#\151172iN6\GS/#mrj4'\rTV]\ETXg>\"br\SOH\NUL\158808+\47718c^\1003405<`\1111751\149060\STX\986585\ETX\162139D\ENQ\30356nqp\1095539\988368c\RSt\1081319G", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = Nothing + } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUserPublic_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUserPublic_user.hs index e51c5ce8aff..395d7f4c898 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUserPublic_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUserPublic_user.hs @@ -38,8 +38,7 @@ testObject_NewUserPublic_user_1 = { newUserDisplayName = Name {fromName = "\\sY4]u\1033976\DLE\1027259\FS\ETX \US\ETB\1066640dw;}\1073386@\184511\r8"}, newUserUUID = Nothing, - newUserIdentity = Just (EmailIdentity (Email {emailLocal = "test", emailDomain = "example.com"})), - newUserPhone = Nothing, + newUserIdentity = Just (EmailIdentity (unsafeEmailAddress "some" "example")), newUserPict = Nothing, newUserAssets = [ ImageAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "5cd81cc4-c643-4e9c-849c-c596a88c27fd"))) AssetExpiring) (Just AssetComplete), @@ -53,7 +52,6 @@ testObject_NewUserPublic_user_1 = { fromActivationCode = fromRight undefined (validate "cfTQLlhl6H6sYloQXsghILggxWoGhM2WGbxjzm0=") } ), - newUserPhoneCode = Nothing, newUserOrigin = Just ( NewUserOriginTeamUser diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs index 5d0a458757c..973d0055265 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs @@ -37,7 +37,7 @@ import Data.Text.Ascii (AsciiChars (validate)) import Data.UUID qualified as UUID (fromString) import Imports (Maybe (Just, Nothing), fromJust, fromRight, undefined, (.)) import Wire.API.Asset -import Wire.API.Team (BindingNewTeam (..), Icon (..), NewTeam (..)) +import Wire.API.Team import Wire.API.User import Wire.API.User.Activation (ActivationCode (ActivationCode, fromActivationCode)) import Wire.API.User.Auth (CookieLabel (CookieLabel, cookieLabelText)) @@ -51,8 +51,7 @@ testObject_NewUser_user_1 = "V~\14040\38047\NULw\1105603\1077601\&1\73084\1020199%\14699]y*\121297jqM\SYN\74260/\1108497-*\US \RSA\SO}\64347c\60361v [\1022394t\1012213R\181051Y\1036488\&6tg\SYN\1044855+\DLE\99976;\ACKOj\DC3\48593&aD:\nf\1002443!*\DEL" }, newUserUUID = (Just . toUUID) (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))), - newUserIdentity = Just (EmailIdentity (Email {emailLocal = "S\ENQX\1076723$\STX\"\1110507e\1015716\24831\1031964L\ETB", emailDomain = "P.b"})), - newUserPhone = Nothing, + newUserIdentity = Just (EmailIdentity (unsafeEmailAddress "some" "example")), newUserPict = Just (Pict {fromPict = []}), newUserAssets = [ ImageAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "5cd81cc4-c643-4e9c-849c-c596a88c27fd"))) AssetExpiring) (Just AssetPreview), @@ -61,7 +60,6 @@ testObject_NewUser_user_1 = ], newUserAccentId = Just (ColourId {fromColourId = -7404}), newUserEmailCode = Just (ActivationCode {fromActivationCode = fromRight undefined (validate "1YgaHo0=")}), - newUserPhoneCode = Nothing, newUserOrigin = Just ( NewUserOriginInvitationCode @@ -132,27 +130,24 @@ testObject_NewUser_user_7 = (Name {fromName = "test name"}) ) { newUserOrigin = Just (NewUserOriginTeamUser (NewTeamCreator user)), - newUserIdentity = Just (EmailIdentity (Email "12345678" "example.com")), + newUserIdentity = Just (EmailIdentity (unsafeEmailAddress "some" "example")), newUserPassword = Just (plainTextPassword8Unsafe "12345678") } where user = BindingNewTeamUser { bnuTeam = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "\fe\ENQ\1011760zm", - _newTeamIcon = DefaultIcon, - _newTeamIconKey = - Just - ( unsafeRange - "\ACKc\151665L ," - ), - _newTeamMembers = Nothing - } - ), + NewTeam + { newTeamName = + unsafeRange + "\fe\ENQ\1011760zm", + newTeamIcon = DefaultIcon, + newTeamIconKey = + Just + ( unsafeRange + "\ACKc\151665L ," + ) + }, bnuCurrency = Just XUA } @@ -165,23 +160,8 @@ testObject_NewUser_user_8 = newUserIdentity = Just ( EmailIdentity - ( Email - { emailLocal = "S\ENQX\1076723$\STX\"\1110507e\1015716\24831\1031964L\ETB", - emailDomain = "P.b" - } + ( unsafeEmailAddress "some" "example" ) ), newUserPassword = Just (plainTextPassword8Unsafe "12345678") } - -testObject_NewUser_user_9 :: NewUser -testObject_NewUser_user_9 = - testObject_NewUser_user_1 - { newUserPhoneCode = - Just - ( ActivationCode - { fromActivationCode = - fromRight undefined (validate "z1OeJQ==") - } - ) - } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/PasswordChange_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/PasswordChange_user.hs index f376b73704f..2fcca87a73e 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/PasswordChange_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/PasswordChange_user.hs @@ -24,8 +24,8 @@ import Wire.API.User (PasswordChange (..)) testObject_PasswordChange_user_1 :: PasswordChange testObject_PasswordChange_user_1 = PasswordChange - { cpOldPassword = Nothing, - cpNewPassword = + { oldPassword = Nothing, + newPassword = plainTextPassword8Unsafe "\SOHf;0B+CKY\1040633W\ENQ\178683\66681\1079258\1036336f\SOH&\166643\1050584\1022602\1091853\145882\tX\190821\FShl\1020866^A\153646=\151238\EM_ow?\\h4\155435\68388\SYN\11851\&4O\NAKnJT\EOTr\CAN~\ACK\ETBR'\992244sX\1001766mlCHvg\1112425ba\1003664R\\\1034092o\989312\1056334*k\r)H\180403\1051096\n#\14366~\\9Q|\v;\USd.\1066580\&0SHP%\1019462\22215'!\1044148N\SUB!Kz@\NUL\74079\1087771\SUBVp\1100111\38836\STX3#\DEL\DC4}}\1094237N\120442`\169346\&7\1036101\DLE\154725^\STX{`i:\rUT!\DC3\1111700\152543\NAKWK\NUL\1098445\1102182eA\140938\ETX\172001\1034473t@?\1014650\SOHJ\1074486\&7\RSg{\78258\&5R_\DC3u\SI\153435\1082441`}\DEL\66836X\DC1\175200D\25079b\176836\&6T\141840\167124p*7\n\\'\vO#\FS\174827(H\NAKn\178850\1015713}2s\143401\&8GA&\1004513\CAN\1068132d\9056\SUB\1059104t @\1056816I/\175842\30192\DC35\28889c\EOT\1046281\22594Uk\SYN\DLE\1099103\&8\GS\1034138\94316R-x\999901\1007697\1008634\DLEO,Z\ETX\1073959\63275f*\f^>\EOTD\r\SI_AQPO33\96451/F\RS\185177y\77854|Fn\1010492E<\1047147\&9\ETX[y`e\168776\65402L\SUB@4i/*\1011887\1102541\9070Ih\SIC1\1031432\t%?kFt\ACK\DLE\US\GSN\171039\f\1094027:\aV\ETXj\18014\SYN\SYN\150071\EMK\1083674\162115\40502Uez)\1080936\FS)8vT;\GS\21613ay\ETX\SI\GS{C=\EM,\SOH\ETXO\162859\ETX&\SOH2%<2s\f\SYN\r6ivo{\1028087WN\1053937R\1039894\1030129\995717\98891[ :\USu\180666^f\1087790\CAN\137895\183333N\SI\145270\EM@pK\1078668\&9\r@Ze\152611f\DC2x\59319M\30205;j\SYN\29669K_~:v&Dpx~_\STX:b;bv\DC3=\14812\&6\SUB\41242\ESCy0Ho.B\"u*{\1018548Vw\SUBW\138263\173995rbY\51982I{q\1041374\ENQ&_Pt\182926'\917559\&5\v\150891\35898\35323Ue@YM\164633)\n\EM\GSn\EOTZ\SO\DEL\\\f`f3T+_\RS@\a\RS\186662}" } @@ -33,12 +33,12 @@ testObject_PasswordChange_user_1 = testObject_PasswordChange_user_2 :: PasswordChange testObject_PasswordChange_user_2 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "7\16927K>\97741\186669m\vG9\tO]kp\63012\SUBVQs\t\984613\1108746\ENQ\1021022!O\998098\EOT=\abrgK_D\1033730<\SYN_\1100470\1086629\ETXH\SO#w9" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "\r]Gy3T\1026217\EM\1020078|R\\@\1056800Xq\155479t\EM\NAK\45450\1031406GU+p\1028583\1037856G46\1111047\a\145730u\EM\SI)@\2452\nk7\989251\22005D\11178\1075520\1105369\&7,h\154963\r\1014527\&3\a\13276ki\SIuUB3=X$\138590]\1046903\bSaAr8*t\DLEX:\1023144KA\SYNu$^rK~`\1062546)\174565MJ\1062282\1020633\SOk\SI\EOTF]\DC2\997860\b\CAN\f=p\1041758&S`\b^\179839;S\\\DC4N:\SO\f\NUL\1076187\&5f\127761~K/\ESC\137715*:\1033030\ENQB5\158024\NUL~m\DLE2\12820\1079647\NUL%\DC1{H" } @@ -46,12 +46,12 @@ testObject_PasswordChange_user_2 = testObject_PasswordChange_user_3 :: PasswordChange testObject_PasswordChange_user_3 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "\DC1\1008131\"iQg2?.V\nR\ad#NZu\SI\154091\"B\USm\1066170\DC4?_-%\SO\DEL,\b\SYN\78542\1070480%U\RS\95262%1\2330\STX#\SUBV-\163363J\142686O\ETXV\DC2Ga\DC3,\1094317O3J\1098970n0\1052934j|\23339cF?\1019037x-\1069855\1094636\&160jp^\179153\FS>\\&\ENQ\EOTg\62450]\1073387\RS\169810\US%\990256\1042714$\985984R\1044140'-^I\1083467 kT\bZ\999047F\t\1084750F8R7\SIYN\EOT:N\SYN\SI\vd\57930Uo\1017473\1052974\vi/KA/\1004923\1051639\DC1e-\47612E\SOH\SO\v\SUB\1057038c\1090019\1003618Z#\991058e'\RS\120431\"\CAN\EOT\SYN?wO\1084580\DELI\2368\1005674\1041651gYJ\147444\&9p\CAN\187441Rn1\187124A\GS1x)\146547k\23622\DC1%S\1016329C>\134586\19597G6\1003504\RS\97878~\996492avKH\GS<\1082858sNVe\7956\152082\DLE\188847\f\ETBmpc'Xi&\150774E|V\1073099}\"\NAK5\96146&\t\f\DC4l|p\a\1024356\1036737UOM%a/9\r\n\1095590\1055708P*K\1073690%\NAKyXE\165112\987387L\DLE$f\ETX\DC1L\\l\11245\49768\\Q\SI\1002707()\58946w.\172820\SI.&\31267nk\vF\143976:\1038638\2606\1016120\SO\RS^\ENQ\DC4\b\1035479\1045289\RS\EOT\EME\1072274I\"W\1104244l\ESC\131418fB\23703+R\1113063\59494?\1061998^TD\46012{k\181947n\60196[|g;\71853\1095649\18432\173156\16164` \9356\1082477\174851dT\1015692p_\13046tN._\1042851\\\52588T%\98330\DLEC\96142\1019008\9148D\NAKrPk\170211\SOwLe\1032698\20202\1022050Jj" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "\EM\GSjg\rcpq\ENQydjzBvM\EM-\181560x\62219qO[p\RSF\1043280\&2\DC4\160364\1019066\EM'L+z\SYN-K\94385\&0\ENQ\US\ETXP\DLE\ESC&h8\141548\1084128c\143493\1009984 \a~=|hx\1031253)V\152928J\991022!@;*_\US]\ESCd\FSI_\nK[\DC1\b/dS\1020193v1(h\ETX\152908-UL(U\nm\1062628\\\1049985\t>\FS\EM\190594~*$\1056230\31211\148228\991805$ch\ACKyCFOIo\DLEvHeF%\168128\&8w3I-\77839(\177181\161298r.\998529s>\155909@\ACKb\EOTa\DLEf\68669_;[-.\1058443q\GS9\SI\145931U\1085428\CAN\ETX\SYNbfMq3]N\160390of?\987479\&2QU#cY\DC2\ETB\a\134728\&8c`\DC4-\1035600\&0_,\61186\DELd^\DELM\1082727\&3\NUL\SYN\DC2`\DC2(z\1073614R\1073511\158846Vqn\94033\CAN\186179Ap,\68655~:>9\SOH\986818L|\26590\984726\&6\1020946c\31513^\1077430\NUL#\68875\7357SD\t0\GS^P\nmg,oVnT%\1074906#\1079052\185568f\32331\FSG\NUL\aPl_\EOT \1071732x\DLEZ\EM\SUB\DLE\1082444\CAN\9126\NAKnSq^lw" } @@ -59,12 +59,12 @@ testObject_PasswordChange_user_3 = testObject_PasswordChange_user_4 :: PasswordChange testObject_PasswordChange_user_4 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "\996042\1013508\137442y8z\188910cH@ge\96750\bJ\ETB\RS|Z5j\f\USJ\DC3\27867#\5822 =\ETX[[#ch1T\SUB\1062971\159509Fr#\CAN\1067286=Q\1110054\162733\n\RSL-X%\1070501h\1065080\1089630xDM'\12493aC@n \SYN\RS6\"e\985673\1062591\ENQE'H\DC3\131230o%5\996218\DEL{\1010852/KU\STX[\153798\1110733/#\1111047H\DC2_bNy\1099854\vH\RS]\986900wdg\1006378\ENQ4|\991191%\GSzgvb?QC3U\vOL\1090175\1009217\171249Us\985275\1007556\1056022y\ETX\1006666a\1089443\1064461IQ\NAK\1102475/\1025821[\146525Y\1110273\&0hg\NUL~:x\DELd\fZDQU\SO;\a9m\\~\f\167899=*|0\1089233\40380R\FS^\70516B2\DC2\1019556y\a\985058/\129335@>Vh\40618\1019580\DC1h4\n;Q&P\DC2A,f(B\SOH\1028143 \138873\1052427\f\140570$3\158205\t\t\DELs\133507Vp\SUBnDA\nsv\151492!'\1098710\144726X\r2\139117r\186851!@\51165\DC2\1073571%\1026015o\"\bi\1075769tV*\1089261\1000193\SYN\52519\1026058i\"+jB-g\40752\RSL\v'\1089204Faf\988489^\997807\69921E\fo\1041666\1032996_\1042556'\1071888\&9 ,F\95367d\121251\161394\DELY\7850)\n\RS(^\"L\GS\993283\1028777.@\DEL0\DC2,w\136018\ENQ:U\US3\1074021(\26102B>\SUBLh}8\36317\1071795\&1\DC3\FS,\NUL\1036218\164959\ENQ\1101169:\1105205J\1060042\n\NUL f0O\1023842m\36567\ETX\b\STXg>bl\1028623\44691p\SOH\45834\ACKE\NUL:fQC!K\1013456\32733(Va]\FST&B\EOT\b_#`\1041118o\DC1\165469\CAN\DC1>\138365H\1018054^\983454\SO\1088879\1112501H_a\1019703M\1094145taIx!c\64005\ACK\GS$i[\147426r=\ETB\30388Dbpc\GSt\96715\51391\25397\1098750H\1008635U..\160586\136531K\131733M`u|V\1083030#s\7110v\EMP\1008700h:H\ni=\150174\49091\ACKK\63386A\SI3b\EMd\EOTk.t\FS]f\132877\ETB\22782\DLE\f\1013087}6\17773l\\\1063285D3\DC3\USa?FR sHm\ETX\1105953\b[\DC2\STX\1091150\78391O|#\STX\GS+\145799\1109990Tf\6422\1036975`\SYNNL\RS\144764\&9\SYN\97231\988154\EM\1019553\ENQ\989472.DKMf\991253}c0\US\rFZ\1025650.\1068209SK\DC3Isq$>\128748\149897^+\1101484\1014800\n\ACK\"^\177274N2Uo|\GSM\27950|nZ\1078716G\tQ\41315\1068764zzGp\FS(y\22194\13258Wg\1110206\15989\ETB\a\142998\83001K\1041605\140118\138647\1044203G\1017800" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "6%CdZ\NAK:]a\160757G\1100807\aHT_\ETX\184817=\1094974H\NAKV\n\188284\43568\SI`\GS4},m5C,)pOU-z,m)G\1085731:\13371\68388\58925\NAKVBlt:'\SYNr\161012\&9\SYNZ~\53239\r\131378\"\b`}l\ENQg\49807x\EMsO\SUB\r\ACK\ETB\1012862*\119158\ACK'\NAK0u\1063315+.N\155568\&5\DLE\996563\1059464\1095806OE[\1066634cJJZ.>#mY=\DEL \SO\1020809\161961Fi~8JT\RS\t\DC2[E\70439\SYNO\SI@\1012929J\STX=\DLE\SYN.\1056562#da\1101967\SO\FSrCR\SOH_!\ENQ-Wtm\140222u\STX\1093627\&1\SYN<\1086071Y\99519F\1092290\174518\165124,H\152431 \1016376\1086967\65320\1078045\100936\161880\64562\n\RS07e\DC4.\1017260\USl-W\1036127\169524_\rSidOZT%46Me]\bf3X\SUB\30968\r]E%uW\1037702\120955c`\SI\1084987 a`/\DC3\1066414}\EOT5\EOTCMY\SUBY\1018010l>xH\RS\169677\26707}vy\SYN\DC1 A\FS\15039\&59N\29728\1000117\SUB@\1007505G\187702!xi\59210\&2JK\ESC\a^YMk\CANQ\t0:\DLEzo\NUL\DLEx1gU\SI\1005915\&8\142146P3V&\146215l)A\168185>\SI\tz\40878R\171716,\rLb\187682\CAN\983254_G\1019834\1008637EY;\20022\DELNvs9fmb#\1103912\46381g\1086578\54419\986014fJ\60290\v\1003578\180699SFA\STXA$\188361\135582\NUL\RSA\1069366E\SUB~\997873t%.P\t\SOHN\23780=\1058283\ENQ*\42808Fm\987705^gW\STXBN?\1062464`AUpn\SO\58276i\ETB}\NAK\35802\&8GN\71264FAxE\\&q^al$\1099577\DC2`z\67120\131492f\ETXnux\149811q\FS\CANy\ACKb\1075992\61816\nWZ\24019oFZ{to\EOT\a\58806\b.\141033\1061510/'\\bL\ETB{fp\983623\1076286O\46626\GS8\1055057\1088721;Z|< \153326\1088059\1111453\&4aC\SOH\161524\SUB.*K\"\"\129454Y\167276v\986403c($`\SOHK4d\125249\FS\122897L\992931\EM\1063797/AnK\163512\&4\44876:\FS\1071653\1048482$\DC3/Ug\143227iyBpz\CAN\ESC\50988M\153299\t)p?\160170[{K\1064379C\187515/i\129567\1015971k\SOVyO\EM\1027000_\SYN\1092978\137534\37394\ENQ{+\150519\CANp\EM\120158\DC3\1039610}\ETX\ACK\rpf&:\SI\EM{[\47214\141578Pj\DC2\1042947\175183;\tz\13562\&57*\ETX\149429\a\1099670`\rM\b\1065597\a\1061713W5\146248v\61801 \63453>Z\127207\177364t\SOH\99385 \24048@Vd\1098979'6`/\RSv\ENQ<\EM\1046071:74s[\SI^rcI55&\DC4(\1044403}5\1072105\t\SUB\1019144g\1055613\ETX[\1049131\1027231\v[i\1106618\ESC$\574\31775#\bq\1086447T@8\183810\1018524\1080923\DC4.o`\f_27^6>\1018938\20504s\175505q[\161155aeG\1042361HB[\FSs\92188t\RS2)[Qc?-\1006821/z\993159-\US.k\32238\DC4Bc\72192c \b1\SOHCE\DC47\171040\ESCw2.{\1014032~|,\EOT^\1106499}x\1099466\ENQL>>P\168482,.T\1049248`\1106998b9u.(z=@&b|\1039337\DC2\21581\ETX\SI~\vz\159863E`\FSe\US\15482=\"QwNN\129353lzq\190036eiq\SYN-d\137123.-n\SOH?B( T=wf\995467\RS\DC2\179872\SYN*|\147417DM\37567*:\STX\189754AS\t7o\64289(\994294O~kP\68006z\f\ESCe\a\987232I\DC4\DC3!EL+G[\r[_\1091777\9539W<\131337\1098445iA\168912'F\RS(SE\SI^\14294l\1054709\US\DC2\SOH\n\41372j\DC3\156318c\17177DN%\140618&\1004034\ACK~^\35003\58010\EM\991741&D\156963\NAKP\SO,\SIk#\bb\33222\t\33164W\54708\b\169426li\155619my\GS\133694f\US\58936z7fFkx\181089=\96578\ETX\1045516;pk\1103897\1096717.\SOH9j\1106500$\1083366\DC1oc" } @@ -72,12 +72,12 @@ testObject_PasswordChange_user_4 = testObject_PasswordChange_user_5 :: PasswordChange testObject_PasswordChange_user_5 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe ".vT\149065\158692){\74556g\1110206\1091301s\1012653\157052\ENQ,~\ENQ\132963\ETX\SYN\EOT\1103480\155219=1\DELo\989094\ENQ{}S;\DC1 \NAK\987299Y\129547Z\GSqPuPV=I\FS\ETX\1043494\60432\SOmRV` '-#\RSGcv]\47869>~ \ETBN\DLE\155012\1109063\181243\1002700\NAKF\CANu\US\43300\78315\DC3\1075822>z\\.k\SYN[?\\\100534\&3TJWN\1033469\\\23429=PJh1\991408\1081195\47549\DC1\1021540\1100099\36799\99980yyf\SI{%a\CANK\165733?Wl%\185431;Y\"\177839~\SYN\124986n2*F\983249\&0\63886Z\ETX\SOnp\t\1008554rz=x\DC3N\50460*8\tj\1085763\1069586\1021364R>\1094815\ACK\1052450Q\US\1016757`kE [\94447;\94463c\bK\f\1003111-WW\SOHr\SOHndW=s\1064135\FS\SIO\176630\142291\1022975l\14890<\SOGx'Z\41402\26364\1054258\STXW\1047089\1022246hS\144850\EM\134018k`mW\58467\25020\&9\DLEE\995366XON+\DLE`]\SO~g\1044869,9\t;\DC3\1050886\3363pD6s\157184\ENQeem\1045132\SOH\24377Lo\1082536ctA\DLE\1113917`B\EM\94062[1<:}7]&\v\44512R\177157a\RS\63093\&6\FS\10794f\ESC\1076238\52233v!t\DLEG\1015620\\f\t^\SOHB\SOH\180364_N+\v\ETXux\NUL.d\2283\STX%{\120714\1085733\134796\1048671uO\1061770\n\EM\r\a\r\36309\DLE4\1043749Hp\1091440q\1079376bJT<\STXVw\985328I+\1034709C\t\27376\SOh.,\1103086:\917965\9480{\ETB\995773zqY\STXE\GS\51683\&8vF}\170082X\42566\983317U\NULWGiN*v\173195\162226\154581\fR?i\1049259\DC3\a\DC3O(\187320xa\NUL.\133821\1058197\1098767j`\"\64700V\176930\69639M+m\STXJx\FS\GSrVs\SYN\GSJ8Q>l\tJj\EOTDGHj\CAN$X\RS\119922D%\DELV\EM\SO8\988454Z%ah\1074629\2919KB\1036581\ETX\SIP\1041071B\142456\ACKe\1093894Re2\1077169}Q:\1006282C\ENQ\1034308<\170708yS=qL\SUBd0{a\2279s\1075662R\1019777\133916NS7\SYNG\1052457N6Z\1026683\1010570\36133mP\DELO\RS 3\1004867G\96938:,\991792\US\1040258\ETXpNgH\"i\190411\169538\CAN50H\RS\51809@jiHF\18488\1089326S=#\EOT\24653&M\186999\ENQ\188436sB\EM\NULVuJ%wk\US\USJY\US\SYN\DC4@[\133710\2562\1102116\170261N|\25196L/Fs\EOT\b~khlJ\\*\1083562tv3\STXsg\ENQ\DC2c>\48829X\985867\1024387\nRg\NAK ;\51240'{~\\\1070452oSr3\DC1P\998414K,\1058087zN\r8\27838\\\165356\SUB@\DC1\SYN\vF\DC2V\ENQU\1077217\ENQy\1105981alR_\73963W\SYN-V\DC3\1058513|\RS\NUL\14311\1069223\DELV0\aHa\162915?PXj\SYN\DEL\985879\49021&t\"V\57972hA\42234\t8\NULk.\189070 \1112762\ETB\59270\185654U\GSQ\1063565\44619p\1061081\GS\DC3\1108003\NAKKR`\57737\40884\&6\r\101067io@o\ACKkrla\174009,\1070019\&0A6#\CAN=\DC2\DC2\161497&\RSan\149845\&6y\SYND\22050a\f\149068h\162218\&3\ETB\178246#O*\ESCy\168142a\5632b\DLEL:\axng6\59689\&1\1040365\181996\65902\ETB%\164339\ENQtfJq\1045673\&2T\SO\DEL\126474q\NULq#\1012957\1002852\\\r\DC2P\1024058(C\1050472Ph\GSBthwz+'\EM" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "S\1034541[2\119932\&0h!\1064848.\987603*P:\NAK(\GS\ETB\24253$X\179274K\NUL9\b]\170369\1020647\1051557K0\138808\DLE94_\26880\SUB\994542a\176494((\998954TXm\DC1_}\994198m\\o\120794yJ\ETX#\b.K,\151241\126477\r:bj\158134.\14517[Dg\49015\152214BH mH\181369\990387[QnJ\EOTo\\\98736*r\CAN\984313m\146285s\SUBD\17341{A\78451p[\131098U\RS\ETX\"%Y\1089637\v%\21671\1105935e|\67637\DC2\ACK,}\176528u&\v\1067595\US@8+\917796?shAqmaA\DC4I\NAK\988836\SI\SUBl\at\1097599\14469vd\187527s\ETB\SI1,\1026043\1092581\68088\10003U#\NAK\NUL8\993973\SOH\165172\178585\&25L7l4K\ETB\ETB\US\183298(\141108\CAN\SYN`!=d\16001\DEL\37607\990640g \1007747\"\DC1\1035551)\ENQG\1075268JZ3\29025/\147766\RS\ACK\28620\DC2\DC2i\DC3\NAK)\DEL\ACK\NULXs`\15691MUmZ!y3\1107617\188523`n\USs<)n9\1030989\GSB\1029508g\1055800\DLEz\ETB\110827T3F\140208\&3\1088347\SOHnx\a\57612\&07\DC4;H%\SI\SYNsQS{%\tj\46313czj\DEL&\DEL\r\1044089\165481\190596,\12670L" } @@ -85,12 +85,12 @@ testObject_PasswordChange_user_5 = testObject_PasswordChange_user_6 :: PasswordChange testObject_PasswordChange_user_6 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "&\1107610,\EM\DC1\SO\GS\NUL\\Q\143835?&%e\751K\991656g\142735]6\1072568XDu\989822.N-Y\SYN\118870\NAK\177961\1082599Z\1067051\41571\"IIzef\172747\157154\1100946\US+h\1063035\156268@T\n\DC3e+ FG\1040063}\1007879p[\1019675~s\58897W\1002225\131986[h\163754$\1014199d\1001302\135635\1083326\r\15121\&9\1054919\SUB\1033452\48331B\146637\1071032g\RS\34856\SIpN(\n_\DC3\CAN\ETB\994340u\"\1055984cq\148292%\168571@_vDc\1073055CW\ENQP\136867\STXHfp;\nM\1110028\154200)mg\1000362}l\1072450h\t\ETX\14968Q\1021295Tj\\b4\FSK!J\\\996951\1037918\ETX\16997[$\1006298f\US\FS\97025h`f#cq0t=4\DC3\v(n\CANb\SUB\CAN\FS\RS\US\157568\1112545W\ETB|\DC4\26469\SOH`\152656O*E_\1014509_4Lrc\1067039\68473\FSE[\GS\95227vbvn\121463\176466WFW^\1109674\&2\1092465\1101465l=\191025\1020663R\1107046p\189999+T\36798\vy$\EOT\184549LpY}\EOTZc\118805zLS\1099150\\\119989\&9Gzc\120792\1050858_\DC2H!#\169248\DC2d\177928!229\NAK\ACK(\1096427c'\142061\"{\b7\tM\63131,#IRi\1091628\n\994326\155033`\DLE \ENQT|!\1097357p\CAN\FS\138789\STX,\94330\r2\1082495s\1097275\\|\35843\ESC\1078746o\DC2i\b\11053gkx\994356kd\1066993\EMi|\13736\65150\160960\ESC/\1010989\&8\1069363:j\1028017\"RM!\96723`()\63658\&2\135558.\1049513B\171714E\1017316\1070909\1028371\RSR\NAKJn\1032860[VZQ\127514W\NULiz`Ie\1058604I\DELMY#)R6\64879\178752\&2b6\tX\r\1048312\1069402N\171772W=\STXAS;q\123203\1083930w\a\SYNptS\NULT\fj\143164\194759T\fSp\68448\ACKR*In\DEL=X\NUL\66188\vM7\121298s\1024216v!\1084042?\1022676I\1082108hQ\1062292.\SO\\\151754j\147624\a\1077885\ETX\1074145bE\1091072\ETX'\1023670\48208\SUBK\GSP\DC3\1081278\DC1\1085046\159684\a\139723n&\1108740Z\ESC\179659 wA>\141155\NUL-\NUL#\"|\165468A\t\ETB\1041615_u)\165061\143580Le'%*\1107600Q\SI\RS\111344\"2\vc3yVbV\1042395\\e\168551\STX\1090925esJYso\163169o\FS$I\1091068oz !\DC3\119098r\RSzt7X\8274%$-\1046768?\SUB\ACKA\SUBkZk3E\t\1067050\ETB\1019523\&1D-G}\1056157\&2Y\DLE\a\at\GS\7200\nQ\182489\1094286et\USK\DLEv\tN@?}>\CANz\987816j>w\DELc@\EMw" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "b\170229n\1018971\SUB\985694C{_\70741&\vMo6gl\STX\"8\1028959\GS\165216\SI(\138759\ETB\DC4\152692\&1\996992I]\v\RS#\"\1112677&aGfR\ENQ\1018847\&1:\ENQ\183934\1047759\&4J}\GStZ\DC31b\by._\26020\165646bK\SIE\1034932\149906dq\128297\"\"\67990L\94037\SOH&\DC3AIx\1009451z\11657z_\68118<\1083603=>FA5Q\1025568]\SUBS\1067075\t\1027792b!\1011202PY\1058512\SYN\1097813\aCK\RS\151398>\1637;u\ACK\ENQ+n}NzAP\69805RXe3Q4+!5-{\43538xEx\173125\USU\ENQd\187890,\DC3]c\164824\&9YkkCUR\1052545zz\a_\US\1052913\FSh\bX:\1097581\EOTs0DC5.UEt\1082356.~\51232\188301#\993299\DC4LwU\27171\1014215q(r_\SIAq\7019\ESCg \1034226G(\SUB\b/)K\1022799\1006348z\166521\&6\1073570M(mHu\1072369B\141057}^Xz@\27397da]VDH=xZ`E|\SYNEr$P\b\SOH.\30581;8/E\1056666\156080G+\USF\1046048\189590\1079895^\1072919/\DC3\SUB\SUB\STXy\ETXl\1012320<\f\159886\DEL\EOT\47816\&5\1010161 .u\ESC\f\1084279a5\US\100760h\988443\20830_\1112230y#]R$a/\SUBg-^:,\134242t\NUL\DLEFd(\SI_r=6&R\39368t#/\30862\1083006\55251\DELd\139094\f\bLL\SYNrl(\95410_jgp^B\148359pZf\131184!\1100088\1079773\191219/S\60206\157985\SIP!\SI\1030276We\DC1Q]\DELMi\SOH2\164247s\64188\59175\179637\&6_fIJ0-E\51588\39286 \b#\99545\52587\GS\1063696\57533\1094025C\1039590\STX[m|O-)\54684\132598\189752\FS\FS\31494\v^r,^PBK\175477r,U;p&~O\1003644\154009\DC1*/f'L)\146351b1mmbu\1070260\DC4X\ACK|q\ETB\186400i$\998123Q\170080\DC1:\NULa\179425v\1057890\ESC\1046601O?\144872\1001618\&9 m&\185419G\NUL'*k7>y\185109)c\1026066\DLE\t,}I\SIzT!R\1051585\&9u\EM\DC42ixf\ETBh\1093277\45899_\ETX&\t\9508\57743F\1054634K\151449\t\ESC8\n{2\1060622\59202IL\SUB\1114011dp}_9\67990:Xd\66188\134097v\ETXjk\52228\&0x\SYNi7y_\DC4\94598La\tK\SO.\SO:#\158037ZsIp6\DC3\tG\21697j@\SO\140605V\171781\1004444\1095580\n-0x3\1070457JsH\49717\&0.g}vU\985649\175749\t\1108868txWw$p-)NErg\DC1\RS\v\61996S\97223eK~\18154\1087578\&7\139648]:\SOW\f+C\ESC\1052448\131579\1070786pH\1082515g\ENQ\DC2W77\35594Il\SO!<\1029111\ETX\42368p)}`\47291}\143330\&9\US=$SXcjkM\186140Fp.8h\1047276['Ta)Zq\175154\4734\fW\51765C\1027418\1103868\1101167\1052280u\nc/\1112595\156385\1015057c0I2]\SOH]g\161033*g)\th\1024978G\1053938\GSv\USXf5!U\54369=#\vG\DC3\ETX?\SOq%]\147834\&7\SYNW+6\t&\1113103\1100216-l&\FSv\129474\DC4\1062308\1053188\ACK\1016990a\1024054m\1046519\NUL@~\1087194RfG>\154171q\CAN\ENQ\68901\DC4`o\r\DLEt\DC3\92906\1069460\1048347\53182\STXf\1094150\1068082i\1014049\1037453u\\\f(h,`\1047778m6\DC4f\SOx\157831" } @@ -98,8 +98,8 @@ testObject_PasswordChange_user_6 = testObject_PasswordChange_user_7 :: PasswordChange testObject_PasswordChange_user_7 = PasswordChange - { cpOldPassword = Nothing, - cpNewPassword = + { oldPassword = Nothing, + newPassword = plainTextPassword8Unsafe "\1078147\&7\65218\RSA\999884k\v\ESC\NAKg&:\NUL6K\NULn>\SUB\119974$\ESC8B'z\"Yc\1032069\&6\ESCU)\NUL\DLE\1101164Zj\44385\83195bJ-\US\"\131804L\a\1067731y\DLEs-}\141826}\GS\CAN\ACK\rR}R\CANL\FSqkZ\n\t\189000\&1|\f5\984053L\ETB8gL\18292\&5\10771cf\\V~\DC1\11412\EM\120833\990084\&1n\"\60837*\ETB\SIaTxU\DELcZ\r#5/\bk\v[\a`\1106514\NULR;(\CAN\rFMN>\995764\ESCt\ACK{'(\141540\ETX\NULT\1057079m\f6\63805B\n\987874" } @@ -107,12 +107,12 @@ testObject_PasswordChange_user_7 = testObject_PasswordChange_user_8 :: PasswordChange testObject_PasswordChange_user_8 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "b\US\1102445X\1020217\EOT.m\"\DC4\DC4\ETX:1c8\1003324>QU]\ETXRe\1032621\SOe\SI_~\t\EM\182748^BGb\991684\SUB\1032747,4Ux\44572\nA\19832\1062925\fo\CAN\1001285#+@\14237J\SYN>\SOH[Q\31322P-\ESC\DC3\1017636\64940\NAKQmD]\f\" \23277\26752\DC3F\1087665W2\GS\FS_\145580S\34366/\SOH@\1008116u\US \DC4x\989165\138589\&5\60472\47193@\995028u\DC3\127467\169198g!\174297HZM\178770\1109385j_;\39894v_~\1020633U\nzDxZ1RN\2467mdb}\152593b;s\ae|&\"\18230l\SO\1075495\73747\165342\418'\DELj\993281UB[\14633\NAK\"F\1071892\71739\&9I\1086187\SUB,+\169163\EOT\EMBtZ}\SUBZRdm\DELA\ahEj\NAK\164812\1045403\49808!i4\160130,,-4T\39327E#\FS\129309\SIKj<\1109332\1019724`R>\111006\139594\DC2Vq4\DLE\131391\&1\51249\ENQM\42303av\181926\STX\1016985\NUL5\164635!&]\22190p\v`k\67413\&6(GB\1042616\DC2L\996758a'7L\1096604[\ETBR\1022507|\1020702bZ\1060760;6\GS`\NAK\1055957\SOn\128679\1080437\1000675L\70839Vn\189246nw\EM!*bw\r\1102406\ACK3\25917J\1100924\DLE\1079071\RS;9\ETB\51636Ts\n?l\171848'y*{ G?>y\166331\&4\1028518\143808M\ENQ@\1106697\&19\62848:\GSfI%%;p\1057791j\STX\52156{7\1045649mR\170180 \1045874`b+\189602\1095783\29108<\997493b'\1113133G\1113924\187365\1018965\DLE\t)\ETB_\STX\188043\ETBq\ETBD\14549\178567\&8\GS9S%t-;~A{\1098493\1009689t\RS\997797\rD\SOH\"\1036045\1080223R\r\r\SO;b\1079046\RS\96789\64328/*m?~G\1005579Z\1029293)\141393\134174\1004939lL=\1066280O,,j;x\SOH\74911\a\n2.+-\16525&bd\142521.\NUL\1105545\DC1\61097d\1016348q\ACK\v\"\155055\1051009\1111466c\DEL\a(./\STX\10580E\1095607P;\"\1100473h\15195{\21638\1108997\1001215Wme=ny\DC2\997396(\153889\990739 \rC\DC3?>ZD\SI}j\SUB\SI\GS\177033\1081156 )V>\1073618\1110301)*l^ip&^\EOT\991196&Y\SO(L&\FSs\1025953\SUBm\194690OR*\1083553\984637UQ9a\173357\RS\"\170635pt\DC3hxbnb\144388\SI\1096629M\36441\183861fJ?}t\1042071T\21290\1041177?\GSg&V\1107865D\NAK\58427M\1083184\&0U\b\53742\1049758\1019549hjT\"\1047744\EOT^\GS#\NAK\tv+t\FS\USL{\1011965\126503x,\1024988\DC4\1026933<7\1074268\ETBw\rz\159720\985242\NAK\SYNq\65888\1019932$\1038698|:\v\SUBd\n3]0P~Q\FS+&@[\v\92526M\v\136444\DLE\US\a_S#H\ESC\1000365\178961\66613\\\"\b}8\USF\t\ENQ2|,\t\US\36910\996072\DLE+\166272D:\148639\&7 \GSe\1085048#'\DC4MpE_n\95537~\1018210@\1059219&=1\1058223\1085407)R\1035591\SOHt\39215?\94500\32949E\171322\EOTzX\1061392/mZ7\39206\ETX`m\1055925!$\v\DC1Z\SI \DC3,\14510\STXil\DEL\DLE\7164\1027803A~EU;:\ri\1083540l\35399vc$\SUB\DEL_\1081553EP4\1103837l~|\a\1051360\ENQ\SYN\1096952+\1074553J\19836\t\GS\aA\EOT\NUL4g!\62787\150045\22255\183201\STXN\ETBrrpN5ks\bZM\ACK*z\DC4(Pl\DC1\"\70701\1067954K[\1008837RH\1032341\CAN[x.\1048119ac9\1111224\59370$\19588\ACK\f\74197Jy!\29122" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "\f\CANj?\ENQ\100674\27294`\v\60820S\aUZ<\190604\v\n\98721\SYN8,\SO`\ETB!\1023917\SUB\988878 \149166~\25356\&7k:\SOHo,w\US`d\1095991E\170702w\ACKl\FSOUl\DC1r\DC1\4696\1005535\177324Y\NUL:*\SO\51294x?>O@mGy+Z,\SI\31196&\EOTZ\14202.w\\\GS\CAN\DEL\1061426\FS#!\100667NFM/\b\168841 T\183374\1014354K\nG\95679\1071981G\1014345!]\GSN}\1071684m\CANH\1032944Nk?\61487K\EM\131256ov\48786V\r\184775p\61887L\1085562e\175014\ESC\173236\USr\135299\NAK\SYN\NAK\DC33r^E\43094Z\bP<\135606\138117\1066630\178853\27013\1078589\ACK\SUB\NULEa'*W\177921\163435\176746\EOTLV4\26629\"-/\118982\1108171\&3\19533\GS\SOH/xiy\1004921\31236\DELZm7s8l\1032610BT({b\59819n\DC1\154649vD\59996\95915r\66886_#d\34706~\53775;BKF\993228] 6f\126105iN^\132202\CANeN\1050181X:\"+\STX\1000519C\DC4*\164663j5\1087078=\49843\163443\&8\46178\1005505\1086358\67354\&9}SK\132067\\0\120968#v~J mt3Xx\RS\US\1053047\v3\997095\SI\DC2Tc\996715\DC4m,\SIn!b\26969'ac\29011oV\18582W\100115\1029633\t\SOH\149270\SO\1052983\&3%hmTnE+\\%C\24956\137609\&3\986293$\1010528\983647\&04|En(\17123\SO\174091`Xy\1069572\984775 \132546~\1054660]\DC3\167285\&9Y\166240\&3T/\1057195\58265\tBS3;A]\DC3\2765P;.\1046618\US\DC3\NAK\GS2[\25411\1061324 \13123\165595\"Q^0\STXT6\18123yB\DC1?\NULv\1081840xd\1060136Y\DLE\1094984\RS\168967\a\SOH4/R\113820\1029185V\DC2A6\1016176(T\\O\USTu\152631\ENQ)\22634R\ETX\vR{\r\STXm\1044646\146582\SUB\154494\32280\1053397\EM\SI\n]&<\NAK-=\1052283|S)\165884\43834Rq5e/wH\SOTh\145516tjJ\1032777g\CAN\DLE\ae\SIV3\94033`\1100278s[KR|Y\CAN\1079014!\ETBM\1091893\157876U\n\f}!K-\ACK\ACKmp\DC28:\GS\EOT\r\1063024\1023444)(\ETX\1017315\983729\60554^\1097871\101098+e\30627\t\160747N\EMi\nfN\98956\149454\&4Q\n\r.3\31727|H\ESC\990983\998110\159749" } @@ -120,12 +120,12 @@ testObject_PasswordChange_user_8 = testObject_PasswordChange_user_9 :: PasswordChange testObject_PasswordChange_user_9 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "\121027il\134081)R{5\ACKXm\1041631\\T\f\DLEp tq8\1070074\&5Q*\"\1087921@\DC4\182306\CANN,\162026\1112739;3.p\STX\fLA-\149790j\bON\ENQn\984593U:\EOT7@\GS\1102637ky)\74361eri5CF_\1086719Y\25273^Q\SYN\NAKQ\DC3[\31615\ESCaY\180511~\t\100087'\DC4M^\EMI\994154\&4\96550\1036840b\DC4r\1078078r\RS\140751\1084467\SIuHc\rg\DC1\ENQd3\SOH=p%ry\1003698\&3mhoP\1106864\v\162715yXMw<\59204\&1\f\1016334\ESC\3501lL\69237\GS$\a\1039285w\985184{)\NAKw82K)\DC36\155645v:\SOHC9i\1039062\17926R\1072663/w\99462\15991\185843e30R@T\121319_\NAK\ESC\\\1092892\n_\1069021\aiinb2I\RS\1098801\138282\SOH\992030Zep>H\1079810uUC%\tS_fH \SO\1084851OH\a\ETXx\a/\ACK\ACK\993315\165332\72410\183658b`%\31687\DC3:UIj\1021763\ETX\1053142\NAK\STXS\DEL[\1028930\&1Z\SUB\US\1088016h%\ACKw\EOT[yh\ag\RS\994622N^{g\1003836\993133\DC2\a-\1061684\rrJ\1036806}~;8\ACK0\1021569\SI0\ACK\ETXfXG\SO}H4\DC2zC\SUBz\DC2\DLEz\DC1\149550\61518H25*Q\1083850\10672\NUL$h\SO\134582\43597$,\65487\61824O)^\\nvK<\989262\&1R ,k\985467k&\1012054\1072126V\98741\170921E\ETBEP\RSR[7F(H\1006507\EOT0\\\CANk\DC1\1085575ql\150344,\DC2\NULv\SO\DLEp\SYNIqpU:$\1051572|+\DC1\SIT\1043680v:\54535\133122\SI\167063\190640\r\NUL\1080625--\NUL\1000447J\158492G\1043941\ENQ7\NUL'o8\1055620\ESC5wuH" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "<>f:;-RJq\6050x\161753QT\100505;X5.\1024124;1\fP={7\136758\151452v\DC4b\ETX\1041908\NAKV/R\SO\1003791!\SO\ENQv\SOC\v\FS[Q\ETBh\CANX-\66455\22025\1087386:BVj\1104389\ENQ\41001\97464\DC2\DC2zRZE\"\DC2\EOT0\164473+i\USE\52704R\GS\1015386\FS\EOT\154897\185499\USew\178235\vR_=\1105788\78173\ETBkB\172309\995208\&7;!?'ZN(n)\1003220|\151096\DC3\n\DC1\ESC\1047644\EM\14646wov\ENQ;\1082140]\118864\1041829\131956\&4\1085467h\42033?iD\DC2V\96442t\78699 %\177867t}%\168450\1068330N\EM\b\7477(\6702\&9~\168927*A\ETB\35836\1087213\ESC\EM7`s\988223G\CAN\171597Q\1032850\"\EMi|\ACK\151936\&4\49571\&1 ^\1034297\38608\1080861BBO\ETB\188460sz\GS\1113432\20959_PsX\152878-)\1013286\f\11345ZAT\ETX^\1103065$\1002688\1102176M\DC3\1106060\1083723\31676\135940\1010227}+p)H\EOT]\61870(fiL\74358\STX]m(c\1099516\1058859wN\135817Zs;&;\1101239\STXc?\nP\164370R\1073337\8218\DEL$\62817\1035797'(v\94886\RS\131427z\USV\DC1\995931+J:\1044870\28567<\161564\EOT\t\SI;Ll\31033Bl\1035926g]\EOT4yZ\143213)Qs\127306b] \149682\NAK{\\\\]\48933\1040819b{\1049468F\182947\&1\"O\NAK*\24604\US\989988\52604\&35>>|\CANH\GSZ_4}6\1056732Ty{?\CAN\"\1061905\1004010dM\a\21624.@\120724\DC3\984067hX\DC299hs}\ESC\n$\ENQ\1044696o+\3801Z\66465h7\172119fa$gT\t\1000627\1111076\1075382X9!\40902\a\EOTH\100163\1019832[\"\SO\1061034|};\1001901\55166D([N\SO\1037726g\180696\22235\142179Bxq\r\DEL\1109671\"o\4735\165730\ACK\20074T\38821do\au\151559\39351\SOH\SOo" } @@ -133,12 +133,12 @@ testObject_PasswordChange_user_9 = testObject_PasswordChange_user_10 :: PasswordChange testObject_PasswordChange_user_10 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "^g\165188\183325!D\1071796\&8I\rv4MC%k\\X\NAK@]\990411\EOTkKg\46739h\v-\EM\1003941d\r\n\46818\164542\DC1Yf\1031947\100661SN\t_q\34663\1024752nn,9\1002494\134950;vL\1005623?\vL#\1004806\159319\1005755Y\49264k\185970\EOTt\54951\&8\SYN\GS\DC1W\ACKxK\100106Dc\1028596k:\1088790\DC4\1024192\&2D?pHS@i\1055560\&5\"A!\127257\DC4\57827M+CS\EOTK\DLE\DEL?y\1098181\162494\47866<\41529y\FSk\SYN${\r\1005146\190068/\DLEY\20575\b\1039849A&\STX\b\998416E\a\1032363J2\120490\1018750\GSa+\nL\ETBE\DEL\NAKnAd\ETB>\ENQO&\b\1011115@:I~-a\SYNk\DEL}\USE)C\DLE)ts\SOH&\128490f\1031578O)8\83270e\59254\&9\58057O\v\r \bt\188311\n\1013246\46070l\SIWGb\1008559@\1059413\22227u\1026214B\1029435t\1109601Sx4bL\f\171190^:6\ETB\EOTz5x\FS\RS_\ESC\1105088\DC3\1111332|(w\1030422\ENQ:\45632\72881k\1036191\RSwC\186931E\1106146\RS\NAKJ\1043833)\120159\1023499a\1068709S\DLE-p\142797Y8~\ACK\f\SYNq\SYN\191139\1061750Vq\SOWh\EM\6136\EOTA\"}P^M1 \150446;\ENQ[\83011\78574cG]o\EM):r\185345\1099699bCeS\1095638E\SYN\39700\42082<\ESC\31948\83108\142987\&0(`\SOH\rr%Y\ACK+\1082430\142137\RSK\38850\1042506\&2ZK\EMc\EM\NAK\US\69438\51321\tyI~b\983734\&4\\t\CANu\1070201W\SYN\53093O\SIwT/\1054638Q\1005484\157400\&0\a\1040610a?\95679\NAK\SIpZi\2699o\RS}R\bm?\137670}\70000\42449\32037\ACK`\NUL\ETB\36412zO$\CANo\tX,gCWQ\FS\137405X\aiI\22269\1099880\SI\1056230" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe ";\STXW\3658\&9e\61104\1010281u\1054825;z\1102201.\CANm\GS8HiR[\1090721\113679h\30385\1099071P\13786ux\EOT=\EOT@-\NAKU~\30622nm\181728\v\SI\"87xzsd\t/\SIABdv\SYN\1079721\38516ov\r\61169\DELC=|EJ.I1ZH\r\1109850A\157023I\DC3h,\150284\157340\40343\DC1\1101518\SIvxV<\100481\CAN\992513k79\6439\&7\1105382\29451\51856\ACKw(d.\r\986761tHWFI\b\2063|c\139209.ZR\ESC\1043464XOH\b\1065603\185045\DELKC\SOe\ENQb\STXtwM;\1063586Qb\EM+C\a*\991991\SI\68430#DEv\70295\&5p\EOT\1017741~&\92197\6498?\DC3A%4\111178\"v\NULg\tmD\991529\DEL?\148659\v\\7\n\n\US\1052346EbL\NAK!` \996371\a]\1050364:\99420o\98763\1038145\r\US}nre\190462=\RS!\1026220\RS_\n\r^+\1112053\155114j\144557h\164197\EOT\1456\1080248\&4jpQ\ETBv\1058697\&6XK\DLE{X\182304|\FS\30623c?\ACKW[\FS)_~,\187940c\133750Ihm\68052\GS\NULJ\"#\b\41024\&0Zh\147884\1013140F&\ETBJ`O\191380H\917827&'Ox!4\ESC\DC1\1038348+DgC\95526d\r'\US{\1062664\47822?z\DC2\17591~\96360\155417\1068401l\14806r\NULN6<\57679\1055613\141513]Hp8\1063393\&0WrR(\25161\19762'gc#_^\997158\31893\23078\179623E$zk\t\NAK4g\STX6)\993627]\DC3\187429\34110p\1012714C\1078346Z2\DC4M\FSJ68E\37649H6\EM&\131250/\DC3ovwm\14557&m\152064M\1027607F\146051>\f\21330\NULG\994703l1*bd\SUB.\ETBWy\154255yo\1081513 zbzr\1034720\32843\EOT\1000002\16121A\133186\btwr(t\ETX\NUL\1038295?9\CAN~j\1018782E\176705$\EM(&\DC4f#eP\EM~\CANCT0i\1007344e\1041535.\1023395u=\DELu1\173333\DEL=\NAKYL" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "\\2~\tLsG\1010318N\73893\19920\SIN\bA\1084446\149836\n\96723\&3\1005158%\53931F\SOH?2\985088\1043578H\184226\tWe(B\26887\SUBU\ENQ\v\188995\"|\53505U\1043137QJ\FSN\1098083\1056930eN\DC1pc\SUBY)\STXR,w\1068893q_\SYN\ETXM\179588\ETB5\19176*\182041\aig2\n-\au" } @@ -159,12 +159,12 @@ testObject_PasswordChange_user_11 = testObject_PasswordChange_user_12 :: PasswordChange testObject_PasswordChange_user_12 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "cA\1013876}1\986971`\11039gQ(6O\148533\ACK\ENQ\144020\1097898S\b\1026784\185340\1076893\1038191.tZ" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "*~\EM\SI\1035297\65924\ETXSi\917783ua\DLE\DC4\SOHgPRF\50551}J\SOH?.\35757\26118S\99244Wb\US{\191213\DC3PD6z;bU\SOc\168740{\NAKC|!\134012w\24191_\15393'XFa>Ds\1032722\&6&\160998BY\1105510\987553u/\146729\187725\NUL\DC2>A.>\1026064\&4~\1066860\983616\1065813\194763\41670n\NAKe4`v#\54249\154796+L\au[\119894H\1003679^\1052741PIo\EM/R\RS\DC3\150872>\a\1011374\&8}ahwE3\1009691k\19141\1096715\23648\131344\SOH>BwWU#\135314} \b\SYNK\1035065\157575\27111\&98-\995803\167979Y\SYNRkE~MQr.\47537\a\EOT\100066FE\SI2re&eE1\SUB\CAN3\30141eKHFCl6\185160;/k\r\SUB\t\41097@&\159450 \47257\vM\\r\SYN$\NAK\40078n\983737\1100061u?D>vA\95624CnF\989092(\71111\176451\62790RhM[\nB>*A\SUBgDpp\NUL\137828\1044414\DC3mQ\ACK\1088526e\164816r\NUL\129550\3404\62114\GS-lk\140775/\1031265>\RS\ETB\DC3\128406LFAs\176184*M\1085893\NULpt\b\100127JN4QU{Jev\EOT\STX\1108765E\t\153121\1033318I4K\\\163474L )RYPBk ~\v\RSn\1013852Xxv6#\1111946|\1067819\f\134655\987741\ESCY\31029z\NUL\ac%i< \n,\128826l\RS\1004102\119256\94776\986312P\1051689V-p\twc\CANV\185920;4\41787\1020199.\b_\1035216)\DC4K\EM,IX\DC13\NUL\1035747\&9h\US:\SYN\ETB81\160334\&1\36963\SI\RSC`\141966\fn\f\100236(\164834\180065-\SI\DC1$g\1046824x\DC1\99084~\181210ADm\NUL\1033535\40647z\999919Q[\STX\v\188766)q\DC14\134546j\DC1$\1038869\178209\1020722\ACKi\995076\&6by\986338E\DEL5\31674\1053862=I\ETB\FSD@C0`\n\1108426JR\97512j\STX\1011610\132328q9\12587\1110037%\ENQ\DC1)\SI \133259S\EOT\an?L\17808\ENQi#6:\39370\984528$VZ\ETB.{m\1105413\ETB\1096254\n\1029048yP?CQN\58229\DC2c\1016719\28430\37793\1021922\1037171b@<\FS0T\SUBs\NUL:\EOT+\NAKe\EMv{u\35899aS5ztr\1095275T\1076768\1067480\1055258\t\138199p\24191!\63947\57751\184259W'/t'\998026Zf?Kaa cX[\DLE@K\rP\DLE\SI6F.\\\1105071\EM$\ETB\185348\995728\1111114\1056306{\CAN\131737\SOF\"+J3g\14443W|\1025079gsE\b\RS\146118\1044328eZJ\rN3\v\r\\\RSTp?\1047550l?|\1052685H\STXI\1041763f\119524>)\150862\1004663v\997187\DC4\62145E\r\NUL:\DELi<\ETB\NAK\1034255\SOHyq\189759\39702\99276\1069813`\SOHt\DC2\1027302\1081467 ^I8\DC4\SUB\t\47100>\DLE\1007609-\154421\US;&\996462jK\\%Bg\170965:\a%\1030923QF*0\173510\1106863HW\52749\ETXs\1026228aAS\1916NbLWp\1051310^{T\29700\179151YL+~\DC2Zv,}\1111746\167987\59316\t\38658v\60679\176523\ETXM\52344,\DC3\1111272~\RS?_y_\DC4u\a\30652\SIOiiFo\1051777\DLEO\DC3{\40043o\1047361\ETB\27903\DC37S'\ENQ;|\1046730Eq!\1073488\160026\1006141It\175865Z\1065806\DC1h#\DEL\138456\7911'\ACK\169193ei\STXs\1104017\1064860sf-\57677\1006904\992638\RS8\ACK\SOH\CAN\146274GJ\141048\GSHp\61180\GS\EOT\186295z\EOT\tp\STX\FS\SUB\96038:)\1029325\42428\SYN\SIg$\139209M\1014224\DC4\ACK8^\NAKt\9753e\1093154\1043578\1079420[\NAKi\66187Q\48349\&3IV3\171979\v\DC3\SUBxDHJ}MT\r\SO\179759\145196\f**Z\64475\1056846]7R\ETB2Ez\DC2$\176439\1000728BM'9\ETXEa\SI\1065849I\1098032\98480" } @@ -172,8 +172,8 @@ testObject_PasswordChange_user_12 = testObject_PasswordChange_user_13 :: PasswordChange testObject_PasswordChange_user_13 = PasswordChange - { cpOldPassword = Nothing, - cpNewPassword = + { oldPassword = Nothing, + newPassword = plainTextPassword8Unsafe "\tY6b\1071064\CANJ0\RS\DC3\fg]\8556^\DC2\1029089S<\ETXn\DLE\161054\NUL\1009413\"\NUL\135103\SIU\1028385K;la\SO\132923`\50089$0#\19913\f~\RS\1050195kyBx\a\ENQ<\DELbY%\1106346{\144787\ENQ\1006226\SUB\n\1111798=\1082031\nl\1027190\49972\t:Z\\\45927\&7,\ETB\\\1043520\1040129m^\ENQ+\ETXepC\CANVO2:!\155826\n\CAN6g}\1100418q6\1056075\&6<\171664\SOH%yP\175359\STX\ACKo\179550g\1071640p\1006475T\1018644\ENQ\SO0'F#^IVd\n\157140\141227A,@\1053337C-\181395g``\166195\NUL\7801\1049487\138364\&5}q\50268\SYN\1089481\134438u(P\33463\SOP\47384e\n\DC1\164033\&6\1083698\SYNM\168121\&7\1027817\DC49\1039185O9 ,Op\983226UR\DC3\NUL\48061\1049901>(\1025638\EM_Nn8\FSb\DEL\92741r[p\1113723za1\DC2w\DC1\24935\SUB\169669\FS_\CAN\EOTN6Vyi&)\1012450^\135732\EOT\CAN\41126BuJ\DC2`\78370)qq^@$*\SYN\119136P\v\7875\ACKg\134713Mg}\ESC\EM\993564\1036198w\983924-e\31379p&\SOd\1022808\74004a\15280\1040139\1056286\RS\143232\1056072'E\181014\98120\&9\DC4\DC4A$\180660h/A`\DC1l]3Qv\14807MR3W\FSsn] a\NUL:3`\95284{`\32597\n\US\DC2.\172218:?y`\DC4\1085202_%S\155378:\NUL\171483\EMk\"\fWYu8-jr)\184?D\12340c\1107469\1096889\1089369^x\SOH{b\DEL3Sl:&0xgT321\180495FU\1068409N\1113930P*L\145663\64596i:\48860\SYN\164807\&0#\ACK\48791\&0v\1049613n\SO\159015P:" } @@ -181,8 +181,8 @@ testObject_PasswordChange_user_13 = testObject_PasswordChange_user_14 :: PasswordChange testObject_PasswordChange_user_14 = PasswordChange - { cpOldPassword = Nothing, - cpNewPassword = + { oldPassword = Nothing, + newPassword = plainTextPassword8Unsafe "6go<\1060200f\41213Y\1084615*KE\1038629])9\1028527\1090910K\2404\1065550\&1\99810>qb2`\NAK7*al\v3I*\156801\SO\1090154\f}\DLE\139358~\129615@jjd\SOHO\SUB\SOH\94999lv\16578B\b3\1066265?Ih1bv\v\SUBz\SOH\FS\128520\&0.\ENQb\50990\FSR<\1111211\EOT\DC1l\"E\125002X6\NAKS\1011858?\ESC\EOTEK\210h\54053\16688\f6\917624\59462>\v_Zd n<\DC2k\1086856s\1069883~\SOH\1011269\CANr\DLEG\998802\NUL`.)lj\DLERr/\149432\\\176664\999860\187741\59007\96806kI\1040467\&9\\.!\STXpj&X?=r\1072676A5\169615\18716\NULex`\SOH{ \121420\ETX\999279^\98959D\DEL\1051244\163196\132146m\58414\35040\&61JU\NULtn@pCF3\fM\155170ZrHM\1024580i\136496zhn\1010172\983207\ENQ\ESC\v\US\ETX\1078490\1027708\DC4lU\DEL\GS\10612[\DC1B\EOT Y\162831&cnLev\20431awe\n\175441+O\69646N\1039476\986854\59235=x[7\"B\RS\DC3\DC1\bQy\DLENq~\1100372G*\1040946C\191033\DELHo[\96055\&9'\1018134\&6\186449e#\ETBK\49381W\SO\1108069&kH7MQ\ETX\EM,v2\SOHN/\1044045\tO\169061\SO\1010256b\185510\1081515\148501q\1037709\1091186ww}=$D\ETXw\DLEb\1069094\EM~\142428T\ESC\CAN\74821Y~{\f{p\138353*w\1062006Juo\n0\150906sYXHT\USK\a\1009732\ENQ\n%q iF\95870\ESC\GS.c\GS\1014409\1066933\USW\EM%x\1003810\NAK>\DC3\1058760" } @@ -190,12 +190,12 @@ testObject_PasswordChange_user_14 = testObject_PasswordChange_user_15 :: PasswordChange testObject_PasswordChange_user_15 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "n\DLE\SOH1\ESC\1030226%\1069394\RS\182899Guy\1039539+\1113955\1023913@\NAK\5561U-dZ'\fB\1055523\30303\SI\US\CAN;V\SYN=\FSz\"\1085023#`\USt]KYs{\v\45407\"\8592\1064953\1006367PP\n\t\31925\1041417\44390\&5u\60622O\29903\SOy\SOH\1051143\184117j2\60717\1030594\14253K\100794\SUB]A\DEL#VL\RS[\\\1044640F\b$UXg5](A\f#b%\1086075\NUL\1041235?\45258\1073954\SOf\1095011\GS\140034b\1052232\&1\998007\996181>\49135rnE \ETB\f\f\FSM\984153DQ\GS" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "\137821\"AZ~;\1071515sK>\1003094\32681rdU\SYNZ\FS\1013170|~\68860{W=X9*\1094479^]h\v\1037179\SI\1105700\ETXy\b\121173\EOTB\RS\SUB\ETB\1087347$\1086752\&4V\DLE*,JI\EM3d\169902\&8`0&\182876r\61161*T\ETX\151630_\"\SUBT\EOT\SYNyw\43410\100742\nP\1111807\vJ\GS;'\a\1019800b}\USu\15085\b\SOH\DC1>f\ESC\185821\SYN\ESC\29398\&6B\FS?*HjHc\1072255\1058427}\1624h\n:\154398\"\1051991\1099837\&9d\128882AXf.Y\139982\&2>3\985533~\DEL\RSaL\DC2!\n}\DEL\DLE\aV\b5\1004615w\44033\ENQ\STX\986416oe\47326\&9\"\1012652\EM\\Z\31242\\\1054641\SOu\1042537\CAN-\GS`7&aJP\1066356\999545_Di1\ap \SUB\STXv2;\170127Fmg\SOHX\1102996;d%\ETXx\99896\EMOr\EOT\DC2\162508\GSvo\1035769\ETX\51961\&8M\NUL~bI\1096210L{p\aq\1026887W{PVOSq\132165\96511!n\t\16523U\t\ESC\1014032!\EM-IFP\1096087\97677G\1056015+l\a\EM.\1051294\f\1031336H\1049728\ESC$f\CAN\NUL*\bnr\1049928\1075881(\189737^\DC3\98799B^\170344[F\999872\ETX\NAKJ9;y\ACK\SYN\66458!51w\64275?Y\EM\DC4-1(x1?(:\SO\181899P\59702_\27711\163618Vx\ETXuN\v\1055926r\CAN\FS\SUB\SIu{\1026849\136958{ \CAN. \1004662\1081463\ENQ\SOHD\DEL@\179223J\ACK:\167491Wy\14989\147498j0(\ENQ\1102373\1014240\&7\SOHTI\RS.\118990#\"\162391/tg ,\1019276\1069245\&1m\186668i4\67826Di:\SOU3M\144745\1112930\1006102\&7A1B\47962\159987\ESC[O}\1028140\1033214\1061595\1000273G?~\SO\1105814\n\23793\CAN\132894K\1109537\157688\&4\\C'\171760t\1092105\1069028^\154207\NUL&aW9OlY%1\t\163491bT\n\133769_DO\70287\&7\EM(z_B\14519\153806Fg7\SYN\EME\1096879' \1105838\ESC2\fm(WW\1091836we?\1088332\26513\CAN\155517LZJ\NUL\DEL\FSYIk}\120430\EOT\4637+kZ\SI\156899x\SUB_\DC4%\177759\1057446VC\1097314\1074153\1072386Oqn\a\RS\1056654\"\18164%8a\28468\132645Tb\"C3\1103957vG\1089945LF#{\96210\998246\160936i\STX :\163339\61888i|\DC1\1011444T\t2\RS2?IHPdw@fhLKXq\61905\1046908qz\1038449z(\189299,\b\SOV\\8C\DC4\5575\&1>\SO\ETX!\131673&O%}XKML\43288j6\NAK\1080490\SO9t9Ku\141219\154727\RS:\1022834df\ETB\996821s\16300\US\173093\986989\ETBL2\"\64028\1047440\ta\bZ\185810,u\1054582\1022464\991444\n\SI\1090918\ESC.B\1024218B$\SOH\SOHv\23330yD\1082294UZ\996426Zy\1031823oX\EOT\SOH\174801'\a\125038ub" } @@ -203,9 +203,9 @@ testObject_PasswordChange_user_15 = testObject_PasswordChange_user_16 :: PasswordChange testObject_PasswordChange_user_16 = PasswordChange - { cpOldPassword = + { oldPassword = Just (plainTextPassword6Unsafe "S+OT\38751b\DLE/B[\100483\&3\47760\GS\180067O#o\25466\&5T,8M~\GST#\987895U{y"), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "5\1103104\NAK\1014216?M\ETBj`-\30597\181188\1026387Z\1094596[\1092626\te=\991832E\DC1'RlS\DLEZJ&L\1107431~G\CAN$\vd6 5\US\1006596\ENQd\r\b\SO\1100302\1110521i)jc@S\156632\1002333\v7\24501tU\a\1049077\\hD\1110213\v\DEL;P0\SYN\\q:\\I\990426Ty\1097835wk\154857z\DELC\36957\GS\3138]Z\16454\SO\US)\133053g\DLE\DLE\GS\ETB|\44640-\STX~\1024260\a\1000452{\ETBK\DC3 ~.Z3\SUBC\986330f(_/\1110859\1055634\1003279\&7\183j-\171356zX\STX+;zT@?\amp\SUB!\36089h\r\992554\SO\SIt?\b\54803/ y\NUL\95035\1077028%\1099069\NUL\1063994\DC2\DC2!|!\DEL\t;D\ESCD\1041733OT\1061393UJWf\1113505\178024\ESC\154767\1050223\FSX\1026016\1020780\CAN|ix\1091727jZ\187257b\NAK\SO\1030980r\DC1)\1053891:\163447\45030\&9<{e\1079093\30596L\NUL\STX\1019960;~\985116}\1052410?+&\NULz\144674\1086689\&7\1030068%x\FS\1036306~\120570\RS\US*\ETXp\1034462\&1\149891\13986\1055542\STX@7yY+\ETX\NUL\1062210$J\1067009T:\EOTzl,!\SUB\DC1%O\DC3\SOHX\FS]\1013399$\152121\1104444\\\139341PX40\CAN\v_,yU^R%\DC3e+-g\172222\SI\DEL%f+h&W\ESC9,Jg,'x|\51952/{Y\r\NAK\1057765\DC2[\1038364\SI\28850Nl\46666\1885\NAK\NAK'\DC2/H<\180011]\ACK\1090504@9\127306Y\150151(\US8\53321\993078c\n'8]\SO\186951q7RH5L\1028090\165@\8885\30083NB2x-\1014943\985470{7o\94409)\1031807#\ETX\42922rid_^Wy7\1029256i\1062709\SYN\99669\n=\21963\&5\8639\1035935\1067300\53855\NUL\vO<\175839\&6\67816\ESC\ETB\EMJpG\ETX}\nM\177929\96385\ENQ\NUL[\1007534\US\1085889\"bl6\\`6\EOT\RS\",6?>Jk\1044669\160533\993117!?4\FS>,\DC1%\61901?y_\ETX\1016387\v\FS\DLEUQ\27172\187044\73101\24011\SOH\169041\NAK\1044569aK\r)f\SUB~N\1020859\DC3k\1012707$B\DEL\SO\ETB\ACK\EM\73084\58832~tVD\RS\SI\n\RS\1037043G<\52368\1007888o\ESCftC\186158\n\36317\b,~\ESCM!\ESC\174873\134091`\1046265\998677b}k\67343\1077779`]1^-\NULO\1013355r\24494\149416\36343`\127285\ACKW\1097424\996658\&3.tS=\983895\ACKs6p[\989667\SOb\180485\1076744W\CANdO\128541da\1063827=\1113561n;\180045\&8Wn=\SI)\1025924%\EOTj\1043094\NUL~D#W%\NAK\b \64862\DC2jr\27380tb\GS\1014983r\bD\ACK\175197oH\4243lJ\51936\1017192\59111\1024329L<=\v\78854*\54478 97q\1013840nS'{->/t'\1065169Xq\917836tmel\1025953\1010549\1013101\SO\DC4\"$\US\SUB\1098531\18016\r\DC3\140813\95239s\28689omb\SOH\1102241P_&\67318X\f*lfw~n!\SYN\ACK\a\60339\1012508U\1104365Y-d\126581\1068676\NAK5\DC1\SYNO\1060779z:\RS)\188550\NAK\1026997\59211\5670n\CANh\1072150F\9559\a\133215\165806\NAK*C/\44946.)\SYN\aP\1107161\1043226\DC4\1087020\515\67972\DLEL\n\180263y5a\146153\54746Iy\11497a(\SIv\SO!GW#g4\EOTb\SI$(\ACK\niKxu\DLEQ>\1038539wGc_NKl\r\13222x\83063z\DC37\RS\1096948\\\NULB\vC\141810\GS\169437C?&q\1009432)+PhcHd\186025\DLEA/F\1035548Y\47461\14070J\1012685jIQ>y\2014\1058904N\98611y\SO~\26014@e\1061608&x\189240\1080205\"Yh%g\SYN$\1069145\1046629|\EOTT\EMP\1011180\1084918v\RS-.e8\SI" } @@ -213,8 +213,8 @@ testObject_PasswordChange_user_16 = testObject_PasswordChange_user_17 :: PasswordChange testObject_PasswordChange_user_17 = PasswordChange - { cpOldPassword = Nothing, - cpNewPassword = + { oldPassword = Nothing, + newPassword = plainTextPassword8Unsafe "\1046443X2cZf\tI\DLE.3\27153\41641\987805\SO\167150\31997\157768U\23766\159716\DC2\993933xy\1103378DB\1095912\USP!\8776\&3\46231\f\14600.1\1020378\1043279ji\135553C\95086\16967\37206\19099\US\NUL~BYB\RSUYc\ETX\1091112e\127528\187472\4411\"cN&\t.9\1098365?\GS^\v\SOH\b\CAN\41627&\13579\1108825a\1014432)z\62357\&6Z\179494\1092724\ESCX\SI\13823xJ\SYN\1428\EM-\DC1I\nB=\1040975n!l\131479~U\1069398;\113684X\187497\59277\EOT\159297\1023481uY\40199z\1054394D\1020153\fFbZtt\CAN:\CANYQ\SOHh\1006361W\1110330\DLE\168743!1}k\\\1055615z+\NAK\1106543\SOH\1094136%\17474?v\1108035h\fN\f\DC2\NUL\SUB\189591\996341P\GSbP\DEL\1107736>ie\1100530\7924i\168174-\30280]4i-\STX/\GSA\b&\v\1043901<\1102709\1106671M\\\991694-pG\FS\169333\DLEHEJO\a|\t(\9209D=x<\ETB aV\1012721O\999045n|mdg\1043448@\1110847f\a\1025181W\190988\19816\DLEh\166909\1092096\ETB~\10652K~\1072426\ESC|\rdi\GS\64637\94773\1081217;\1026647\&8e^\142140\DELT/D\US\NAK\983847hTTe|N8\1077575\&1\1092491tJR\a\155288iJ2\998006}\36187\28713\25201\SO\1109108\&0\2753!y\SOH1W\USzX\SOH0\991532s\119987h\78486\135733#\1074355\138222SR\988575,V\180455v\NAK\164938\&1g\SUB\ESC\97713\1081062\STXQ):\US/E'\131476\DLEz!\SO@\1020670Hy5*R\1010303\SUB\990422\1044281\1014588\1063943\178348\1062043`\49558\DC2.M\1113770W\171312\ACK\1024710Imf#dHF\ETBc\194659\DC3QcG\1070916B\NULW<\RSC\1059704\988425\1022019L7" } @@ -222,8 +222,8 @@ testObject_PasswordChange_user_17 = testObject_PasswordChange_user_18 :: PasswordChange testObject_PasswordChange_user_18 = PasswordChange - { cpOldPassword = Nothing, - cpNewPassword = + { oldPassword = Nothing, + newPassword = plainTextPassword8Unsafe "$\1043357izIh\65323E\152268b\fi\165052v=:\t9\1029608\r\10484!\1051779:\1003340&/#\1091275G\188407$W\990383>\EMDo`F\nY\EML\EOT\t\NUL5\996488bC8\5233Bq\1018037$p\NUL\v\9478R^\SYNGF&\1012032+]\156711]\22754\38792;:\131701\155917w\1065591\NAK\DC4\SYN\1060773\1015476gi\SI\"\vq\6329vV\1040593\DLEYya\1102677};,K3\DLEn\ETX\ACK\DC2+\184693\142191\SO^q\DC34+Iby-\ENQ\1053606\162697_" } @@ -231,12 +231,12 @@ testObject_PasswordChange_user_18 = testObject_PasswordChange_user_19 :: PasswordChange testObject_PasswordChange_user_19 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "k\ENQ\SIW\142801|YQ\999097H)\EMa\35968gXC|&\fE`\176817UQ\1096875\GS\1042874\ACKj\94562\142093\ENQc\t\1015620\SYN/8\SOHL\986768\&6\132434\1071731\34028\SOHy& \ETB\52652\SIf\1005119\&5\t\1060616K6A\a FxP\26949i\35802rc\18038\186543\172362\151462H\149276h[GU\nuX\SI%~I\184399Sv\r\DC494\DC3\SOH\989634E~q\DC2\990048\120529\tR\SI1$\NAK\ETX1\165481\1009573#\nD{\1034729@\1045950q\1036461J\97887\au\SUBB#4\EOT\8381\1087000\161668g\1011547q6(=\SUB\58393\n\13236\58038g%\SO\1066841l\1003446\1011686\997871\153172\NAK\f\CAN~\1051732qs\155291I0|\62022\SUB\161505\1084819\\Dq\SUB{z=\CANKL\53422\GS\DC4\1095233G7ewkJ1\35446J8 O\152777\96173V(\n\SOHuT\184493\142630\&4-\988150\&0\v#\1008772$qO-\SOH/T1\NUL@\53323\1012898\n2s8Bfh\"{vy\EOTG\28934\ETB\DC2g\NAKx\40967$\1111313:\1096564z\984205\r\1113615\50569\1016459\1089112z\1059587\62507U\992158ksD\DC2W,%\STX}\SYNY\1063541\EM\148916\1026506\SYNu\1068118a\DLExoH\b\96516ro^\ESC|\14524\137137\174774\&2\1015701!ReL.)\GS\995824a\134494\111281\38182\ETX\1055512\DLE\53907\DLE5?\DC3\988857Y:\1077940\t)\96370\48426\147806*\158714\1042527`\STX\NAK\FS\GSg\t\1084955*fM\994607\1029549,\ESCTL\STX\NUL\986074\1096953:\a_9i\524\168231\986631Wxh%\1104374`t\1062137i\139608mD\30436\ESC\18940\RSzJ\1014566" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "L`\EOT,Xp\US\SUB\DC39\SOH\986402+\ACKQ\1011739\163475\SYN(\126117S8c\EOT\SOH=d\152742\FSL\34501\EOT\US]\94933~6\DLE:\1038349\46131\RS\DELU}dYj\vm\DC33\ESC\EOT\ESC\STX\SOho\n[P)\EOT\"r\148842s\132918\&6\1013939\1054104X%g^\1111091MDA\GS=\131957I8\r\1059039\DC4-\1093354\95894\992259\155020\".\146604~-\24057]\bBLv\ACK-u5\1099612^st\26172;\SUBq\FS\ESCd\998793\&3\GSII\STXS\177535\DC20\SUBHy\1108265\18293>\DC1e4';\ESCv\f\SYNxF\RSWD\40069\NUL\15936WB\FS\145512/v\1094497\&9\SUB[a\1031802\t\n\187075y%\1065833\&5B,hyc\"!b#h\1092617XC\GS7\995391mZ\NULECj:O:\v/J\SO\1102347\996658\&9\EMs\DC4\a\1059269d>HEz\FS\171554/n\NULeC\1004734\CAN\65713\&2\181341\STX\ACK\1013277v\1000956\94105\986760E`xtrZWt7\164746wMA9r<\1021337\15097Ovo{\1112295\v#f\1040937\991008\SOH\63011j\FSb\r\1011414\v\FS9e\136229?\1019925q\1021008\f\172280/X\24799\STX\ENQl\FS\v\74972\131088RC>Y\ENQ\1073582\&2v\GS\ETB\US-\\,\1041777e\nf\1021970\GSA\DC1\DC4y\1007481\1102343q8\SYN#\NAK\984437\43846j.\n6Is\SUB\1049642\t\1020034tL\1049999\DELT{\173861\1059180Sz\68055\988553\EM\US[\DLE\48766\r<6CnyQ\DLE\RS146\1059541J\DC4\1059543ceb\NULr6(P\917894(\1072768ic\34855/\ENQ\50857\18315\&7\DC2^b\CAN\1000777\f\US#\15234r8-\154704u\r\1016712\SYN\NAKH\SO\985948\27600\1011459\"'\46452\ESC'=\SOH\19188b_\DC1\186563\SUB\174895x`\a\1041293\140522c\EM|\984810\aA\rQV?\1058487\fZ\f\ESC\SI0D\SOHQI\ETX\990028prt\163629\94675\36885\171880\1096809\&9\46899u\EM\1102387\n\13498\DC3}h\1032138o\DLE\1063962tFT\1095317%Az\1086440\&3\US\SOH\EM\38682'\DC3^1\14526(>E\DLE(\n\1066401z[Wg\1100054ad\1007846Mnv\8290\1091875|e\190345\\g\DC4\51159jIsn\DC2\16061\178290\&0\DLE>Lr*Q:\ETX\"\183845\DLE\98183\STXq\DEL" } @@ -244,12 +244,12 @@ testObject_PasswordChange_user_19 = testObject_PasswordChange_user_20 :: PasswordChange testObject_PasswordChange_user_20 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "{dNa\GSEIDDNi\"&P\"Dx~\96634s \NAK&\ESC\SOHe\917580,/}}@\1024844G\USRi\177540\vG\EOT\1068093\"dIcX\128456?\53433h4\RS8}E\b\fAI+\138835\DLEN\ETBg'l\DC4\DEL1lg}\1002968\GS9u\t=\186263)\1048038A)\ETBBD\ENQ\1741%\CAN3\b+,\151430G)0%\74936\78333t8\1105056\CAN\988091oU\DC2N\NUL\DC1U~\1100670[\138598\1110439\&6##\151597<\a\SYN\986482V6\vb>\NULh\NAKq\f\176602<5dHa\tg\DEL\24672\66025\&2=tZ\1050161L9M\a2k\1001329\987951vOkA\r)\r\60697O \63131lNli\34835\\\"b`G\52957\1039861\161828n\DLEP\1077887i0k\1015841w\1040786?\\\ENQg\1005909\RS[Z\SOHN\SOH\CAN\186595:\FS\185811\40960_kBD\"C\DLEB4]w\DEL/JF\NUL?L9V\\9\1096654W\1104044'\FS={e\153126>\1098415\139415D\1112130A\a7G\ETXb\983698Crt*Y\nhD\150279\&5\151537)F\NUL\ETB*\1035725yCu\ETX\SUB%\SIbZ; G\1079499)\SO\1012440_\NAK\DC3,~\175703\SO\153562d\1101051\1084728\&4\1018181R\1059397\19127\1099372\1004409^\161681\32886\&1\DC3\USn\1102891*!\FS|\r=\166562[ql\189334S\NAKr\ESC'\SUBE(\SYN\f{\1112073 |\b\50511\42582\155138\1009867E\NAK\139848\&4\151681\t\68617X\1000541\EOT\1104748.Z\1085819\177246\176778\DC4t\CAN\DC3\23081\&5HV\ESC:$e,L\STX\992003\&7R\1012763.wq\62951\24985:\60845\SOH\SOH\a\67714\8047\&0*.\1022795\1087787\120217P\r\b\167713\1096692\&5\147092\121232\149850\DC1\bsc-\1082366i\DC1\2721\183884\154420A\NAK$\190574jNR\917908\SI\120778\16684\989256;\5681\1057323\SYNRd\STXI;\EM\aK\20933\59636,\EM%\1073632\ENQ\1089709J\1061355nR$Spf\1093436Lsp\1046367[\\\1105079\97069y\t\SYNbC7}|\DLE\SUBg5@]2\1017800S4E\be[\1054254\&6\RS4\146792z\DC2d\nm\83369/JqK\SOHQf\1081923\1079670\&0\95005\SOHHa\1014928&8\111343\61186)~m\101024\RS\vG\SYNz(p\EOT\1052203V\f4\"^+?A|\1037820\n\340r\USF&\CANt\1037756r\tP\SYNDW\DLE2\DC1E|PgQ/\1055897\1034173P\rNH\SYNS\30936\1050463\29463" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "\tNYm7k;\985171|w~ue]St\52529[\GS\983717e\DC21\EM\SI\"C\1059834\&0\1003638!\995247|xiw\1027219~YT\57860+\ESC'\185609\&6\1010421(;\ETX!\b\1071987&Y9tW\984137\72988\GS\ACK\1083519\1086906\1107857&a,\NAK\31149\1088114y/d\1080408\SUB\169799\150046wOS\atp\1000950B\181672\ETBi\DC2\1090827\1080180\DELek=r\138679\60557\ESCrf\3126UkFh\ENQ9YA\t]\NULUJS@1?o_-P\ETBxW\171817\139732\48291<\1060487\133433&\DC4\SO6na\1000867!Z}'H\1052135w\r0W*\24217J\SYNIwk\9238AZ\1023004\30337\1013798w\1015506\ETX>\1080073S\158446\1061588o\190641\175249\1070034J\EOTu\STX:(\1066396\172284\1054181@\1030039\n\DC3xMJ\30746\147879Oxj@Np\1066698\1000349\1087808x\SI\ACK\US\988847T\v_w,)w7j\ACK\1046770\1038846\US'h\31697\&4\NAK\138144V\37643g\f\1099746\&3\129560\ETXR\SIPdc{a\STX\191154\DLE\ETX((\CANf\EOT\f\188879e~[+\RSg=g#&MQ\DC2%4\r\r\ETX\65235<\170329#\1109142\&5\36874\USv\bpt\DC3'\EMF\"2\1113106\SUBe\1087311$\1010352 \1068376bK>m>\f.\1052106m\64101MvQ\1065915Q\70336\177129)/\1056483<\CANy\995545J!\DEL#1\v\aq\DC2\1102215\DLE\CAN\1089020D\ACK7W\EMw\"\151987\&3\STXv\21304\126082\ETXxW\189371\1054427<^~\993642\r:WGlhl-!|W.\3598;n\1077840`<\CAN\1109050;NJi\DC3\53248\t ]\DLEH\100145_Z\996436\24307\"\185147\1002533\71437\24999a\DEL\US\1084155\132179\&4U\1017349v\1098626S\166457S.\36067i(\ENQB|VD\43028gW\"->N4\153954R\190825\992013\DC1\NAK\59376\20565%\160113[`\120495@B\168437qjKW\DLEm z\1034188\167428j\1029865P%\SI\98769._\r\DC1(N\990561\DC3\b\DC1\1072625e\41522'olW\ACK>\SYNp\988282H\RSe{\"RN\51331\ETB\DC2\">\1007951Q\DLEYoj+~\FSSMU\"ubD\142953KtW\FST\99243\20978\SOHQm:\RS8)g\1040404\ayZ\156789\1022349E\99162j n83Hf\163774\DC3\47323/2C\DC4\FS]A8-\1067911vp`\1033438a\47231\12226\182868n\ESC\"Xw$\aG" - } + unsafeEmailAddress "some" "example" } testObject_PasswordReset_provider_13 :: PasswordReset testObject_PasswordReset_provider_13 = PasswordReset { email = - Email - { emailLocal = "\994700\&5\ACK\132331!\1085699\nVb\1027357nU&\1037025u\169968", - emailDomain = "+I\176471q\1064856\SYN\1069753#A\163779\DLE}.\SOHu\1015059" - } + unsafeEmailAddress "some" "example" } testObject_PasswordReset_provider_14 :: PasswordReset -testObject_PasswordReset_provider_14 = PasswordReset {email = Email {emailLocal = "v", emailDomain = "\1090313"}} +testObject_PasswordReset_provider_14 = PasswordReset {email = unsafeEmailAddress "some" "example"} testObject_PasswordReset_provider_15 :: PasswordReset testObject_PasswordReset_provider_15 = PasswordReset { email = - Email - { emailLocal = "+\150753~\1073496VFc\RS\1102900R\a\ESC4J_\1087106I\f\1043823Dj\DC1\EOT\62142q", - emailDomain = "\1020153\138280n\1062475Gh?\vPXOO\v\1092723\DC2" - } + unsafeEmailAddress "some" "example" } testObject_PasswordReset_provider_16 :: PasswordReset testObject_PasswordReset_provider_16 = PasswordReset { email = - Email - { emailLocal = "]\1111436Dn\b\NAK\n\17695\167052\ENQ\1024236\&2\r\1069249\1002489\1038720", - emailDomain = "%L(\EM\1109782\STXk\EOTo\170961B\18655O*/+", emailDomain = "\48353"} + unsafeEmailAddress "some" "example" } testObject_PasswordReset_provider_18 :: PasswordReset testObject_PasswordReset_provider_18 = PasswordReset { email = - Email - { emailLocal = "\FS\1022850\1012117^3\68431*(\1037814\99655", - emailDomain = - "\1037557Y\ESC|=\137727E.A.\NUL\1002333K>\1067053cZZ~\CAN\1058810i\DLE.r\43079\1002153 \176978" - } + unsafeEmailAddress "some" "example" } testObject_PasswordReset_provider_19 :: PasswordReset testObject_PasswordReset_provider_19 = PasswordReset { email = - Email - { emailLocal = "x|\58643\1101318J8\1007195|%\142798'9\1089195\172026\1085440F\1098543xyP\1054659 4,", - emailDomain = "!]w6:\SOHd4t(\1103884\1052833$\SOHrl9\9929\120677t8" - } + unsafeEmailAddress "some" "example" } testObject_PasswordReset_provider_20 :: PasswordReset testObject_PasswordReset_provider_20 = PasswordReset { email = - Email - { emailLocal = "\39795\&2\SYN)=Xd\155177}o", - emailDomain = "4\SUB\188588\1054317g\NUL\1092307\984568Q`\\\SOU\1017696" - } + unsafeEmailAddress "some" "example" } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Permissions_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Permissions_team.hs index fd47570ce6c..2403aff6562 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Permissions_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Permissions_team.hs @@ -40,12 +40,12 @@ import Wire.API.Team.Permission ) testObject_Permissions_team_1 :: Permissions -testObject_Permissions_team_1 = Permissions {_self = fromList [SetBilling], _copy = fromList [SetBilling]} +testObject_Permissions_team_1 = Permissions {self = fromList [SetBilling], copy = fromList [SetBilling]} testObject_Permissions_team_2 :: Permissions testObject_Permissions_team_2 = Permissions - { _self = + { self = fromList [ DeleteConversation, AddTeamMember, @@ -58,7 +58,7 @@ testObject_Permissions_team_2 = GetTeamConversations, DeleteTeam ], - _copy = + copy = fromList [ DeleteConversation, AddTeamMember, @@ -75,7 +75,7 @@ testObject_Permissions_team_2 = testObject_Permissions_team_3 :: Permissions testObject_Permissions_team_3 = Permissions - { _self = + { self = fromList [ DeleteConversation, AddTeamMember, @@ -87,7 +87,7 @@ testObject_Permissions_team_3 = GetTeamConversations, DeleteTeam ], - _copy = + copy = fromList [ AddTeamMember, RemoveTeamMember, @@ -102,7 +102,7 @@ testObject_Permissions_team_3 = testObject_Permissions_team_4 :: Permissions testObject_Permissions_team_4 = Permissions - { _self = + { self = fromList [ DeleteConversation, AddTeamMember, @@ -113,13 +113,13 @@ testObject_Permissions_team_4 = SetMemberPermissions, DeleteTeam ], - _copy = fromList [GetBilling] + copy = fromList [GetBilling] } testObject_Permissions_team_5 :: Permissions testObject_Permissions_team_5 = Permissions - { _self = + { self = fromList [ CreateConversation, AddTeamMember, @@ -131,7 +131,7 @@ testObject_Permissions_team_5 = GetTeamConversations, DeleteTeam ], - _copy = + copy = fromList [ CreateConversation, RemoveTeamMember, @@ -145,7 +145,7 @@ testObject_Permissions_team_5 = testObject_Permissions_team_6 :: Permissions testObject_Permissions_team_6 = Permissions - { _self = + { self = fromList [ CreateConversation, AddTeamMember, @@ -158,7 +158,7 @@ testObject_Permissions_team_6 = GetMemberPermissions, GetTeamConversations ], - _copy = + copy = fromList [ CreateConversation, AddTeamMember, @@ -175,7 +175,7 @@ testObject_Permissions_team_6 = testObject_Permissions_team_7 :: Permissions testObject_Permissions_team_7 = Permissions - { _self = + { self = fromList [ AddTeamMember, RemoveTeamMember, @@ -186,13 +186,13 @@ testObject_Permissions_team_7 = GetTeamConversations, DeleteTeam ], - _copy = fromList [AddRemoveConvMember, GetBilling, DeleteTeam] + copy = fromList [AddRemoveConvMember, GetBilling, DeleteTeam] } testObject_Permissions_team_8 :: Permissions testObject_Permissions_team_8 = Permissions - { _self = + { self = fromList [ CreateConversation, DeleteConversation, @@ -207,7 +207,7 @@ testObject_Permissions_team_8 = SetMemberPermissions, GetTeamConversations ], - _copy = + copy = fromList [ AddTeamMember, RemoveTeamMember, @@ -222,20 +222,20 @@ testObject_Permissions_team_8 = testObject_Permissions_team_9 :: Permissions testObject_Permissions_team_9 = Permissions - { _self = + { self = fromList [ CreateConversation, DeleteConversation, AddRemoveConvMember, GetMemberPermissions ], - _copy = fromList [CreateConversation, AddRemoveConvMember, GetMemberPermissions] + copy = fromList [CreateConversation, AddRemoveConvMember, GetMemberPermissions] } testObject_Permissions_team_10 :: Permissions testObject_Permissions_team_10 = Permissions - { _self = + { self = fromList [ CreateConversation, DeleteConversation, @@ -247,7 +247,7 @@ testObject_Permissions_team_10 = GetTeamConversations, DeleteTeam ], - _copy = + copy = fromList [ CreateConversation, DeleteConversation, @@ -264,7 +264,7 @@ testObject_Permissions_team_10 = testObject_Permissions_team_11 :: Permissions testObject_Permissions_team_11 = Permissions - { _self = + { self = fromList [ DeleteConversation, RemoveTeamMember, @@ -274,13 +274,13 @@ testObject_Permissions_team_11 = GetTeamConversations, DeleteTeam ], - _copy = fromList [RemoveTeamMember, GetMemberPermissions, GetTeamConversations] + copy = fromList [RemoveTeamMember, GetMemberPermissions, GetTeamConversations] } testObject_Permissions_team_12 :: Permissions testObject_Permissions_team_12 = Permissions - { _self = + { self = fromList [ CreateConversation, DeleteConversation, @@ -295,7 +295,7 @@ testObject_Permissions_team_12 = GetTeamConversations, DeleteTeam ], - _copy = + copy = fromList [ CreateConversation, DeleteConversation, @@ -314,7 +314,7 @@ testObject_Permissions_team_12 = testObject_Permissions_team_13 :: Permissions testObject_Permissions_team_13 = Permissions - { _self = + { self = fromList [ CreateConversation, AddTeamMember, @@ -324,13 +324,13 @@ testObject_Permissions_team_13 = SetTeamData, SetMemberPermissions ], - _copy = fromList [SetTeamData, SetMemberPermissions] + copy = fromList [SetTeamData, SetMemberPermissions] } testObject_Permissions_team_14 :: Permissions testObject_Permissions_team_14 = Permissions - { _self = + { self = fromList [ CreateConversation, DeleteConversation, @@ -342,7 +342,7 @@ testObject_Permissions_team_14 = GetMemberPermissions, SetMemberPermissions ], - _copy = + copy = fromList [ CreateConversation, DeleteConversation, @@ -359,7 +359,7 @@ testObject_Permissions_team_14 = testObject_Permissions_team_15 :: Permissions testObject_Permissions_team_15 = Permissions - { _self = + { self = fromList [ DeleteConversation, AddTeamMember, @@ -371,13 +371,13 @@ testObject_Permissions_team_15 = SetMemberPermissions, DeleteTeam ], - _copy = fromList [] + copy = fromList [] } testObject_Permissions_team_16 :: Permissions testObject_Permissions_team_16 = Permissions - { _self = + { self = fromList [ DeleteConversation, AddRemoveConvMember, @@ -386,7 +386,7 @@ testObject_Permissions_team_16 = SetMemberPermissions, GetTeamConversations ], - _copy = + copy = fromList [DeleteConversation, GetBilling, SetTeamData, SetMemberPermissions, GetTeamConversations] } @@ -394,7 +394,7 @@ testObject_Permissions_team_16 = testObject_Permissions_team_17 :: Permissions testObject_Permissions_team_17 = Permissions - { _self = + { self = fromList [ DeleteConversation, AddTeamMember, @@ -406,7 +406,7 @@ testObject_Permissions_team_17 = GetTeamConversations, DeleteTeam ], - _copy = + copy = fromList [ DeleteConversation, AddTeamMember, @@ -423,7 +423,7 @@ testObject_Permissions_team_17 = testObject_Permissions_team_18 :: Permissions testObject_Permissions_team_18 = Permissions - { _self = + { self = fromList [ CreateConversation, AddTeamMember, @@ -433,7 +433,7 @@ testObject_Permissions_team_18 = SetMemberPermissions, DeleteTeam ], - _copy = + copy = fromList [ CreateConversation, AddTeamMember, @@ -447,7 +447,7 @@ testObject_Permissions_team_18 = testObject_Permissions_team_19 :: Permissions testObject_Permissions_team_19 = Permissions - { _self = + { self = fromList [ CreateConversation, DeleteConversation, @@ -462,7 +462,7 @@ testObject_Permissions_team_19 = GetTeamConversations, DeleteTeam ], - _copy = + copy = fromList [ CreateConversation, DeleteConversation, @@ -479,7 +479,7 @@ testObject_Permissions_team_19 = testObject_Permissions_team_20 :: Permissions testObject_Permissions_team_20 = Permissions - { _self = + { self = fromList [ CreateConversation, DeleteConversation, @@ -491,7 +491,7 @@ testObject_Permissions_team_20 = SetMemberPermissions, DeleteTeam ], - _copy = + copy = fromList [ DeleteConversation, AddTeamMember, diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ProviderActivationResponse_provider.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ProviderActivationResponse_provider.hs index d3ba47e5892..ae26a5ba72f 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ProviderActivationResponse_provider.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ProviderActivationResponse_provider.hs @@ -18,172 +18,140 @@ module Test.Wire.API.Golden.Generated.ProviderActivationResponse_provider where import Wire.API.Provider (ProviderActivationResponse (..)) -import Wire.API.User.Identity (Email (Email, emailDomain, emailLocal)) +import Wire.API.User.Identity testObject_ProviderActivationResponse_provider_1 :: ProviderActivationResponse testObject_ProviderActivationResponse_provider_1 = ProviderActivationResponse { activatedProviderIdentity = - Email - { emailLocal = "\16405\145084,kyz-\r\6937\1047584[\1099176#mh>6", - emailDomain = "c\EOT;QAjc\EOT2O\SO%\ENQ-\1003781\SUBn$\1009844\985973b" - } + unsafeEmailAddress "some" "example" } testObject_ProviderActivationResponse_provider_2 :: ProviderActivationResponse testObject_ProviderActivationResponse_provider_2 = ProviderActivationResponse { activatedProviderIdentity = - Email - { emailLocal = "\1089349\"K1\ETX;}\"n~X\134776\161302Fd\FS1^f\DELo}M\1053484q\v=\183432xU", - emailDomain = "C\\" - } + unsafeEmailAddress "some" "example" } testObject_ProviderActivationResponse_provider_3 :: ProviderActivationResponse testObject_ProviderActivationResponse_provider_3 = ProviderActivationResponse { activatedProviderIdentity = - Email - { emailLocal = "J\53673uv\EOT\SYN\NUL:XJ#\rA[\1063999\NAK6r\119313\DELA\DC3\DC3\65757\1003687p/\1081952\twP\1071823\RSCq[\DC4\62257(\1002708P]OL\191214\NUL\SI?\CAN\FS\DEL\62658\1067853O?*\133393\"~\95514\NAK5\DC23\993032\1062731GC \a:T\1086654|$r\1024226Q*US\119666\7973\990723\1092776\1012647\&2\SIXp\DC1l,{\53831$\1091822\SYNw\RS\1014066p\159782$6\1003029\17252\SYN\178493\&7\1094964]\141621\SIi\1073342%\n\SO9i\DC1\ETBI#\ETX\ACKz\"LJ+\f\EOTU\f!nSGq\1041642\1079338\b\n\SYN\58961\1100402\1107153vkoE\\>L\1071747\992957\&2\14662\61032V\USfCJJ\1041994\f\183187\DC1\141258\37968S=\1107082v\994620/jdg\1002901U\1025416s\tO\ESCD\DC2\"\1059656;\162790`C#\DC2\1073802^j^q\133762;`\1044114\1037819\DLE\986390\&0Q\1039253\188705\136022\CAN\1097897\&99\58156\v\132926\1080381\"\1015895\1101268\97449JW\DC3n\1048086\SUB'\ETB!\a;\CANF\1008408:\SI\CAND\61480Nhu\ETBvWC]i\1023609]>mM\96616\989899ISK+\97925\SOHm\"h8\30835\STX\DC43F/A\142221\1002286\98732M/\44462\1041696`\ENQ\1053777\22262k\FS+\t\1010757\DC1,6\a\68820D\1045784.:$'P\20749\1018853\1083057\166962w6V\"I~\f`\9746N2\DC3\SO\DC1S\1111933/\55133ZfjtU\GS\1022578u%\ETB[k\SYN\1038646s+G\EMh6\ETBk\1042066\STX\NAK\SOHi\1024430P\994456\DC1\999049O\a\r;;\72866\988084\DC1y\DC2\t`\ETX{n\CAN#fb\156098z\1089529@\148590-\134697\DLE_[)' r\SOh\ESC\1005694\&4\SIi\t\173183\1062912\SIEe\113729y\SOH\FS\26106U\DLEY\ETB[Wu\148140\1043600\a\1108631d\28497\b\1066901\\&F2F=L\rlk\NAK\1060365x\44894\n\1050464\1017030s\t\992979]T\\\1016800\1103758\177517z=^\ACK2)Y]E\NUL@\1024775_\1009598\"]\1027129\1018765\153761o0>Qd\ACK`GD\NUL\1013350p\51546JF\aw\FSCo\165594\DC2\ENQ\bq\\;\SUB\58807\986762to/Q/3\10426F\78530j\147563}yi_/\bj*y\1010612&\FS#KG\1040949\t{\USn\1009397\994198|\th;CJ|S\158628D\1100265rh\1043492\DC3\RS5VV\1017190ux9\SOu\141038?\1104936_\tx\DEL>4\994644\17535-d:y`*T7\166631_7\GS\SYN\36847bkb\EM\STX\1051731\DC4$\1022793i\NUL\178323\167679xUT\1003494a\ETX\134094V^\GSvz?;E\153708\67127\63995\n8X\183538\997398\CANot\r\ETB\NULV9>\126574`\58999c\SUB\66417W\161422\51315\120691rh\DC3&\139906\1102627RC\v&=2\b\1086617\ETX\994487)\8703\&7Uj\1029880A,ckL\CAN\155729o27\150661\ACKL\rucW\DC3\189366Bw_=\b" @@ -50,7 +48,7 @@ testObject_ProviderLogin_provider_2 = testObject_ProviderLogin_provider_3 :: ProviderLogin testObject_ProviderLogin_provider_3 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "h)\49363Z\1063824\1071314", emailDomain = "\186791\EM\19978\995909f\b"}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "K\3841|\1085726HU\n\\Qik\EM\US\153712xG\165429\&1g\37919\&9\1104489v4\1082951\b\ENQ\1067666\1085466\&2\CAN\1081359|\1008692\NAK&\SI;2\DC1\b\1002383A\a=u\SI\40099\tz!pwH1\DC4\SO.lexJ\RS\1057638\&3\EM&s\177278\DEL\1004232\SON\1093350\v\1051086\49256\1091462#n\RSY\1087974\DLE#%ObL=s\59640(\f\EM\985401\1080301y@i\CAN/o\nof}Z\121432V\NULDqD<\EOT\987137\92240 ;\\\GSJ)\ESC\170276Sth\STX^'\993179\994691\171357/ct9\"\ENQ$s*\1112139\983635T\1000238\b\CAN\1053269\1032292\&9Drm\74016$3\78461j\37449y*R@\1045490\1075417mGM\1024517^~\ETX\SO*\"2#\1095216S\FS\CAN|Y]\SOWy&N\159632Dw\f\DC3Y\54016%^+\1070488s\1082203\SOe\1078681\SYN\1038724\71339\51097o\r\184885z\"jn2\f'(B\SYN?s#*d\ENQ\FS\24664\n\160475\ENQ\1088778$f)n\25546}&N\t\1074142\"1\1079112u_QCU\SYNeR`\1011732N\ty\1049057\15798\DC1\1106895k;\1088300\144423t~JX=vN6\a_~C\US\1001142\SO\1089507?$b\1031649\EMA\SUB=\57375vm\n\EOT\1031498'g@\\\162341AhVW\37558\143758\131257\1048128\4146\vO)U\1042082\1030755\\Ly\39677eA4\41869\ni\994151\43752\EOT_\97713n\DC4\140878\\S%\50171\1080044e\CANu\998027\1051199'V-D\1028947X,q\DC3\DC4\169513\ENQ$\46447\64290.e2l\1061537\NAK\ETX{TW\n\52800\1329H\1049309\1059378\994850\1094923\SI<\NAKM;[d\DC4\b}y3Jt\100213%l.\fU\1043697H\STX-.#\SOH\f\151738!\CANY{\ETBd\152209[(\1040856]j\181307am!Z[w\STX'*Tv\174621PM~\1033877\&0\RS!b\128530\\P}3v\EM\63181n\1064827\&9F2gW\DEL4\139178\1022339\t\176600\147459\175596hR\ENQ%\37966z\984421\1013392?*\13832(#\EMr\DC4\br\161885WN[ N{\1095601\EMv\53960\SUB\989224\1100619\1054425\DC1[S\DC38\RS?\1056015\aA\US\1047760A\GSb\98984\1057798-\CAN(j\7084t\ENQ;}\SIHX\ETB]_,\1110377xVY\92234\EOTH\1009657i\95997\GSuu'>S\SI\SOH\28747\19442\62495\1029286\GSb\985600\161392/\EM\1073931\&7\DC1/X\DEL\DC4oZ\1003485\171281\27236|FN'\1088003\DC1\1095084wB\DLE]\48797 V\1075141\1063573\an\1099423\t1\1049162 [}C}gG\1112557#o\990395kF\ETX\bK\1066860g<\SIn\CANZ\95543$}h\US\SYN\147130\&5m\78875\1022687<\1024861\38580h\DLE\37612\983382\r#_\1081233fD\SYN`\1048444_{n~I\EOT#7\"K\v{\28291L\1076561\DLE\1028456\991117\28670]l x\US\1060025\180458U\\\ACK\137215\37941\b>\RS$Y\1105355\EOT\DC4\135173\159718\66296gE[Z\129159\1030459\SYN@\ETX\1089314u\8040\46827(.\ENQ" @@ -59,7 +57,7 @@ testObject_ProviderLogin_provider_3 = testObject_ProviderLogin_provider_4 :: ProviderLogin testObject_ProviderLogin_provider_4 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "\188360%\135755\169860", emailDomain = "wY0nE\45983d,"}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "\997376`h\134851[/\DLE\SYN\1057356\190778c\DLE\144700\STX$\1049974V\DC2}\"\1056292\&1\34169K\USC\183035\ENQ\DEL'\998099\&70\RSe\1032261\DC2w_\42155\SYN" @@ -69,7 +67,7 @@ testObject_ProviderLogin_provider_5 :: ProviderLogin testObject_ProviderLogin_provider_5 = ProviderLogin { providerLoginEmail = - Email {emailLocal = "\1090846\1076550\ACKf?\1064024\DC1R`", emailDomain = "\31290c?\GS\1008740bOP"}, + unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "\DELK\71213\EMr\52582s>qsz\1054321\1103761\14642\EM<*\1034130\37787\CAN^&\128177\&9N <\1092368YR.\163469A\ACK)u3\1077954\&6]\60462\133926\58248IwBZ\STX\991401\1006288mA}2\12958y\124971\168984\1071055\1032417/Q%E\bL<\STXI\ra/\1012873q-\50596 ]U:\DEL^YR0){\f{g\171878\ACKF\US<:\159079]IC\43618\99859\97648O($h\991036\DC1\SOHe\FS\SOHxQ)\1060127\1070870\177697\1050228OZ\1055218\NAK\"\989577/\DC3x[\153967\187729\DLEV\187019\99263\t\155326E)H\acIsa\CAN4\SUB\64144\&8\70723\ai\ETXDD4w\182368\tEjf\DC3h\1057977\1058648a\2001\ACK\30165\132313\118793\SYN<,\1000370+Q2'o\EM\60960\STX]\EM%\DC23PQ&H\1008877\1050884\SYN\US\1003442YR\1029695\18252\NULEx\v\SOH\110594\154395\132048O\US>FG\EM\6072\1035840(\185650B4,\161948\1082520\ENQ\1011783HUJ\SYN\1069998*\1100665w;j7\1041915\&1TJx\SI\1044958\1099495Pn\ETX*O\NULt>a_X|_MmL\6099\DC1\984250\985977:\1094973Cu[r>\1005272vp\DC4" @@ -78,14 +76,14 @@ testObject_ProviderLogin_provider_5 = testObject_ProviderLogin_provider_6 :: ProviderLogin testObject_ProviderLogin_provider_6 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "z_\1019380\DC2", emailDomain = "\180905VDG"}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "R\NAK\19239m\20399|\168697|&\DC4\54144_/\1079716\60856Te\179713" } testObject_ProviderLogin_provider_7 :: ProviderLogin testObject_ProviderLogin_provider_7 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "R\1107663\1101373Tk\47808\&4\DEL\fm1r", emailDomain = "`O|Q%"}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "AV$\r:\SO\1057925\twBe#aP\GSf\\D\1093131\f\"\1062474\DC4\1063531N\vW\1032165\151144; O\1095945\1039439\163822\1053248\996935\158292\180227*a\1032308w,9\11932\33469\1042359Q$\NAK\NAKpv\992954(Cl\ESC\"cg&2j\ETB\100514|\ENQv&e\48648g\58097fK'j{F\RS\174779zn~\26851a?\989074\ACK \26744Z,#\128914:/\1073971\999239/sGF\ETBi;{|\9210?np\24919_pCi;1j\1075816w\132426\1101926\RS&\1094263q$\DC2p\GSC\ENQV\r\179342g\SUBls,\166835d\SYNC \1003970<}\1090450)x>~\113696VV\1038818\ENQm\177584\RS\DC2\146064\STXykNo\1109305]\FS\n\nZ\ETXp\1093301\1040700\14783\70715oy!%m\1055994Pg\29043Mz\63458\151167\142629\ENQ\GSoiO\1079223R\FSHG(\155361\1043624<[nAlz'\EMN\aX^J-\33133\DEL\SUB`ubS2a\FS\1089953I\DLE\NAK\1066424u8rSJT\34653\983177\1103439|\b\997721V\SUB\CANK.(\126129\a\1111643\1099135y&t(\54546\139956Y@t\n]\rlJ-\65671H\SOEp_Nv<=^\37923\RS>A\RSt\NAKL\1083189\28040t\EOT\1021817|/\46641\NULs50\EOT\167880\1053339\SI1\1081864\a\1004866\RS\8114W\157166vsqz\CAN\32807m\986009\60083~j\1045359\1031943l\169109Zd\1030016\SOHlKy\NULITt\20709\184328\SOS(\34490K\45599*\NULn\28796^\188678\&0\1040248\DC3\1109095\149822\1084021\FS\\\22362f\1106493&N(*\151139\1032885\NUL~*_\NAK\1034617\1023597\&6s\1046400\41249z\NULs8!0m\USb\142489\\1lu2?>7x2^t3\54489L\1080612\30405|j[Hi\SO75&\EOT!\37099_pFu;'\7181(\169297Jk\SI;\n\93039@m\41290~\SYN\SI\38271\FS\1041438\ACK(qh%\"\SYN\ETBC\1089293C\63782\&8Ff,O\ESCks.q\38452J+W\994044\&3/\a\882Q/\127952&\EOTl\ENQ55]5\SUB<`\t\f[#\t7!YI\tei\66807\37932\&1yUS\1095848X2o\1030170\t\96997{\t\ETXOp\tf$B\STX?\NULxv\1029314P-u:CW?\1014394z\EOThO&\97914\179719\ESC\1045462z\DC2\178600\&7\SO\15990@|\171677\&5" @@ -94,7 +92,7 @@ testObject_ProviderLogin_provider_7 = testObject_ProviderLogin_provider_8 :: ProviderLogin testObject_ProviderLogin_provider_8 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "\a", emailDomain = "z\1065930\13842V:\178758\DC4\136826"}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "\1032807,+\26563:\1071780%G\NAK\997900/\DLE@\1027414\25655l7\DLEw\EOTe\ETX\USV\1028504i\1102233\tJ\1023546\188083k\143090Hz%|aG\1039292\r\52139Q\US&\GS\EM42\ACKC{t\16867]g\FSdO\48840`.\184346`m[\ETX\1077921-\ENQ\16213x\ESC\1101818N\142994\DLEK8\188217[\DEL\988988\1008523k|e\NUL\DEL}\SO\991947<\SOHg\1031754G\97218K|^\1095277\RS\167966M\168754/i\1093780!#\186388}\7777\EM0\1107848\&2 B^\DC1e@,I\SUB\1060988\93989v\1010096\NUL]\"c\138108\47542\RS\SOHB\DC4;+N\1108696UI:\\Zc\1066121zm|+{W\988550I\2530MU\EM\992874\n\NULH\ESC!n\1020509q.8{\1004748\162235T>\127905\1059100_ \ESC\NAKZ\EOT9\78187\990745r\ETB|\SOH\83522'#\3536\ETX{|=P\153911VTH\b\991886\8452'\95845\SOHvSk\1042204\52955\28694K#^\20633O!'_}\1093507\1043069\DLE\STXQp}\DLEfd\128876(\a;\1003531`_\a&\110809LI\GS\"\159092\\71\a\DC2F\998197\182925L\1070548a@\SYN\r\1076739\97129h\ACKP;_Yj\4138CH~V9\ETX\DC2M\SUB\1107806\1058529%5;\SOHd]" @@ -103,7 +101,7 @@ testObject_ProviderLogin_provider_8 = testObject_ProviderLogin_provider_9 :: ProviderLogin testObject_ProviderLogin_provider_9 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "?!", emailDomain = "\f\1047642v\30589\ACK\28844"}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "\191077R\tF3{7\986307Q\5004\ESC\189822h[\NULV_m8\ETX:K\r\1003166(\1088407\DEL\DC4\1066192\rKz\ETB\96897^o/&!6\SUBV\bU\8154-1n-\1022625r\ETX\35324F\a\1087954+\990349,\\Xm\ESC\789\1107982|\53584\21152\EM=\SUB\1049274fR\n\1028364cC>jhZ-\vBnq\ENQ\ACK\"\DC2L\60229\1089806\US;Q\1018560D\"Q@\1027316Qq\20765" @@ -113,7 +111,7 @@ testObject_ProviderLogin_provider_10 :: ProviderLogin testObject_ProviderLogin_provider_10 = ProviderLogin { providerLoginEmail = - Email {emailLocal = "N\1079983C\1019848\987758Q\1016550?\148085X", emailDomain = "\1069665\42373l"}, + unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "-=\32480w\ACK\990091YN\186686Mx\fz\20991\FSa\416e0ARM\167347\DLE{\8548A\r@3\16428\DC3\ETXgA\47834\&0g\147348\v\49080\1086233OQ@+\1007101kw=~Z=q\1075779\SOHq\179325\SOH\45786\1013252\1008755l\ne\1071386\1009919)Z]A\1012627_o\95076\146226\1045971Z;\18446M\132612\1112886\\\1088243b\r\50791\1020046%\39407vLCFZ]f\GS\t7\1096142uN? \DELY,hXW\16146\SO\8281%[t7R\48925S\n^\EOT\150600\191010J\1000079\tj;P\189612rw\DEL>@\SI\SO~ \48000k7\1102878{m@\1023548}D*i\t|\1007134\vF\1070537+\57561\SUB(qIOhI#\CAN\1009728\1008900\&7E\144838/\SIM_z\1028908\2400\11810\1691\NAK\n\US\SYN^)\SUBLVo\RS\50243\178287w\12126UeQi\ACK\12340\97806&\52880\DEL3S$$\179126\RS6\1077404\1067610*\131637Kkk&Ie\NUL{\CAN@}\66331L\92974\152099L-\r6U$3wC \993821\1009888yf3\1086000u\144879}}\1102943\&9\SOX\1103437t\1027564{\DC2m\1072289o\FSEzaDAZ\24534T\t1\DLE\37862\145559\NUL\75066\DC4D|t0'W\SI,l\aYI\SO\1011545\182577l,\SOH\1018137k\aB\1048822\SYNn\993483\aEjoDc\127837EhKM\DEL|\159141R\68861\t\ENQ\DC1q\SI\SYNC3\1011231\EMUy\vB\132997a\34076!8\ESC\GS\NULw\40648xEV\ENQy\DC36\1106820\18443_\184217\ETB\96220\&7f\1108082v0\38992\ENQ2*\1025120bW \1032288\DC4\140659,\18298zh\1100304rO\1027921\DC4N(\t\1041653\SO\48792,D|\1066254\998442)\GS;q8{\US3\FS)\1089425Z\STX\SOH\SUB,:htBg6Qz\1059787\1002112\1068685G\RS\DEL\41342\26517iS\172060\&8cdr\ESC;\DC2nf38\28682gl$9\ngdX\59459\NAK\1062568\1035354\1086375\144492q6~\DC1d\ESC\n\153700-5B\1038372/>M0\t\bVK \65825&\DLE<\161308\133305\&2Id\993601\&0\fKg\CANZF\164950rE\156086\1026024l$\119993\SOHf\1062528T(bH_e\"\7643w\45515\STX9o^\SYN\DEL\147213eM\ETX\188951-\181301\1111821E\999551\t\121125@(\46533m\148530\150405\986944Q\f~\1032957\&8Dv1\30512m/r\ESCYr\8210yq\SI \1084841x\96176\17805\149489\DC4\RS\157280zR\SI\1053172\&0\EOT\1034755\147571kF\43160@\1094412xqo/R\165394\DC3G\DLE\179117\10618\97627\SO@NL\1101566\DC30)uc\139710\&9\1000371\1002633\&1\DC1\f\DC4\1018005\1101074|\fN.\20166z\ENQr\SOH,;\EM\162767\136173\ETBa\145336\EMd\GSKvH\1003959\&3G(F\DC4)`\1092995O\1061374\CAN\SI\fqL\1094451\FS\tI@\SOH/k}`e~-\NAKo\120778S\1105769\3991h\92498^u>\992342\&1\143466%E\DC1o\182499us\1033582\RS\45417\1110073\SI\1051765\41403\fl\SOHt/\1027515U\190039\1045439\NUL\a\160207\RS\144945\ESCI\SOH\\\f\180467Ni\DC2=>\DC4#\121462*\DC3I\ETBO\SOh{\1085656\US\24409\FSw!MY\181626X\45991.\tf?a\17399\1051598=\SYN\1036417\1082173\&3><\1003370Iw\"\SI\CAN2'@29\DC2\50699\\\1077056\r\4318\\4w\983354%/6\1104193g\58587~.g\74148\50911\1086151 Au\ETBP\141262\170054\174447x\1094618\1080957\34978\1069200?\99187\ACKI :\STXZy$\EM#\1092580\146673jv\b\179001<\SI\EOT\159484\DC3!\1011657a\a\62678|\194723])\DC1\NUL]K\1038195\&2qo6,\100506z\37503\1108939Av\995584v\990741\v[e\CAN\DC24!)\a),@e?\32096DA\1356%%F+\1007987Oc\175553\a\r\1022239\NUL\EOTg4g*\8850AkKC\RS\SYNg>=\171781\&3\1039525\NUL\DC4\1086181p:2HH\137199\29594\DC3\148134\159816q\SIk%\188310\23312\141112\\a\120019E\30348\SI\183647Nr\SYN$&\1002603\1088350i\36041,\151865\FS7f\98027e\CANL\39708\DC2\999013>\EM\1032242N\48509\EM\DEL\1056374\&7p\1109882vP1?,c\1065156Xv\SUBN'\SOH\1000812lE\a\110963J\DLE\176674U\1017909kP\1014472`O\28714\NAKjjC\1046204 Me`06.\SOH\EMs\1108581\984997Z)\505Bw\NULzKZ\170904H\ETXF6yhu\111193\35302d+\120753y\1055023\1044971\1113599)\151175|\SO\EOT \1088176\986374,\SUB$.-7\145796a\EOT\1066588R5g\1058599rM\143676\1015936H\NULM\SYN\rT\174913Z\NAK\59354\993617\190813\US\1025870-\45910\ENQ\1013980~h\RSpV\NAKMTRyE9\ETBt\EOT\32355\184982\186747]$v\181164\182566ap\1039132; \1092510\t\"Ww\98795$\1068166y:`M\NUL/~Z\\eF6\1044984\NUL8\998267\83100\&1\NUL\12214\NULdaS\DC1\1057662f\74458E\vt\128426C\EM%Q\1066510\132734Xp*78\34762`\194697<\1078829dJ\1064424\1086026\SIb8u\147637\ESC3\166488F\1096009\b\13973\83061E\n>\52949q\vj\SUB\CAN\SO\1113037\984171Y\EOT\1078017X%\145648in\1014030w\f9\65731\1077451\1020021Ff\1059532c>\n$}\EM\25419Y\54667)\1095983\32915\1017920\SUBW+l\ab\62782\24910\DC4\145359FmR]|u\ETBkR\a7D\51919G\53611\164554@\SUB\36018\ETB/kt.\US{\1092794#^(zcVm\f\1094121'6j$[\ACK\ESC/VOQ\996687VLw\1067078#f\RSC\146914\97477@s\46339me2\ACKe\fs5g\US\144879\97542I$t\54653\ETB\SI\1073768&tPz\1010457\RS\NAK\150524\DEL&`x\1007249?:1p\61424\US\993411\RSC\157776\15121X\1051749\65224_ZZ\SOaTW\DC4\EOT\1110937$\CANp\44513|(\92589\&67\15667\&7\29934\1058652\98055zLc-Y^\1004000\f\34235\n\92226\b|\1044732\\\DC1\NUL\1068972\EM,W7`\183734\DLE\142014Jn\999320Efa\EM\NULJLwN" @@ -151,7 +149,7 @@ testObject_ProviderLogin_provider_13 = testObject_ProviderLogin_provider_14 :: ProviderLogin testObject_ProviderLogin_provider_14 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "q\100180\DC1 3%Y+RO\1022392\&1", emailDomain = "4`\1034127"}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "\SUB1\1011613\1053868\RS\DEL\GSG\1105681.\1071787T\STX\SI\128952:\165225\&8\ESC1aV\"\EMG|g|7\ETXP1\1113285A{_\44503\10934bufZ?\96371\53879\186363\DC3\1016903~g0\1011005B\186373\ESCR|\SO'4\t\ETB \1093146\67705\SYN\28861\40040\1058799\1095038I\1021393\&9\110714\DC3\EOTP+\SYNz@m\16478!-u}(\ESCG\\\187881\b|3\1032903\37872e\"\36203-\t\1095705\DC2" @@ -160,7 +158,7 @@ testObject_ProviderLogin_provider_14 = testObject_ProviderLogin_provider_15 :: ProviderLogin testObject_ProviderLogin_provider_15 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "\f9a\flw\NUL,", emailDomain = "i\DC2/d\158245I\ETX\\\150537t"}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "':\1006988%\52805|\986668\1030808\135993\17938\1004363\CANo+!4T\b\ACK\99223W~\1028898\151854\"u(4l4nH2\1101674A\16088-\1018022\24141\111131K3@q#\161349A\ETXz}\190078 \ETBr\59344\9426\1078379C\45259\SI|\1067233\DEL\SYNx\ENQ<\1018777\GSrj\1070063\SUB\189644\EM\ACK[\RS\137992!VP\au\FS92f\\\ESC\172837 ^g\1015290\13337t\fvS\1043307\t\ENQF\r}\ESCP\144296<\ENQ)kD\99644\NAK\1067207@P\158876-J\11318\NUL\DC2R\52639\61640x\1000441\17876\ESC{Y\fk\n\37226\58197\ab\1032034u6\29086?5\ESC`\SI\ENQ`_\158422\&2d\RS~61Sk\178580\1068936K\50191\DLE\1019284\1080388\1107195\ETX\8366\134653v\DC1+\FS\1108302c8\42659\2331V\1104718\ETX\DC2h9\17336L\48192\&7\97754\154294)u#\NAK,\DEL)vl\1014830\&7Ogx\RSS\a\173846L~'g]\67981xG\995706j^\1102897\127370ZD('\DEL\n\DLE\DC35\1112268\185079L\1096532\1075622-z\f]la\1092147=]n#a\1038542\30579\1083984\vCPCRM\1042106\146305=uh2:Z\ETX\34750E\1105620Y[N\DC3\ESC\1110568\1066309_7\FS\1059513\&7\986868\&4x\nE\67808\29850\63016S\29795\be6\b1 F\bl\v\SUB\1045323\152687\ESC\DEL\128863C\ACKvX'/4\1091841\23510\151941+\996124$;+\DC3\174286/r'\1027484\v\983594H\1106500_M\EMUU^\1077134X=\EM0\ETX\ESC\1082555DQ\DC2\14716\133147=rm5Bv*\69735\1021551\171583" @@ -170,7 +168,7 @@ testObject_ProviderLogin_provider_16 :: ProviderLogin testObject_ProviderLogin_provider_16 = ProviderLogin { providerLoginEmail = - Email {emailLocal = "\1067205bp\STX.=d", emailDomain = "y\15356\DC3\161068\41681\21426\1020089Zq\128566\143938"}, + unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "\1036973\DLEv<\94969:C\1007217\&3\SO1_!\ESCP\t\180873JW?\97294\1017846\1045977l\1091653=\65478ct}sUS\1071996\EM\EOT\SYN\2051|/HR~.\1048900/N\f\\\141892\&4\32647t=CY\179433y\DC3\1054140p}_\r\rhEa\1075791c\f3\STX^@\GS\129362@\STX[=\ENQw\DC1\1029934\DC1\101063~\1092617\96064.\1089637\ESCn\36147.\1087267]6`A]\1050790pA\t\121086\NUL+\EOTMu\157233\ENQ\SI\1043849\ETX?&\FS\GSX\1100944dh6Fp\4861\DC4i\136377\1066303\&3Z7@8\ESC{\t\EOTQ4g\145660\65499)I\65609[t\EOT9\1026387w3\SOF<\NUL0\1071913~#\20452m6\61319I\CAN\1105033\988334-\ESC:76\DC4\US/?suL\1059445)x~\ETXn\nHI@b\1101299iT\988713%{a\52797!%\95084Ke\58514y\1087193>\1008548h*\74260Rs\ENQ[!R\EM\SI\129448a4\50351b\DEL=r<\SOHAQy_u\986104\NAK\73693\DLEj9,\RS\1008176\1060001[\71236)\EM\27772O\EOT\1029715\NAK\1067872\SUBm\ETBm;~!Nqx\r<%?\CAN\STXK[KxY5M?\DC3nS;\1039064\1056961@\143038\&1lb\40219-\f\USJZ\997750Ki\td\1091304i\132307[\100579~$#\SYN\SOHg\1921\1064317g\SYN9];|\a\189928bR\CAN\54684\143272\1035894\35517AlfL08\121168\DLE\US=.0\b%yk6n\169325\ENQ[\\\NAK\1032731\45474R\189972\179614[f\1038013\&5\NUL4f\13023\176583t\1100811\155909*\r/V$\1038174\44770\&8<\66811\132407-\b2\1066405\&6%f_-\1109010\&3a\ESC_\1110891\\u3XZ9 \187801Y5\1059719(\1006889R\b\1008505f\DEL\b\1031245z}\b\bY6UOz=\5767\DEL\1043399W,\1111327\DC4\1044326\66036\SYN>\1059628\92344\CAN\1114008Z\1076807\1019237g\SUB\1084387Z\t!Q46\DC2\68818jM\ETXc\1054316EZ\DEL\a\95416rcDK\SI\STX9\11372\1079523\bs\CAN}:\1101964\52216!gp\NAKz\NAK\119927\SO\62276L\1029468\no\97894;*E\a\15680gST\bj~\1071090d\1100387V\1015961Qf\1068607L\SOi\fY\190596\&6\GS<6N\"\a]\DEL,s9\1096598\1070844M\SI\990526k\US\32548E\1088460\RSTJkEc\FS\1012905\&4\1068530QE\1012911\24946\&8\92573\985406'\147964\SYN\1087141\a>?\v\a\r{rW,\1037280d\ENQ$2FA\1056946\USwL\30127!\993861\DLE\RScf\140888\NAK)uSz\1058795O#B3;\1035768rC\STXDA5\"\ACK \bj\DC1^\ACK\146554i\EOT\1108759BM]\CAN!\1111593\189909\rA&\r;GTE\CANz\EM6tQu\136534Rn\tT[\SOH\FSQn>\24878L\92573\1104026\FS#\180044&n\1033900\998864\ENQK\NULH#\DLEnpu\1057804Bt{X\FS2je@5\1047027\DEL\1035261\1071250;\"\1100173Q\n\99865\1085440\55133\DC1U~@:\US~\38191\nRLiD\1007697q\994233{9d\128484.\54287\NAK\b]HlZ\153287\t\26577\51191\CAN{3\1029087E;F8G0g\60158\1022972&'\1023598C\1054219\DC4i@v\CAN%[_g\1000893\&5\1087835+;.DtH\2792z|\1051376\1045157\DC4\1052468Z\fS\1029176\r\128163r~r^[\170410\17975o\SOH\25519_H+;\GS\127793\a&m\984047U8f%\FS\17902\37532Z\1029865\&8},P\1080560H\SUB\b\1054328H^\134222`gRf\133942\1092609+V\DC3\FS\SOH\128429#" @@ -188,7 +186,7 @@ testObject_ProviderLogin_provider_17 = testObject_ProviderLogin_provider_18 :: ProviderLogin testObject_ProviderLogin_provider_18 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "1+dHm*\71181b", emailDomain = ""}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "N\155960\EOTC9\DC4\DC1\n\170284\35034\22589\DEL/p\1091599\&8\vg\\\nYGz|/\aNd\1029498\57517\DC2%\53814o\SYN\SI\38380r\1084853\987326d\1109120o\1093352\DLEl5&}i\ESCg\1104181\6571'`8N\1097917@\136616\1008516Q[o\\\f\DEL\1104293\5068\ad~\42876\ACKK\42883snr]pLA\ACK\1069066\ACK\73854Xl,\DEL;\DELwz\EM\998652\1069068\SYNc\NAKA7q\DC3\GS\53763ft\183331f\182272\\\1016358\1107081\SOH\4988\&9F\1036590\156517D\1066242\NULz\1030228\151834\&4\r\1072124\1071359\1036618\SIO\CAN\EOTY9\48690C0|8\168539]n\140134\SI\1024618\60889ra\ENQ\1076182\n]J\GSB\62239\64626\ETXgT\95776P\EOT>t\64744\a\CANr\t\1009997;Yy\NUL\EM\1071471Qz\SUB-'\ETB*\SOH;J4]9\182652\ESC\137920[z\40314=cpG_;\EOTu\1078222ic\EOT\vkz\SUB|}\1011861\v]\20796G3\f\f0\DLEd\v,\1080412\17244[#\57647~8OG`n)\1105707\&5\143076\SYN\67623wK\SOH;\DC1+[\1090978'\1013253\3118\SO\27597-\tB\1061075h\vVl\n$\RS\ETB\ESCw^!\DLE\1104169\DC3\986109\&3j\FS\149241\&8oj\96002\DEL\b\1010145t4>:\35038_\EOT\CAN\135636F\1027492lup\ETX+n\SOHP'yV;:\170481V\DC1~ 1\1007357\1103037A-\"&8}\t\DEL=\EM\SOH\NAK\DC3F\986041\1097800\NAK\54547/BI\5055,#\EM,\1036876gp\RSw][\DC2>\v*b\53753,\SO\EM\STX\69989\325CuTd\178982\&27B\1017690oRm$\157877Y3\n'?\SI\v\nf\175460K\SOH6\ESCl]`U)m\18666" @@ -197,7 +195,7 @@ testObject_ProviderLogin_provider_18 = testObject_ProviderLogin_provider_19 :: ProviderLogin testObject_ProviderLogin_provider_19 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "W\1036612`)SR\STX,Zw\n", emailDomain = ""}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "yMl\64761\&0~\1050682\1021006a\1094043\fQ\94632~\\y\1074075\EM\ETXrqFsvG%So\ACKaTH,\f\STXP\100229\"\DC4o;UE\a\SYN\54275\DC2r`\vu1)2\DC3j*\"c9\988223\1066302)h#\164540m*j\DC3\44782~v+@\v\n5*ui\17305uS\1003317\1052173sqM\STX\1032754\FS\187251\156456fG\156207tv}\44129?V\r\1051429]\1014021\ETB\DLE*E+V}\155873\&9\EM*\1015779[o\17882\1106946\vr2\a\1067600\ENQ \47429@O\30164^\1059677R\42526!HLT\1018705.\1046981\1079471\GS=+D\160272\&4|\1023902T\NUL\111266\ETB\1028696\1054400w\1093841#\1038538b\EMF\a3k\CAN*#\989166\&3YKs\DEL\ACK\SUB\ACK\140776D\n\1089340(\EOT\1083765&/u\13397W@)pLY\987574)<{\GS[LkCU|\\\t\CAN\DC3Q\SI\GSS\DC2\1082373\1055041\STX\DLE\1005747\&3655H%\ETB\1002444\\k\150559xl\GSAf\CANy\NAK\1398\157796\1004189\auH\ESC\60829y0(\14035\986900q\99290\ENQ\1015592\SUB6l\GS\1101212H5\SIX&a1j#e\EOTgf6\1099735{\1100353\ACKa{\r\1075038\SUB\1021868Ih\v4\62793\13891\1065696\aSq\54460\135283B #\1022849\984792%1&\5746\1101604\NULW\NUL\1005888\v\12495,@p\996704_s\1005516\195083:\1053163\26817\&3m\1073977\v_\1030132\SYNL>\FSq_1\ETX\1014033I6L\ENQ\78160\995216\994139Y\64467zH\USu\1060134\1008128n\SYNvA\ETBG(\t~\GST\38349N\US\1055431bju\RS\164557Z\133252P \4977\rq\ACK\"K\SOH\DC4\aqcJ\SI{\171894\1057036K\NUL\175514F\1063815,\34846\SUB\ETXW\DC4\49752\ACKN\EOTLAE#\1052903hIWO\vj\125250:cP~q\1007541\ENQU4\32477\2386*z\EOT){\NUL.T\1065030=\176972\188734\1112137]\n\50074\&4s\f\1011957\140326+\52789\"Z\DC1oN2\174181\FSoee\DC1wD\FS\SOZ\165295\&4\b\NUL\r\917953\137757\DC4:7\1034978" @@ -206,7 +204,7 @@ testObject_ProviderLogin_provider_19 = testObject_ProviderLogin_provider_20 :: ProviderLogin testObject_ProviderLogin_provider_20 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "#\4241|\18697\1075733", emailDomain = "\\\ETX\1091078A \ETXDT\r<"}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "y\SUBWK\1113065\US:d\996680\&1\1043738d\n]\29018\SOH\1033033Q\SUB\DLE\1049295\CAN\984477#7$\1067481'i2/%u84\RS*\989691\1039072\&4^BB?Ox\41256sW\r\1111532\&2\NAKF.\1058002I\13200\&8\STX\ETX`\GS!\1051949<5Y\1053480d\1029498P\1085303P\1110913?1\1100218n\NAKa\191218-\169668\9581\SOHc\FSne_\1089495\1073819c7\9454Yoe\NUL_>\178568\NAK\NULN\120250\1017029J\SUB\9102OkZ~yV$\SOHR\NULQ\1084924[\1072369\1103823?\RS\23331\1006369t`\3663}t\58854^p\GS\20579\1083297qA\bR^\54231\1070382f\132642qZr\ETXK\ENQ1\SUB$\DEL\1048222\1041456\1057367\1015965\v_\63061C\SUBb[g\156231i\1063061j%jZ9L\SI6\1092470l~\t{}>N\1028038\1093894;\CAN\1012890#\DC3:\f\EOT.\1034408|\999878%iMr\132269:\ETX\r\1059952\ETBB\1030258;\SOT_KJ=b0\1002499/u\SOq\fcV[h\146171}`\NAKN\DLEP\129031H\CAN:F\49767\988419+\1037039%2}|s1\49683[Rp\SOHO\t_E\rK\SUB|\SOH\DC3x$\1020540\1083269.\64699\ESCZ\69926\SOHU`\t^\t0'8\"O\SUB\1048972\STXnbGo3_\1056648\1083755\995789\SYN.%\RS\30271\1091646Q\GSD:gJ=Q-0\1113951q\STXx\ETXr\ENQT?\ETX+$\29558>\1103438\16152\396\f\146489Qj\SI\1067862Fs.\SO\118908Y.\1023882J\33637\f\154768A\156772\5117P5\FS\ENQ\118931\SOH\SOH\1059107\1064213\17324\1061449/x?\SYNI\61424\185549\NAKU\1007536\14119Ig\145126L]^.\166857}&(\188383\188556vGg3\1026179\SOl\EOT\142432\RS\fs\164737p5LS\998282\SOH|\nvm6#\1103808\1103698\GS\DC1\RS\b\157935\1006286\SOH\ESC\184018\1053117bE\RS\ACK`wj#\1014169>Q\US\1051484ss\150964\1092969ck%>Q\3766B$\STX&\28586%\UST\US\ETB:B\ESC\1025229\bC\185625h;O:?\v\1035263j\136984j\1011335;t\1080482\t\SO@\4855\1035237p\190479\a\996213\15858@,\169117duBfR\DLE\72879!\ENQ<\DC2\SOj\1080800XFs\1037447'W\NUL@SDA\1088037" diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ProviderProfile_provider.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ProviderProfile_provider.hs index d94ab7f9b68..6f4d98b170e 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ProviderProfile_provider.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ProviderProfile_provider.hs @@ -54,7 +54,7 @@ import Wire.API.Provider ), ProviderProfile (..), ) -import Wire.API.User.Identity (Email (Email, emailDomain, emailLocal)) +import Wire.API.User.Identity import Wire.API.User.Profile (Name (Name, fromName)) testObject_ProviderProfile_provider_1 :: ProviderProfile @@ -63,7 +63,7 @@ testObject_ProviderProfile_provider_1 = ( Provider { providerId = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000700000006")), providerName = Name {fromName = "\46338\DC4"}, - providerEmail = Email {emailLocal = "OR\32966c", emailDomain = "\RS\ENQr"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -94,7 +94,7 @@ testObject_ProviderProfile_provider_2 = { fromName = "?\1050399\62357\12541$?\150548uTY\1101349fH\ETB\STX\ENQ\b\DLE%:!Y\ETB\92301\53905\1096036\1012090*]/Z\1050093>-\EOT\1041175\1025575!_*--7\SItEg\t\1028966\DC3\1079962\CANvE\DLE\134924?=\SO\1026118\40813\167977O\24641k\NUL\1019104\32399.\SI" }, - providerEmail = Email {emailLocal = "", emailDomain = "_ \1060474\990125"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -156,7 +156,7 @@ testObject_ProviderProfile_provider_4 = { fromName = "\SO\1046147n\1016911\&7f\1077840i\SI8|\STXe\nN~$[vAU\62541r1`\NAK\f/\b~\1084745PEhV={\1037388\160696\f\EM\1063647}}\3137x\994880\994942\1069553%\foA\50458\98884~t\182452\12080\t\1073906\rWA\186565\1104351t" }, - providerEmail = Email {emailLocal = "h\161768\t\1097554G", emailDomain = "\134955/\DC4"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -183,7 +183,7 @@ testObject_ProviderProfile_provider_5 = ( Provider { providerId = Id (fromJust (UUID.fromString "00000003-0000-0007-0000-000700000003")), providerName = Name {fromName = "\6923gr\n\35429-\37180f\fJ9\RSl)\f\20518_H^Xh\bA;O|"}, - providerEmail = Email {emailLocal = "%>", emailDomain = "\1075658\17096q"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -214,7 +214,7 @@ testObject_ProviderProfile_provider_6 = { fromName = "\DC3 &3\DLE\n\163723'\65487\&7\7618\ESCEwP\\\125089\DC2^\"\1023814\1002704\DC3\DEL-g\29654<\v\4324hAjOZ)\1045139W_\154260\135873s|+\1030412\"~D\1039156C\DLE\ETX\95249\ETBw\SOH\DC191\"\"6D\b\DLE\NUL.PC\RS\SO\1094846\1044317<\171750iuN\182436\1088261U{wgq\FSD\v\1034790\SUB\"\nw{Rl\ACKUa3\RSNx\SI" }, - providerEmail = Email {emailLocal = "\142265.\EMk\1035106", emailDomain = "n}\190773\n"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -338,7 +338,7 @@ testObject_ProviderProfile_provider_10 = { fromName = "\1027347|\US\187412-C:\v\CAN\1007173;|\DC3d\ACK>@\95987\165903\&2\DLE\138359\SUBM7/\1069218b\ACKO3m[{" }, - providerEmail = Email {emailLocal = "gd\1046608\1072562e", emailDomain = ""}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -369,7 +369,7 @@ testObject_ProviderProfile_provider_11 = { fromName = "T\35190nJwq\65943[\FSV\DC2I\179267\SOH\ENQJ:\"ay\1021260\998962\1026006L\SOH&%lT[l/?\1044443_\DLErW\1012807\1017169]\137723\1082379\83105wM\DC4#\39095" }, - providerEmail = Email {emailLocal = "", emailDomain = "W"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -400,7 +400,7 @@ testObject_ProviderProfile_provider_12 = { fromName = "h-\rE,\148173 \1088186t\DC3S\EM\14287&8Nf\EOTE$;;\163703\SO\SYN\191282D,pE\STX?'X*\STX\DC1>&\1103170WCGM=Ey\1088250,\44485$" }, - providerEmail = Email {emailLocal = "Z\148819", emailDomain = "!\1045867\69665\f\23358"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -431,7 +431,7 @@ testObject_ProviderProfile_provider_13 = { fromName = "r\1096559\DC2%_izF\33135e\46380\DC1/\r`u\1022998\a\SIf\60524\1098075f\1073391oP\EM&\131116\SUB\1059302\1108967jY\992453\1111715\ETXd\1063946e\1001823HK\129359\ETXy\1106634TE\SOH?\148357 \ENQ\ESC\1015779q#\SO\"(Q^\DEL\183337&\SYN\18804\DELPI]Q\"X\SUB\14938\145510x '" }, - providerEmail = Email {emailLocal = "\SOH#\134551", emailDomain = "u\SO\19353"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -458,7 +458,7 @@ testObject_ProviderProfile_provider_14 = ( Provider { providerId = Id (fromJust (UUID.fromString "00000007-0000-0006-0000-000700000007")), providerName = Name {fromName = "\SOH4\ENQ\ACK>\rx~J$k!~\t\DC14\985222\DLE\ETB\r\ETBy!9"}, - providerEmail = Email {emailLocal = "<", emailDomain = "M\SI"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -489,7 +489,7 @@ testObject_ProviderProfile_provider_15 = { fromName = "uU1;\34969b \ENQ%\1090011\51919\64324\&2f\1054192\b\1076489\DC40({\983593\1096114\997924}z\168790Lq\STX5.\STX\1092385s\1024579[\t\CANE\67601\95200W\1105521'\1036690_\1103544\&6g\a\160335\1033905X5j\1041586Q\100988\53621o/<\DC3wm\1069822'\135972F=\167089d\164390da\1010656\&6fbnN\ETB=\1062861\ETBx\v?{3\46096u\154857K\153847\26427" }, - providerEmail = Email {emailLocal = "\2355\CAN:c5", emailDomain = "G\1049614E`"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -520,7 +520,7 @@ testObject_ProviderProfile_provider_16 = { fromName = "sl\n\1104119\SUB\989977\a\143145\1017719>\DC4\SO\1016279\160098\&7\DELm7\NUL\35472\RS\54106uXwwA\1062534!\156472\EMN\1082758\164617\5214\ESCEZa\1030079\186679ZWY\189148|&\21785ikM\SUB\1061267\1056212\160249\988858\1020580K\SOHY:\ESC*Wzc\"\ACK\1038549\1092558\rWB8jSl\GS\EM\1003586\23627\STXf`\132324LN\nje" }, - providerEmail = Email {emailLocal = "\180899", emailDomain = "\34121z\16843'"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -547,7 +547,7 @@ testObject_ProviderProfile_provider_17 = ( Provider { providerId = Id (fromJust (UUID.fromString "00000008-0000-0000-0000-000400000006")), providerName = Name {fromName = "\37146|_;\1090300\48254\STX4/\13124yqDttZ\SUB\1065843y\17715\177370"}, - providerEmail = Email {emailLocal = "X\1050408J1\SYN", emailDomain = "\1024482%"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -578,7 +578,7 @@ testObject_ProviderProfile_provider_18 = { fromName = "~\US;@\1081284Oa\184911\DC2\SOHnw0\DC3Y\1044296.Qn\1111681\1078852\na>\ENQ<\1008904g\DC3\1017402x\1051129*8-T*\ACK'\NAK[/\1043140g\142008VT\EOT\12290,\179242\1069014kC\98612s\DLE+\GSz\78289\1040663`l" }, - providerEmail = Email {emailLocal = "", emailDomain = "\bU\SUB\62879\58674\148214G\FS\DLEK\44599tO\1109580i\GS_v\\\CAN\1018104\DC18 --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Test.Wire.API.Golden.Generated.SendActivationCode_user where - -import Data.ISO3166_CountryCodes (CountryCode (AO, BB, FI, FR, IN, MU, PM, VI, VU)) -import Data.LanguageCodes qualified (ISO639_1 (CU, DE, DV, FI, GD, GN, HO, HY, IU, KK, KW, PA, TG, VE)) -import Imports (Bool (False, True), Either (Left, Right), Maybe (Just, Nothing)) -import Wire.API.User - ( Country (Country, fromCountry), - Email (Email, emailDomain, emailLocal), - Language (Language), - Locale (Locale, lCountry, lLanguage), - Phone (Phone, fromPhone), - ) -import Wire.API.User.Activation (SendActivationCode (..)) - -testObject_SendActivationCode_user_1 :: SendActivationCode -testObject_SendActivationCode_user_1 = - SendActivationCode - { saUserKey = Right (Phone {fromPhone = "+77566129334842"}), - saLocale = - Just (Locale {lLanguage = Language Data.LanguageCodes.CU, lCountry = Just (Country {fromCountry = VI})}), - saCall = False - } - -testObject_SendActivationCode_user_2 :: SendActivationCode -testObject_SendActivationCode_user_2 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "\1021635", emailDomain = "nK"}), - saLocale = Just (Locale {lLanguage = Language Data.LanguageCodes.DE, lCountry = Nothing}), - saCall = False - } - -testObject_SendActivationCode_user_3 :: SendActivationCode -testObject_SendActivationCode_user_3 = - SendActivationCode - { saUserKey = - Left - ( Email - { emailLocal = "#\ACK\1103236l\1069771F\147486", - emailDomain = "-\DC32\1101045\&1\DC2\1014718\167922\SO\68149" - } - ), - saLocale = - Just (Locale {lLanguage = Language Data.LanguageCodes.GN, lCountry = Just (Country {fromCountry = VU})}), - saCall = True - } - -testObject_SendActivationCode_user_4 :: SendActivationCode -testObject_SendActivationCode_user_4 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "b", emailDomain = "4M\1076452P\149723$[\DC2j"}), - saLocale = Nothing, - saCall = False - } - -testObject_SendActivationCode_user_5 :: SendActivationCode -testObject_SendActivationCode_user_5 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "test", emailDomain = "example.com"}), - saLocale = Nothing, - saCall = False - } - -testObject_SendActivationCode_user_6 :: SendActivationCode -testObject_SendActivationCode_user_6 = - SendActivationCode - { saUserKey = Right (Phone {fromPhone = "+38093636958"}), - saLocale = - Just (Locale {lLanguage = Language Data.LanguageCodes.DV, lCountry = Just (Country {fromCountry = IN})}), - saCall = False - } - -testObject_SendActivationCode_user_7 :: SendActivationCode -testObject_SendActivationCode_user_7 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "B+l\1054055\1082148", emailDomain = "\a%"}), - saLocale = Nothing, - saCall = True - } - -testObject_SendActivationCode_user_8 :: SendActivationCode -testObject_SendActivationCode_user_8 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "\NUL3", emailDomain = "\59252g\155998\11926Ea?\DC2\\\DC4"}), - saLocale = Just (Locale {lLanguage = Language Data.LanguageCodes.HO, lCountry = Nothing}), - saCall = True - } - -testObject_SendActivationCode_user_9 :: SendActivationCode -testObject_SendActivationCode_user_9 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "Rn\STXv", emailDomain = "(\NULN"}), - saLocale = Nothing, - saCall = False - } - -testObject_SendActivationCode_user_10 :: SendActivationCode -testObject_SendActivationCode_user_10 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "\t\1040376\NUL2\160662t\152821", emailDomain = "^s"}), - saLocale = Nothing, - saCall = True - } - -testObject_SendActivationCode_user_11 :: SendActivationCode -testObject_SendActivationCode_user_11 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "rT", emailDomain = "a\tL\DC4"}), - saLocale = - Just (Locale {lLanguage = Language Data.LanguageCodes.HY, lCountry = Just (Country {fromCountry = BB})}), - saCall = False - } - -testObject_SendActivationCode_user_12 :: SendActivationCode -testObject_SendActivationCode_user_12 = - SendActivationCode - { saUserKey = Right (Phone {fromPhone = "+6599921229041"}), - saLocale = - Just (Locale {lLanguage = Language Data.LanguageCodes.VE, lCountry = Just (Country {fromCountry = MU})}), - saCall = True - } - -testObject_SendActivationCode_user_13 :: SendActivationCode -testObject_SendActivationCode_user_13 = - SendActivationCode - { saUserKey = Right (Phone {fromPhone = "+260369295110"}), - saLocale = Just (Locale {lLanguage = Language Data.LanguageCodes.KK, lCountry = Nothing}), - saCall = False - } - -testObject_SendActivationCode_user_14 :: SendActivationCode -testObject_SendActivationCode_user_14 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "B;b\164357\DC1\SIHm\DC3{", emailDomain = "?\64159Jd\f"}), - saLocale = - Just (Locale {lLanguage = Language Data.LanguageCodes.KW, lCountry = Just (Country {fromCountry = PM})}), - saCall = False - } - -testObject_SendActivationCode_user_15 :: SendActivationCode -testObject_SendActivationCode_user_15 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "\1024828\DC1", emailDomain = "t=\69734\42178\1032441,AG2"}), - saLocale = - Just (Locale {lLanguage = Language Data.LanguageCodes.IU, lCountry = Just (Country {fromCountry = FR})}), - saCall = False - } - -testObject_SendActivationCode_user_16 :: SendActivationCode -testObject_SendActivationCode_user_16 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "O_\37211\1022996^t", emailDomain = ""}), - saLocale = Just (Locale {lLanguage = Language Data.LanguageCodes.FI, lCountry = Nothing}), - saCall = True - } - -testObject_SendActivationCode_user_17 :: SendActivationCode -testObject_SendActivationCode_user_17 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "T\vI9H}C\STX\SO\1017900", emailDomain = "\151457\35555=N"}), - saLocale = - Just (Locale {lLanguage = Language Data.LanguageCodes.PA, lCountry = Just (Country {fromCountry = AO})}), - saCall = True - } - -testObject_SendActivationCode_user_18 :: SendActivationCode -testObject_SendActivationCode_user_18 = - SendActivationCode - { saUserKey = Right (Phone {fromPhone = "+715068856505655"}), - saLocale = Just (Locale {lLanguage = Language Data.LanguageCodes.TG, lCountry = Nothing}), - saCall = True - } - -testObject_SendActivationCode_user_19 :: SendActivationCode -testObject_SendActivationCode_user_19 = - SendActivationCode - { saUserKey = Right (Phone {fromPhone = "+22888251856"}), - saLocale = - Just (Locale {lLanguage = Language Data.LanguageCodes.GD, lCountry = Just (Country {fromCountry = FI})}), - saCall = True - } - -testObject_SendActivationCode_user_20 :: SendActivationCode -testObject_SendActivationCode_user_20 = - SendActivationCode {saUserKey = Right (Phone {fromPhone = "+8943652812"}), saLocale = Nothing, saCall = True} 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 c41eb5bd45a..4171affd0c3 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 @@ -22,7 +22,7 @@ import Data.Json.Util (readUTCTimeMillis) import Data.UUID qualified as UUID (fromString) import Imports (Maybe (Just, Nothing), fromJust) import Wire.API.Team.Role (Role (RoleAdmin, RoleExternalPartner, RoleMember, RoleOwner)) -import Wire.API.User (Email (Email, emailDomain, emailLocal), ManagedBy (ManagedByScim, ManagedByWire)) +import Wire.API.User import Wire.API.User.Search (Sso (..), TeamContact (..)) testObject_TeamContact_user_1 :: TeamContact @@ -33,7 +33,7 @@ testObject_TeamContact_user_1 = teamContactColorId = Nothing, teamContactHandle = Nothing, teamContactTeam = Nothing, - teamContactEmail = Just (Email {emailLocal = "({", emailDomain = "q"}), + teamContactEmail = Just (unsafeEmailAddress "some" "example"), teamContactCreatedAt = Just (fromJust (readUTCTimeMillis "1864-05-11T12:52:22.086Z")), teamContactManagedBy = Nothing, teamContactSAMLIdp = Just "r", @@ -51,7 +51,7 @@ testObject_TeamContact_user_2 = teamContactColorId = Just 2, teamContactHandle = Just "", teamContactTeam = Just (Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000200000001"))), - teamContactEmail = Just (Email {emailLocal = "\SI5g", emailDomain = ""}), + teamContactEmail = Just (unsafeEmailAddress "some" "example"), teamContactCreatedAt = Just (fromJust (readUTCTimeMillis "1864-05-08T03:35:20.125Z")), teamContactManagedBy = Nothing, teamContactSAMLIdp = Just "N\DC4", @@ -69,7 +69,7 @@ testObject_TeamContact_user_3 = teamContactColorId = Nothing, teamContactHandle = Just "", teamContactTeam = Just (Id (fromJust (UUID.fromString "00000002-0000-0002-0000-000100000000"))), - teamContactEmail = Just (Email {emailLocal = "", emailDomain = "A%s"}), + teamContactEmail = Just (unsafeEmailAddress "some" "example"), teamContactCreatedAt = Just (fromJust (readUTCTimeMillis "1864-05-09T04:40:28.583Z")), teamContactManagedBy = Just ManagedByScim, teamContactSAMLIdp = Just "\"c`", @@ -87,14 +87,14 @@ testObject_TeamContact_user_4 = teamContactColorId = Nothing, teamContactHandle = Just "U6", teamContactTeam = Nothing, - teamContactEmail = Just (Email {emailLocal = "", emailDomain = "ot\1000154"}), + teamContactEmail = Just (unsafeEmailAddress "some" "example"), teamContactCreatedAt = Nothing, teamContactManagedBy = Nothing, teamContactSAMLIdp = Nothing, teamContactRole = Nothing, teamContactScimExternalId = Nothing, teamContactSso = Nothing, - teamContactEmailUnvalidated = Just (Email {emailLocal = "foobar", emailDomain = "example.com"}) + teamContactEmailUnvalidated = Just (unsafeEmailAddress "some" "example") } testObject_TeamContact_user_5 :: TeamContact @@ -141,14 +141,14 @@ testObject_TeamContact_user_7 = teamContactColorId = Nothing, teamContactHandle = Nothing, teamContactTeam = Nothing, - teamContactEmail = Just (Email {emailLocal = "\ETX\189173", emailDomain = ""}), + teamContactEmail = Just (unsafeEmailAddress "some" "example"), teamContactCreatedAt = Just (fromJust (readUTCTimeMillis "1864-05-06T11:54:20.119Z")), teamContactManagedBy = Just ManagedByWire, teamContactSAMLIdp = Nothing, teamContactRole = Just RoleAdmin, teamContactScimExternalId = Just "0307979d-c742-4421-954a-9ceb1f22e58f", teamContactSso = Nothing, - teamContactEmailUnvalidated = Just (Email {emailLocal = "foobar", emailDomain = "example.com"}) + teamContactEmailUnvalidated = Just (unsafeEmailAddress "some" "example") } testObject_TeamContact_user_8 :: TeamContact @@ -159,7 +159,7 @@ testObject_TeamContact_user_8 = teamContactColorId = Just (-1), teamContactHandle = Just "\bdL", teamContactTeam = Just (Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000100000002"))), - teamContactEmail = Just (Email {emailLocal = "", emailDomain = "\ETB"}), + teamContactEmail = Just (unsafeEmailAddress "some" "example"), teamContactCreatedAt = Just (fromJust (readUTCTimeMillis "1864-05-06T04:27:11.179Z")), teamContactManagedBy = Just ManagedByScim, teamContactSAMLIdp = Just "", @@ -177,7 +177,7 @@ testObject_TeamContact_user_9 = teamContactColorId = Just 2, teamContactHandle = Nothing, teamContactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000200000001"))), - teamContactEmail = Just (Email {emailLocal = "\186866&\1040794", emailDomain = "U"}), + teamContactEmail = Just (unsafeEmailAddress "some" "example"), teamContactCreatedAt = Just (fromJust (readUTCTimeMillis "1864-05-11T18:31:16.554Z")), teamContactManagedBy = Just ManagedByWire, teamContactSAMLIdp = Just "\164542\US", @@ -213,7 +213,7 @@ testObject_TeamContact_user_11 = teamContactColorId = Just (-3), teamContactHandle = Nothing, teamContactTeam = Nothing, - teamContactEmail = Just (Email {emailLocal = "m", emailDomain = "\183237"}), + teamContactEmail = Just (unsafeEmailAddress "some" "example"), teamContactCreatedAt = Nothing, teamContactManagedBy = Just ManagedByScim, teamContactSAMLIdp = Nothing, @@ -231,7 +231,7 @@ testObject_TeamContact_user_12 = teamContactColorId = Nothing, teamContactHandle = Just "", teamContactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), - teamContactEmail = Just (Email {emailLocal = "", emailDomain = "(-"}), + teamContactEmail = Just (unsafeEmailAddress "some" "example"), teamContactCreatedAt = Just (fromJust (readUTCTimeMillis "1864-05-06T13:09:44.601Z")), teamContactManagedBy = Nothing, teamContactSAMLIdp = Just "\SUB:", @@ -249,7 +249,7 @@ testObject_TeamContact_user_13 = teamContactColorId = Just 0, teamContactHandle = Just "S", teamContactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000200000000"))), - teamContactEmail = Just (Email {emailLocal = "\SOH\13765", emailDomain = "_C"}), + teamContactEmail = Just (unsafeEmailAddress "some" "example"), teamContactCreatedAt = Nothing, teamContactManagedBy = Just ManagedByScim, teamContactSAMLIdp = Just "\993657\a", @@ -267,7 +267,7 @@ testObject_TeamContact_user_14 = teamContactColorId = Just (-3), teamContactHandle = Just "\"\US\DC4", teamContactTeam = Nothing, - teamContactEmail = Just (Email {emailLocal = "4)=", emailDomain = "I\DLE"}), + teamContactEmail = Just (unsafeEmailAddress "some" "example"), teamContactCreatedAt = Just (fromJust (readUTCTimeMillis "1864-05-08T20:31:37.388Z")), teamContactManagedBy = Just ManagedByScim, teamContactSAMLIdp = Nothing, @@ -285,14 +285,14 @@ testObject_TeamContact_user_15 = teamContactColorId = Nothing, teamContactHandle = Just "J", teamContactTeam = Nothing, - teamContactEmail = Just (Email {emailLocal = "9L", emailDomain = "\61733("}), + teamContactEmail = Just (unsafeEmailAddress "some" "example"), teamContactCreatedAt = Just (fromJust (readUTCTimeMillis "1864-05-11T14:15:19.890Z")), teamContactManagedBy = Nothing, teamContactSAMLIdp = Just "", teamContactRole = Just RoleExternalPartner, teamContactScimExternalId = Nothing, teamContactSso = Nothing, - teamContactEmailUnvalidated = Just (Email {emailLocal = "foobar", emailDomain = "example.com"}) + teamContactEmailUnvalidated = Just (unsafeEmailAddress "some" "example") } testObject_TeamContact_user_16 :: TeamContact @@ -303,14 +303,14 @@ testObject_TeamContact_user_16 = teamContactColorId = Just (-1), teamContactHandle = Nothing, teamContactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000200000001"))), - teamContactEmail = Just (Email {emailLocal = "", emailDomain = "j"}), + teamContactEmail = Just (unsafeEmailAddress "some" "example"), teamContactCreatedAt = Just (fromJust (readUTCTimeMillis "1864-05-08T15:43:05.866Z")), teamContactManagedBy = Just ManagedByWire, teamContactSAMLIdp = Just "k", teamContactRole = Nothing, teamContactScimExternalId = Nothing, teamContactSso = Just (Sso "https://example.com/issuer/123" "0307979d-c742-4421-954a-9ceb1f22e58f"), - teamContactEmailUnvalidated = Just (Email {emailLocal = "foobar", emailDomain = "example.com"}) + teamContactEmailUnvalidated = Just (unsafeEmailAddress "some" "example") } testObject_TeamContact_user_17 :: TeamContact @@ -321,7 +321,7 @@ testObject_TeamContact_user_17 = teamContactColorId = Just (-3), teamContactHandle = Nothing, teamContactTeam = Just (Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000200000001"))), - teamContactEmail = Just (Email {emailLocal = "X\1007558", emailDomain = "D(0"}), + teamContactEmail = Just (unsafeEmailAddress "some" "example"), teamContactCreatedAt = Just (fromJust (readUTCTimeMillis "1864-05-10T20:50:28.410Z")), teamContactManagedBy = Just ManagedByWire, teamContactSAMLIdp = Just "\138052", @@ -339,7 +339,7 @@ testObject_TeamContact_user_18 = teamContactColorId = Just 3, teamContactHandle = Nothing, teamContactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000000000000"))), - teamContactEmail = Just (Email {emailLocal = "\1109166]L", emailDomain = "\23664"}), + teamContactEmail = Just (unsafeEmailAddress "some" "example"), teamContactCreatedAt = Nothing, teamContactManagedBy = Just ManagedByWire, teamContactSAMLIdp = Just "\DC2", @@ -357,7 +357,7 @@ testObject_TeamContact_user_19 = teamContactColorId = Just (-3), teamContactHandle = Nothing, teamContactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000200000002"))), - teamContactEmail = Just (Email {emailLocal = "N", emailDomain = ""}), + teamContactEmail = Just (unsafeEmailAddress "some" "example"), teamContactCreatedAt = Just (fromJust (readUTCTimeMillis "1864-05-10T11:20:36.673Z")), teamContactManagedBy = Nothing, teamContactSAMLIdp = Nothing, @@ -375,12 +375,12 @@ testObject_TeamContact_user_20 = teamContactColorId = Just (-3), teamContactHandle = Just "0\1085403\1021449", teamContactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000100000001"))), - teamContactEmail = Just (Email {emailLocal = " --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Test.Wire.API.Golden.Generated.WithStatusNoLock_team where - -import Data.Domain -import Imports -import Wire.API.Team.Feature - -testObject_WithStatusNoLock_team_1 :: WithStatusNoLock AppLockConfig -testObject_WithStatusNoLock_team_1 = WithStatusNoLock FeatureStatusEnabled (AppLockConfig (EnforceAppLock False) (-98)) FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_2 :: WithStatusNoLock AppLockConfig -testObject_WithStatusNoLock_team_2 = WithStatusNoLock FeatureStatusEnabled (AppLockConfig (EnforceAppLock True) 0) FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_3 :: WithStatusNoLock AppLockConfig -testObject_WithStatusNoLock_team_3 = WithStatusNoLock FeatureStatusEnabled (AppLockConfig (EnforceAppLock True) 111) FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_4 :: WithStatusNoLock SelfDeletingMessagesConfig -testObject_WithStatusNoLock_team_4 = WithStatusNoLock FeatureStatusEnabled (SelfDeletingMessagesConfig (-97)) FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_5 :: WithStatusNoLock SelfDeletingMessagesConfig -testObject_WithStatusNoLock_team_5 = WithStatusNoLock FeatureStatusEnabled (SelfDeletingMessagesConfig 0) FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_6 :: WithStatusNoLock SelfDeletingMessagesConfig -testObject_WithStatusNoLock_team_6 = WithStatusNoLock FeatureStatusEnabled (SelfDeletingMessagesConfig 77) FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_7 :: WithStatusNoLock ClassifiedDomainsConfig -testObject_WithStatusNoLock_team_7 = WithStatusNoLock FeatureStatusEnabled (ClassifiedDomainsConfig []) FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_8 :: WithStatusNoLock ClassifiedDomainsConfig -testObject_WithStatusNoLock_team_8 = WithStatusNoLock FeatureStatusEnabled (ClassifiedDomainsConfig [Domain "example.com", Domain "test.foobar"]) FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_9 :: WithStatusNoLock ClassifiedDomainsConfig -testObject_WithStatusNoLock_team_9 = WithStatusNoLock FeatureStatusEnabled (ClassifiedDomainsConfig [Domain "test.foobar"]) FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_10 :: WithStatusNoLock SSOConfig -testObject_WithStatusNoLock_team_10 = WithStatusNoLock FeatureStatusDisabled SSOConfig FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_11 :: WithStatusNoLock SearchVisibilityAvailableConfig -testObject_WithStatusNoLock_team_11 = WithStatusNoLock FeatureStatusEnabled SearchVisibilityAvailableConfig FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_12 :: WithStatusNoLock ValidateSAMLEmailsConfig -testObject_WithStatusNoLock_team_12 = WithStatusNoLock FeatureStatusDisabled ValidateSAMLEmailsConfig FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_13 :: WithStatusNoLock DigitalSignaturesConfig -testObject_WithStatusNoLock_team_13 = WithStatusNoLock FeatureStatusEnabled DigitalSignaturesConfig FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_14 :: WithStatusNoLock ConferenceCallingConfig -testObject_WithStatusNoLock_team_14 = WithStatusNoLock FeatureStatusDisabled ConferenceCallingConfig FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_15 :: WithStatusNoLock GuestLinksConfig -testObject_WithStatusNoLock_team_15 = WithStatusNoLock FeatureStatusEnabled GuestLinksConfig FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_16 :: WithStatusNoLock SndFactorPasswordChallengeConfig -testObject_WithStatusNoLock_team_16 = WithStatusNoLock FeatureStatusDisabled SndFactorPasswordChallengeConfig FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_17 :: WithStatusNoLock SearchVisibilityInboundConfig -testObject_WithStatusNoLock_team_17 = WithStatusNoLock FeatureStatusEnabled SearchVisibilityInboundConfig FeatureTTLUnlimited diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusPatch_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusPatch_team.hs deleted file mode 100644 index d4bb69801a7..00000000000 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusPatch_team.hs +++ /dev/null @@ -1,84 +0,0 @@ -{-# LANGUAGE OverloadedLists #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Test.Wire.API.Golden.Generated.WithStatusPatch_team where - -import Data.Domain -import Imports -import Wire.API.Team.Feature hiding (withStatus) - -testObject_WithStatusPatch_team_1 :: WithStatusPatch AppLockConfig -testObject_WithStatusPatch_team_1 = withStatus (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just (AppLockConfig (EnforceAppLock False) (-98))) - -testObject_WithStatusPatch_team_2 :: WithStatusPatch AppLockConfig -testObject_WithStatusPatch_team_2 = withStatus Nothing Nothing (Just (AppLockConfig (EnforceAppLock True) 0)) - -testObject_WithStatusPatch_team_3 :: WithStatusPatch AppLockConfig -testObject_WithStatusPatch_team_3 = withStatus (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just (AppLockConfig (EnforceAppLock True) 111)) - -testObject_WithStatusPatch_team_4 :: WithStatusPatch SelfDeletingMessagesConfig -testObject_WithStatusPatch_team_4 = withStatus (Just FeatureStatusEnabled) Nothing (Just (SelfDeletingMessagesConfig (-97))) - -testObject_WithStatusPatch_team_5 :: WithStatusPatch SelfDeletingMessagesConfig -testObject_WithStatusPatch_team_5 = withStatus (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just (SelfDeletingMessagesConfig 0)) - -testObject_WithStatusPatch_team_6 :: WithStatusPatch SelfDeletingMessagesConfig -testObject_WithStatusPatch_team_6 = withStatus (Just FeatureStatusEnabled) Nothing (Just (SelfDeletingMessagesConfig 77)) - -testObject_WithStatusPatch_team_7 :: WithStatusPatch ClassifiedDomainsConfig -testObject_WithStatusPatch_team_7 = withStatus (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just (ClassifiedDomainsConfig [])) - -testObject_WithStatusPatch_team_8 :: WithStatusPatch ClassifiedDomainsConfig -testObject_WithStatusPatch_team_8 = withStatus Nothing (Just LockStatusLocked) (Just (ClassifiedDomainsConfig [Domain "example.com", Domain "test.foobar"])) - -testObject_WithStatusPatch_team_9 :: WithStatusPatch ClassifiedDomainsConfig -testObject_WithStatusPatch_team_9 = withStatus (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just (ClassifiedDomainsConfig [Domain "test.foobar"])) - -testObject_WithStatusPatch_team_10 :: WithStatusPatch SSOConfig -testObject_WithStatusPatch_team_10 = withStatus (Just FeatureStatusDisabled) (Just LockStatusLocked) (Just SSOConfig) - -testObject_WithStatusPatch_team_11 :: WithStatusPatch SearchVisibilityAvailableConfig -testObject_WithStatusPatch_team_11 = withStatus (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just SearchVisibilityAvailableConfig) - -testObject_WithStatusPatch_team_12 :: WithStatusPatch ValidateSAMLEmailsConfig -testObject_WithStatusPatch_team_12 = withStatus (Just FeatureStatusDisabled) Nothing (Just ValidateSAMLEmailsConfig) - -testObject_WithStatusPatch_team_13 :: WithStatusPatch DigitalSignaturesConfig -testObject_WithStatusPatch_team_13 = withStatus (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just DigitalSignaturesConfig) - -testObject_WithStatusPatch_team_14 :: WithStatusPatch ConferenceCallingConfig -testObject_WithStatusPatch_team_14 = withStatus Nothing (Just LockStatusUnlocked) (Just ConferenceCallingConfig) - -testObject_WithStatusPatch_team_15 :: WithStatusPatch GuestLinksConfig -testObject_WithStatusPatch_team_15 = withStatus (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just GuestLinksConfig) - -testObject_WithStatusPatch_team_16 :: WithStatusPatch SndFactorPasswordChallengeConfig -testObject_WithStatusPatch_team_16 = withStatus (Just FeatureStatusDisabled) (Just LockStatusUnlocked) (Just SndFactorPasswordChallengeConfig) - -testObject_WithStatusPatch_team_17 :: WithStatusPatch SearchVisibilityInboundConfig -testObject_WithStatusPatch_team_17 = withStatus (Just FeatureStatusEnabled) Nothing (Just SearchVisibilityInboundConfig) - -testObject_WithStatusPatch_team_18 :: WithStatusPatch GuestLinksConfig -testObject_WithStatusPatch_team_18 = withStatus (Just FeatureStatusEnabled) Nothing Nothing - -testObject_WithStatusPatch_team_19 :: WithStatusPatch SelfDeletingMessagesConfig -testObject_WithStatusPatch_team_19 = withStatus Nothing (Just LockStatusUnlocked) Nothing - -withStatus :: Maybe FeatureStatus -> Maybe LockStatus -> Maybe cfg -> WithStatusPatch cfg -withStatus fs ls cfg = withStatus' fs ls cfg (Just FeatureTTLUnlimited) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatus_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatus_team.hs deleted file mode 100644 index 78523389109..00000000000 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatus_team.hs +++ /dev/null @@ -1,108 +0,0 @@ -{-# LANGUAGE OverloadedLists #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Test.Wire.API.Golden.Generated.WithStatus_team where - -import Data.ByteString.Conversion (parser, runParser) -import Data.Domain -import Data.Misc -import Imports -import Wire.API.Team.Feature hiding (withStatus) -import Wire.API.Team.Feature qualified as F - -testObject_WithStatus_team_1 :: WithStatus AppLockConfig -testObject_WithStatus_team_1 = withStatus FeatureStatusEnabled LockStatusUnlocked (AppLockConfig (EnforceAppLock False) (-98)) - -testObject_WithStatus_team_2 :: WithStatus AppLockConfig -testObject_WithStatus_team_2 = withStatus FeatureStatusEnabled LockStatusUnlocked (AppLockConfig (EnforceAppLock True) 0) - -testObject_WithStatus_team_3 :: WithStatus AppLockConfig -testObject_WithStatus_team_3 = withStatus FeatureStatusEnabled LockStatusLocked (AppLockConfig (EnforceAppLock True) 111) - -testObject_WithStatus_team_4 :: WithStatus SelfDeletingMessagesConfig -testObject_WithStatus_team_4 = withStatus FeatureStatusEnabled LockStatusUnlocked (SelfDeletingMessagesConfig (-97)) - -testObject_WithStatus_team_5 :: WithStatus SelfDeletingMessagesConfig -testObject_WithStatus_team_5 = withStatus FeatureStatusEnabled LockStatusUnlocked (SelfDeletingMessagesConfig 0) - -testObject_WithStatus_team_6 :: WithStatus SelfDeletingMessagesConfig -testObject_WithStatus_team_6 = withStatus FeatureStatusEnabled LockStatusLocked (SelfDeletingMessagesConfig 77) - -testObject_WithStatus_team_7 :: WithStatus ClassifiedDomainsConfig -testObject_WithStatus_team_7 = withStatus FeatureStatusEnabled LockStatusLocked (ClassifiedDomainsConfig []) - -testObject_WithStatus_team_8 :: WithStatus ClassifiedDomainsConfig -testObject_WithStatus_team_8 = withStatus FeatureStatusEnabled LockStatusLocked (ClassifiedDomainsConfig [Domain "example.com", Domain "test.foobar"]) - -testObject_WithStatus_team_9 :: WithStatus ClassifiedDomainsConfig -testObject_WithStatus_team_9 = withStatus FeatureStatusEnabled LockStatusUnlocked (ClassifiedDomainsConfig [Domain "test.foobar"]) - -testObject_WithStatus_team_10 :: WithStatus SSOConfig -testObject_WithStatus_team_10 = withStatus FeatureStatusDisabled LockStatusLocked SSOConfig - -testObject_WithStatus_team_11 :: WithStatus SearchVisibilityAvailableConfig -testObject_WithStatus_team_11 = withStatus FeatureStatusEnabled LockStatusLocked SearchVisibilityAvailableConfig - -testObject_WithStatus_team_12 :: WithStatus ValidateSAMLEmailsConfig -testObject_WithStatus_team_12 = withStatus FeatureStatusDisabled LockStatusLocked ValidateSAMLEmailsConfig - -testObject_WithStatus_team_13 :: WithStatus DigitalSignaturesConfig -testObject_WithStatus_team_13 = withStatus FeatureStatusEnabled LockStatusLocked DigitalSignaturesConfig - -testObject_WithStatus_team_14 :: WithStatus ConferenceCallingConfig -testObject_WithStatus_team_14 = withStatus FeatureStatusDisabled LockStatusUnlocked ConferenceCallingConfig - -testObject_WithStatus_team_15 :: WithStatus GuestLinksConfig -testObject_WithStatus_team_15 = withStatus FeatureStatusEnabled LockStatusUnlocked GuestLinksConfig - -testObject_WithStatus_team_16 :: WithStatus SndFactorPasswordChallengeConfig -testObject_WithStatus_team_16 = withStatus FeatureStatusDisabled LockStatusUnlocked SndFactorPasswordChallengeConfig - -testObject_WithStatus_team_17 :: WithStatus SearchVisibilityInboundConfig -testObject_WithStatus_team_17 = withStatus FeatureStatusEnabled LockStatusUnlocked SearchVisibilityInboundConfig - -testObject_WithStatus_team_18 :: WithStatus MlsE2EIdConfig -testObject_WithStatus_team_18 = - withStatus - FeatureStatusEnabled - LockStatusLocked - ( MlsE2EIdConfig - (fromIntegral @Int (60 * 60 * 24)) - Nothing - (either (\e -> error (show e)) Just $ parseHttpsUrl "https://example.com") - False - ) - -parseHttpsUrl :: ByteString -> Either String HttpsUrl -parseHttpsUrl url = runParser parser url - -testObject_WithStatus_team_19 :: WithStatus MlsE2EIdConfig -testObject_WithStatus_team_19 = - withStatus - FeatureStatusEnabled - LockStatusLocked - ( MlsE2EIdConfig - (fromIntegral @Int (60 * 60 * 24)) - (either (\e -> error (show e)) Just $ parseHttpsUrl "https://example.com") - Nothing - True - ) - -withStatus :: FeatureStatus -> LockStatus -> cfg -> WithStatus cfg -withStatus fs ls cfg = F.withStatus fs ls cfg FeatureTTLUnlimited 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 57daad1dd22..3a898d764ce 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 @@ -19,6 +19,8 @@ module Test.Wire.API.Golden.Manual where import Imports import Test.Tasty +import Test.Wire.API.Golden.Manual.Activate_user +import Test.Wire.API.Golden.Manual.CannonId import Test.Wire.API.Golden.Manual.ClientCapability import Test.Wire.API.Golden.Manual.ClientCapabilityList import Test.Wire.API.Golden.Manual.Contact @@ -30,6 +32,7 @@ import Test.Wire.API.Golden.Manual.ConversationRemoveMembers import Test.Wire.API.Golden.Manual.ConversationsResponse import Test.Wire.API.Golden.Manual.CreateGroupConversation import Test.Wire.API.Golden.Manual.CreateScimToken +import Test.Wire.API.Golden.Manual.CreateScimTokenResponse import Test.Wire.API.Golden.Manual.FeatureConfigEvent import Test.Wire.API.Golden.Manual.FederationDomainConfig import Test.Wire.API.Golden.Manual.FederationRestriction @@ -38,9 +41,15 @@ import Test.Wire.API.Golden.Manual.GetPaginatedConversationIds import Test.Wire.API.Golden.Manual.GroupId import Test.Wire.API.Golden.Manual.ListConversations import Test.Wire.API.Golden.Manual.ListUsersById +import Test.Wire.API.Golden.Manual.LoginId_user +import Test.Wire.API.Golden.Manual.Login_user import Test.Wire.API.Golden.Manual.MLSKeys +import Test.Wire.API.Golden.Manual.Presence +import Test.Wire.API.Golden.Manual.Push +import Test.Wire.API.Golden.Manual.PushRemove import Test.Wire.API.Golden.Manual.QualifiedUserClientPrekeyMap import Test.Wire.API.Golden.Manual.SearchResultContact +import Test.Wire.API.Golden.Manual.SendActivationCode_user import Test.Wire.API.Golden.Manual.SubConversation import Test.Wire.API.Golden.Manual.TeamSize import Test.Wire.API.Golden.Manual.Token @@ -64,7 +73,8 @@ tests = (testObject_UserClientPrekeyMap_5, "testObject_UserClientPrekeyMap_5.json"), (testObject_UserClientPrekeyMap_6, "testObject_UserClientPrekeyMap_6.json"), (testObject_UserClientPrekeyMap_7, "testObject_UserClientPrekeyMap_7.json"), - (testObject_UserClientPrekeyMap_8, "testObject_UserClientPrekeyMap_8.json") + (testObject_UserClientPrekeyMap_8, "testObject_UserClientPrekeyMap_8.json"), + (testObject_UserClientPrekeyMap_9, "testObject_UserClientPrekeyMap_9.json") ], testGroup "QualifiedUserClientPrekeyMap" $ testObjects @@ -101,12 +111,21 @@ tests = ], testGroup "ClientCapability" $ testObjects - [(testObject_ClientCapability_1, "testObject_ClientCapability_1.json")], - testGroup "ClientCapabilityList" $ + [ (testObject_ClientCapability_1, "testObject_ClientCapability_1.json"), + (testObject_ClientCapability_2, "testObject_ClientCapability_2.json") + ], + testGroup "ClientCapabilityListV6" $ testObjects [ (testObject_ClientCapabilityList_1, "testObject_ClientCapabilityList_1.json"), (testObject_ClientCapabilityList_2, "testObject_ClientCapabilityList_2.json") ], + testGroup "ClientCapabilityListV6 - non-round-trip" $ + [testToJSON testObject_ClientCapabilityList_3 "testObject_ClientCapabilityList_3.json"], + testGroup "ClientCapabilityList" $ + testObjects + [ (testObject_ClientCapabilityList_4, "testObject_ClientCapabilityList_4.json"), + (testObject_ClientCapabilityList_5, "testObject_ClientCapabilityList_5.json") + ], testGroup "Event.FeatureConfig.Event" $ testObjects @@ -135,6 +154,10 @@ tests = (testObject_CreateScimToken_3, "testObject_CreateScimToken_3.json"), (testObject_CreateScimToken_4, "testObject_CreateScimToken_4.json") ], + testGroup "CreateScimTokenResponse" $ + testObjects + [ (testObject_CreateScimTokenResponse_1, "testObject_CreateScimTokenResponse_1.json") + ], testGroup "Contact" $ testObjects [ (testObject_Contact_1, "testObject_Contact_1.json"), @@ -229,7 +252,8 @@ tests = (testObject_UserEvent_14, "testObject_UserEvent_14.json"), (testObject_UserEvent_15, "testObject_UserEvent_15.json"), (testObject_UserEvent_16, "testObject_UserEvent_16.json"), - (testObject_UserEvent_17, "testObject_UserEvent_17.json") + (testObject_UserEvent_17, "testObject_UserEvent_17.json"), + (testObject_UserEvent_18, "testObject_UserEvent_18.json") ], testGroup "MLSPublicKeys" $ testObjects @@ -238,5 +262,54 @@ tests = testGroup "MLSKeysByPurpose" $ testObjects [ (testObject_MLSKeysByPurpose1, "testObject_MLSKeysByPurpose_1.json") + ], + testGroup "SendActivationCode" $ + testObjects + [ (testObject_SendActivationCode_1, "testObject_SendActivationCode_1.json"), + (testObject_SendActivationCode_2, "testObject_SendActivationCode_2.json") + ], + testGroup "LoginId" $ + testObjects + [ (testObject_LoginId_user_1, "testObject_LoginId_user_1.json"), + (testObject_LoginId_user_2, "testObject_LoginId_user_2.json"), + (testObject_LoginId_user_3, "testObject_LoginId_user_3.json"), + (testObject_LoginId_user_4, "testObject_LoginId_user_4.json"), + (testObject_LoginId_user_5, "testObject_LoginId_user_5.json"), + (testObject_LoginId_user_6, "testObject_LoginId_user_6.json") + ], + testGroup "Login" $ + testObjects + [ (testObject_Login_user_1, "testObject_Login_user_1.json"), + (testObject_Login_user_2, "testObject_Login_user_2.json"), + (testObject_Login_user_3, "testObject_Login_user_3.json"), + (testObject_Login_user_4, "testObject_Login_user_4.json"), + (testObject_Login_user_5, "testObject_Login_user_5.json") + ], + testGroup "CannonId" $ + testObjects + [ (testObject_CannonId_1, "testObject_CannonId_1.json"), + (testObject_CannonId_2, "testObject_CannonId_2.json"), + (testObject_CannonId_3, "testObject_CannonId_3.json") + ], + testGroup "Presence" $ + testObjects + [ (testObject_Presence_1, "testObject_Presence_1.json"), + (testObject_Presence_2, "testObject_Presence_2.json") + ], + testGroup "Push" $ + testObjects + [ (testObject_Push_1, "testObject_Push_1.json"), + (testObject_Push_2, "testObject_Push_2.json") + ], + testGroup "PushRemove" $ + testObjects + [ (testObject_PushRemove_1, "testObject_PushRemove_1.json") + ], + testGroup "Activate" $ + testObjects + [ (testObject_Activate_user_1, "testObject_Activate_user_1.json"), + (testObject_Activate_user_2, "testObject_Activate_user_2.json"), + (testObject_Activate_user_3, "testObject_Activate_user_3.json"), + (testObject_Activate_user_4, "testObject_Activate_user_4.json") ] ] diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Activate_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Activate_user.hs new file mode 100644 index 00000000000..583968bb55d --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Activate_user.hs @@ -0,0 +1,56 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Wire.API.Golden.Manual.Activate_user where + +import Data.Text.Ascii (AsciiChars (validate)) +import Imports (Bool (False, True), fromRight, undefined) +import Wire.API.User +import Wire.API.User.Activation + +testObject_Activate_user_1 :: Activate +testObject_Activate_user_1 = + Activate + { activateTarget = + ActivateKey (ActivationKey {fromActivationKey = fromRight undefined (validate "e3sm9EjNmzA=")}), + activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "fg==")}, + activateDryrun = False + } + +testObject_Activate_user_2 :: Activate +testObject_Activate_user_2 = + Activate + { activateTarget = ActivateEmail (unsafeEmailAddress "valid1j28hfna" "iagh28nuwkas"), + activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "")}, + activateDryrun = False + } + +testObject_Activate_user_3 :: Activate +testObject_Activate_user_3 = + Activate + { activateTarget = ActivateKey (ActivationKey {fromActivationKey = fromRight undefined (validate "DkV9xQ==")}), + activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "61wG")}, + activateDryrun = True + } + +testObject_Activate_user_4 :: Activate +testObject_Activate_user_4 = + Activate + { activateTarget = ActivateKey (ActivationKey {fromActivationKey = fromRight undefined (validate "V3mr5D4=")}), + activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "sScBopoNTb0=")}, + activateDryrun = True + } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CannonId.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CannonId.hs new file mode 100644 index 00000000000..9f9de73dbab --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CannonId.hs @@ -0,0 +1,34 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Wire.API.Golden.Manual.CannonId + ( testObject_CannonId_1, + testObject_CannonId_2, + testObject_CannonId_3, + ) +where + +import Wire.API.CannonId + +testObject_CannonId_1 :: CannonId +testObject_CannonId_1 = CannonId "" + +testObject_CannonId_2 :: CannonId +testObject_CannonId_2 = CannonId "sdfiou" + +testObject_CannonId_3 :: CannonId +testObject_CannonId_3 = CannonId "1!_*`'\"" diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ClientCapability.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ClientCapability.hs index f739ad108e1..c2c5d152bcc 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ClientCapability.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ClientCapability.hs @@ -21,3 +21,6 @@ import Wire.API.User.Client (ClientCapability (..)) testObject_ClientCapability_1 :: ClientCapability testObject_ClientCapability_1 = ClientSupportsLegalholdImplicitConsent + +testObject_ClientCapability_2 :: ClientCapability +testObject_ClientCapability_2 = ClientSupportsConsumableNotifications diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ClientCapabilityList.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ClientCapabilityList.hs index 477e4bbcadf..bbb616c990a 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ClientCapabilityList.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ClientCapabilityList.hs @@ -19,10 +19,35 @@ module Test.Wire.API.Golden.Manual.ClientCapabilityList where import Data.Set qualified as Set import Imports +import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.API.User.Client (ClientCapability (..), ClientCapabilityList (..)) -testObject_ClientCapabilityList_1 :: ClientCapabilityList -testObject_ClientCapabilityList_1 = ClientCapabilityList mempty +testObject_ClientCapabilityList_1 :: Versioned V6 ClientCapabilityList +testObject_ClientCapabilityList_1 = Versioned $ ClientCapabilityList mempty -testObject_ClientCapabilityList_2 :: ClientCapabilityList -testObject_ClientCapabilityList_2 = ClientCapabilityList (Set.fromList [ClientSupportsLegalholdImplicitConsent]) +testObject_ClientCapabilityList_2 :: Versioned V6 ClientCapabilityList +testObject_ClientCapabilityList_2 = Versioned $ ClientCapabilityList (Set.fromList [ClientSupportsLegalholdImplicitConsent]) + +testObject_ClientCapabilityList_3 :: Versioned V6 ClientCapabilityList +testObject_ClientCapabilityList_3 = + Versioned $ + ClientCapabilityList + ( Set.fromList + [ ClientSupportsLegalholdImplicitConsent, + ClientSupportsConsumableNotifications + ] + ) + +testObject_ClientCapabilityList_4 :: ClientCapabilityList +testObject_ClientCapabilityList_4 = + ClientCapabilityList mempty + +testObject_ClientCapabilityList_5 :: ClientCapabilityList +testObject_ClientCapabilityList_5 = + ClientCapabilityList + ( Set.fromList + [ ClientSupportsLegalholdImplicitConsent, + ClientSupportsConsumableNotifications + ] + ) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CreateScimToken.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CreateScimToken.hs index 51c9bd8ecad..e2c32ffcf55 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CreateScimToken.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CreateScimToken.hs @@ -21,7 +21,7 @@ import Data.Code import Data.Misc (plainTextPassword6Unsafe) import Data.Range (unsafeRange) import Data.Text.Ascii (AsciiChars (validate)) -import Imports (Maybe (Just, Nothing), fromRight, undefined) +import Imports import Wire.API.User.Scim (CreateScimToken (..)) testObject_CreateScimToken_1 :: CreateScimToken @@ -30,6 +30,7 @@ testObject_CreateScimToken_1 = "description" (Just (plainTextPassword6Unsafe "very-geheim")) (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "123456"))})) + Nothing testObject_CreateScimToken_2 :: CreateScimToken testObject_CreateScimToken_2 = @@ -37,6 +38,7 @@ testObject_CreateScimToken_2 = "description2" (Just (plainTextPassword6Unsafe "secret")) Nothing + Nothing testObject_CreateScimToken_3 :: CreateScimToken testObject_CreateScimToken_3 = @@ -44,6 +46,7 @@ testObject_CreateScimToken_3 = "description3" Nothing (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "654321"))})) + Nothing testObject_CreateScimToken_4 :: CreateScimToken testObject_CreateScimToken_4 = @@ -51,3 +54,4 @@ testObject_CreateScimToken_4 = "description4" Nothing Nothing + (Just "scim connection name") diff --git a/libs/extended/src/Options/Applicative/Extended.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CreateScimTokenResponse.hs similarity index 52% rename from libs/extended/src/Options/Applicative/Extended.hs rename to libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CreateScimTokenResponse.hs index 3a44fecb188..799a9fb775b 100644 --- a/libs/extended/src/Options/Applicative/Extended.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CreateScimTokenResponse.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE NoImplicitPrelude #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -17,25 +15,24 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . --- | A version of "Options.Applicative" with extra utilities. -module Options.Applicative.Extended - ( module Options.Applicative, - - -- * Extra option readers - autoRange, - ) -where +module Test.Wire.API.Golden.Manual.CreateScimTokenResponse where -import Data.List.Extra (stripInfix) +import Data.Id (Id (Id)) +import Data.Time (Day (ModifiedJulianDay)) +import Data.Time.Clock (UTCTime (UTCTime, utctDay, utctDayTime)) +import Data.UUID qualified as UUID import Imports -import Options.Applicative +import Wire.API.User.Scim --- | A reader that accepts either @N@ or @N..M@ (not necessarily just --- numbers). -autoRange :: (Read a) => ReadM (a, a) -autoRange = eitherReader $ \arg -> case stripInfix ".." arg of - Nothing -> (\a -> (a, a)) <$> readEither arg - Just (l, r) -> case (readEither l, readEither r) of - (Right lv, Right rv) -> Right (lv, rv) - (Left e, _) -> Left ("can't parse lower end: " <> e) - (_, Left e) -> Left ("can't parse upper end: " <> e) +testObject_CreateScimTokenResponse_1 :: CreateScimTokenResponse +testObject_CreateScimTokenResponse_1 = + CreateScimTokenResponse + (ScimToken "token") + ( ScimTokenInfo + (Id (fromJust (UUID.fromString "2853751e-9fb6-4425-b1bd-bd8aa2640c69"))) + (Id (fromJust (UUID.fromString "e25faea1-ee2d-4fd8-bf25-e6748d392b23"))) + (UTCTime {utctDay = ModifiedJulianDay 60605, utctDayTime = 65090}) + Nothing + "description" + "token name" + ) 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 0a9db679bd7..1f8907364d9 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 @@ -41,6 +41,7 @@ profile1 = UserProfile { profileQualifiedId = Qualified user1 domain1, profileName = Name "user1", + profileTextStatus = Nothing, profilePict = Pict [], profileAssets = [], profileAccentId = ColourId 0, @@ -57,6 +58,7 @@ profile2 = UserProfile { profileQualifiedId = Qualified user2 domain2, profileName = Name "user2", + profileTextStatus = rightToMaybe $ mkTextStatus "text status", profilePict = Pict [], profileAssets = [], profileAccentId = ColourId 0, diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/LoginId_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/LoginId_user.hs new file mode 100644 index 00000000000..c739d7d034b --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/LoginId_user.hs @@ -0,0 +1,48 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Wire.API.Golden.Manual.LoginId_user where + +import Data.Handle (parseHandle) +import Data.Maybe +import Wire.API.User +import Wire.API.User.Auth (LoginId (..)) + +testObject_LoginId_user_1 :: LoginId +testObject_LoginId_user_1 = + LoginByEmail + (unsafeEmailAddress "some" "example") + +testObject_LoginId_user_2 :: LoginId +testObject_LoginId_user_2 = + LoginByEmail + ( unsafeEmailAddress "some" "example" + ) + +testObject_LoginId_user_3 :: LoginId +testObject_LoginId_user_3 = LoginByHandle (fromJust (parseHandle "7a8gg3v98")) + +testObject_LoginId_user_4 :: LoginId +testObject_LoginId_user_4 = LoginByHandle (fromJust (parseHandle "lb")) + +testObject_LoginId_user_5 :: LoginId +testObject_LoginId_user_5 = + LoginByHandle (fromJust (parseHandle "z58-6fbjhtx11d8t6oplyijpkc2.fp_lf3kpk3_.qle4iecjun2xd0tpcordlg2bwv636v3cthpgwah3undqmuofgzp8ry6gc6g-n-kxnj7sl6771hxou7-t_ps_lu_t3.4ukz6dh6fkjq2i3aggtkbpzbd1162.qv.rbtb6e.90-xpayg65z9t9lk2aur452zcs9a")) + +testObject_LoginId_user_6 :: LoginId +testObject_LoginId_user_6 = + LoginByEmail (unsafeEmailAddress "some" "example") diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Login_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Login_user.hs new file mode 100644 index 00000000000..161f8dd7844 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Login_user.hs @@ -0,0 +1,80 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Wire.API.Golden.Manual.Login_user where + +import Data.Code +import Data.Handle (parseHandle) +import Data.Maybe +import Data.Misc (plainTextPassword6Unsafe) +import Data.Range (unsafeRange) +import Data.Text.Ascii (AsciiChars (validate)) +import Imports +import Wire.API.User +import Wire.API.User.Auth + +testObject_Login_user_1 :: Login +testObject_Login_user_1 = + MkLogin + (LoginByEmail (unsafeEmailAddress "some" "example")) + ( plainTextPassword6Unsafe + "\b5Ta\61971\150647\186716fa&\1047748o!ov\SI\1100133i\DC4\ETXY\SOR\991323\1086159Ta^s\ETB\SI[\189068\988899\26508\CAN6\STXp\1069462-9\983823&\NAK\1052068]^\13044;>-Z$Z\NAK\r\1101550a\RS%\NUL:\188721\47674\157548?e]\ETX \142608 C\SOH\SIS%8m\1091987V\147131[\1006262\&6\171610\1011219\164656SX\n%\1061259*>\t+\132427Y\989558\993346\GSU\1067541\&6TU!*\40114\&90\1055516\RSV\162483N\t*\EOT{I<\1084278\SOH\183116!c\\\n\1107501\183146\DC1,-xX\EMV?\t\168648\1054239\DC2\DEL1\SOHu\SOH\63459\53061\SO+h\ACK::\RS\21356_g,\SO*\v\DC4\1093710HFF\188918\1081075fF\ESC2\SOHT\DC1)\fc\35905l\1061547\f#~\STX]\1035086/Or)kY\1031423\SOHNCk\1067954\&5\1083470x=H\NUL\23760\1058646\1099097E/$\DELpbi\137522\FSKi\15676\1018134\t7\"OL\54208\7516\&5\43466\NUL(\1030852\166514\SOH\149343\994835\25513C==\GSTV3\DELl6\999006.Z)$\16723|\172732\1090303J;O\GSbw\vI\1101024I\SYN\DC2^\149630\STX3%i\EMW\138614\DC4\1113619tsL5\147087W\96700(_,\1091179*\1041287rckx\SOH\SIs\SOHJd\140574\SYNev.\DC4\DLE\99082.\1106785\996992\143448\US_\ETBf\STX\SO\DC3\1043748\&6O\DC1Q\SOH'\GS,|]W\SIa\62568\151062.\v\aH&-L\DC2+\147179\1095524\EOTm)\19925\181147\183368!\185223\142946m\DC4\DC3\1034282m\GS\185509>>\"NDw\1076877hY\1033831sFKz^ \1108187\&5Qec\NAK}|\1108194.Q\173114imb\1027220 p;\1089082\SYN\1065748kF\1102854r8o\DC1" + ) + (Just (CookieLabel {cookieLabelText = "r"})) + Nothing + +testObject_Login_user_2 :: Login +testObject_Login_user_2 = + MkLogin + (LoginByHandle (fromJust (parseHandle "c2wp.7s5."))) + ( plainTextPassword6Unsafe + "&\RS\DC4\1104052Z\11418n\SO\158691\1010906/\127253'\1063038m\1010345\"\9772\138717\RS(&\996590\SOf1Wf'I\SI\100286\1047270\1033961\DC1Jq\1050673Y\\Bedu@\1014647c\1003986D\53211\1050614S\144414\ETX\ETXW>\1005358\DC4\rSO8FXy\166833a\EM\170017\SUBNF\158145L\RS$5\NULk\RSz*s\148780\157980\v\175417\"SY\DEL\STX\994691\1103514ub5q\ENQ\1014299\vN.\t\183536:l\1105396\RS\1027721\a\168001\SO\vt\1098704W\SYN\1042396\1109979\a'v\ETB\64211\NAK\59538\STX \NAK\STX\49684,\1111630x\1047668^\1067127\27366I;\NAKb\1092049o\162763_\190546MME\1022528\SI\1096252H;\SO\ETBs\SO\1065937{Knlrd;\35750\DC4\SI\1075008TO\1090529\999639U\48787\1099927t\1068680^y\17268u$\DC1Jp\1054308\164905\164446\STX\"\1095399*\SO\1004302\32166\990924X\1098844\ETXsK}\b\143918\NUL0\988724\&12\171116\tM052\189551\EOT0\RS\986138\1084688{ji\ESC\1020800\27259&t \SI\ESCy\aL\136111\131558\994027\r\1054821ga,\DC4do,tx[I&\DC4h\DLE\ETX\DLEBpm\1002292-\a]/ZI\1033117q]w3n\46911e\23692kYo5\1090844'K\1089820}v\146759;\1018792\\=\41264\&8g\DLEg*has\44159\1006118\DC3\USYg?I\19462\NAKaW2\150415m\t}h\155161RbU\STX\ETBlz2!\DC3JW5\ESC\1026156U\SOg,rpO\5857]0\ESC\479\1005443F\SI\1045994\RS\SO\11908rl\1104306~\ACK+Mn{5\993784a\EM2\v{jM\ETBT\1058105$\DC1\1099974\GSj_~Z\1007141P\SOH\EOTo@TJhk\EOT\ETBk:-\96583[p\DLE\DC1\RS'\r\STXQ,,\1016866?H\rh\30225\rj\147982\DC2\\(u\ESCu\154705\1002696o\DC4\988492\1103465\1052034\DC1q\GS-\b\40807\DC1qW>\fys\8130,'\159954<" + ) + (Just (CookieLabel {cookieLabelText = "\1082362\66362>XC"})) + (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))})) + +testObject_Login_user_3 :: Login +testObject_Login_user_3 = + MkLogin + ( LoginByHandle + (fromJust (parseHandle "c372iaa_v5onjcck67rlzq4dn5_oxhtx7dpx7v82lp1rhx0e97i26--8r3c6k773bxtlzmkjc20-11_047ydua_o9_5u4sll_fl3ng_0sa.")) + ) + ( plainTextPassword6Unsafe + "\120347\184756DU\1035832hp\1006715t~\DC2\SOH\STX*\1053210y1\1078382H\173223{e\\S\SO?c_7\t\DC4X\135187\&6\172722E\100168j\SUB\t\SYN\1088511>HO]60\990035\ETX\"+w,t\1066040\ak(b%u\151197`>b\1028272e\ACKc\151393\1107996)\12375\&7\1082464`\186313yO+v%\1033664\rc<\65764\&2>8u\1094258\1080669\1113623\75033a\179193\NAK=\EOT\1077021\&8R&j\1042630\ESC\t4sj-\991835\40404n\136765\1064089N\GS\\\1026123\72288\&5\r\97004(P!\DEL\29235\26855\b\1067772Mr~\65123\EMjt>Z\GS~\140732A\1031358\SO\\>\DC16\">%\45860\1084751I@u5\187891\vrY\r;7\1071052#\1078407\1016286\CAN'\63315\1041397\EM_I_zY\987300\149441\EMd\1039844cd\DEL\1061999\136326Cp3\26325\GSXj\n\46305jy\44050\58825\t-\19065\43336d\1046547L\SUBYF\ACKPOL\54766\DC2\DC1\DC1\DC2*\rH\DLE(?\DC3F\25820\DLE\r]\1069451j\170177 @\ENQT\1100685s\FSF2\NAK]8\a\DC3!\NAKW\176469\1110834K\1025058\1112222_%\1001818\1113069'\1098149\70360(#\SOHky\t\ETB!\17570\NAK\DC4\ESC{\119317U2LS'" + ) + (Just (CookieLabel {cookieLabelText = "LGz%\119949j\f\RS/\SOH"})) + (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))})) + +testObject_Login_user_4 :: Login +testObject_Login_user_4 = + MkLogin + (LoginByEmail (unsafeEmailAddress "some" "example")) + ( plainTextPassword6Unsafe + "&\991818\1023244\83352\STXJ<-~\STX>\v\74228\151871\&5QN\53968\166184ql\NAK\74290\&3}{\DC3\173242S\22739;\t7\183958_F~D*f\1049940)\1067330-9\20699\&7GK= %\RS@kOF#\179945\1094401\124994\&8_\42309\GSL\37698\ETX\1047946\&0Wl1A`LYz\USy\20728\SUBo\ESC[\DC4\bt\66640a\ETXs~\USF\175140G`$\vG\DC1\1044421\128611/\1014458C>\SI" + ) + (Just (CookieLabel {cookieLabelText = "\SO\NAKeC/"})) + (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))})) + +testObject_Login_user_5 :: Login +testObject_Login_user_5 = + MkLogin + (LoginByEmail (unsafeEmailAddress "some" "example")) + ( plainTextPassword6Unsafe + "z>\1088515\1024903/\137135\1092812\b%$\1037736\143620:}\t\CAN\1058585\1044157)\12957\1005180s\1006270\CAN}\40034\EM[\41342\vX#VG,df4\141493\&8m5\46365OTK\144460\37582\DEL\44719\9670Z\"ZS\ESCms|[Q%\1088673\ENQW\\\1000857C\185096+\1070458\4114\17825v\180321\41886){\1028513\DEL\143570f\187156}:X-\b2N\EM\USl\127906\49608Y\1071393\1012763r2.1\49912\EOT+\137561\DC3\145480]'\1028275s\997684\42805.}\185059o\992118X\132901\11013\r\SUBNq6\1019605'\fd\RS\14503\1097628,:%\t\151916\73955QD\1086880\ESC(q4KDQ2zcI\DLE>\EM5\993596\&1\fBkd\DC3\ACK:F:\EOT\100901\11650O N\FS,N\1054390\1000247[h\DEL9\5932:xZ=\f\1085312\DC3u\RS\fe#\SUB^$lkx\32804 \rr\SUBJ\1013606\1017057\FSR][_5\NAK\58351\11748\35779\&5\24821\1055669\996852\37445K!\1052768eRR%\32108+h~1\993198\35871lTzS$\DLE\1060275\"*\1086839pmRE\DC3(\US^\8047Jc\10129\1071815i\n+G$|\993993\156283g\FS\fgU3Y\119068\ACKf)\1093562\SYN\78340\1100638/\NULPi\43622{\1048095j\1083269\FS9\132797\1024684\32713w$\45599\126246)Si\167172\29311FX\1057490j{`\44452`\999383\159809\&4u%\1070378P*\1057403\25422\DELC\RSR\SYN-\51098\1011541g\68666:S>c\15266\132940\DLEY\1066831~a)YW_J\1063076P\a+ U\1084883j\EMk\SOH\1096984\DC1\18679e\172760\175328,\5135g@\DC2\GSHXl.\ETB\153793\&2\DC3mY\1054891\tv?L8L\1074044N\133565\nb1j\1044024\148213xfQ=\\\ENQe\995818\1023862U\DC2p{\SO\1099404jd^@U\994269tP.\DC2Y%R`a\r\160622\&7}HnUf\132856m^7:\NAK=\52348>l\95313hwp27\149950jE\fx=!.\DC3]Ar\tw\DC4&\SUBk\194572s\1042820\4498I\146071\61461\1060645dsY\DLE\181922dX.\146295i]\151113\1028288\rWS\USU\1098732\SUB\49884\1083906\DLE\STXN~-\SO6\190031\1110322\\O\185165Jc\1052359\1071278\NULHSo\DLE-W\DC36\170321I\1068712)\99800={\99796h\27961\61707M\1022570FwJQ\1111976ck\SUB\CAN|UV-\NAK\SOH|\DC4;\f\156907\145795\ENQS\NAK.B\"D\163007#o*\126577\32988m\RS\1049834B3Gg;\DC1\\\180659\1098926\ENQ B^\SI\152630$e\39220\170037>fMgC\187276,o\128488\\?\1033955~/s\SOH?MMc;D18Ne\EOT\CAN)*\STX\GS\16268 +-- +-- 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.Presence + ( testObject_Presence_1, + testObject_Presence_2, + testObject_Presence_3, + ) +where + +import Data.Id +import Data.UUID qualified as UUID +import Imports +import Wire.API.Presence + +testObject_Presence_1 :: Presence +testObject_Presence_1 = + Presence + (Id . fromJust $ UUID.fromString "174ccaea-7f26-11ef-86cc-27bb6bf3b319") + (ConnId "wef") + (fromJust $ parse "http://example.com/") + Nothing + 0 + "" + +testObject_Presence_2 :: Presence +testObject_Presence_2 = + Presence + (Id . fromJust $ UUID.fromString "174ccaea-7f26-11ef-86cc-37bb6bf3b319") + (ConnId "wef3") + (fromJust $ parse "http://example.com/3") + (Just (ClientId 1)) + 12323 + "" -- __field always has to be "", see ToSchema instance. + +testObject_Presence_3 :: Presence +testObject_Presence_3 = + Presence + (Id . fromJust $ UUID.fromString "174ccaea-7f26-11ef-86cc-37bb6bf3b319") + (ConnId "wef3") + (fromJust $ parse "http://example.com/3") + (Just (ClientId 1)) + 0 + "" -- __field always has to be "", see ToSchema instance. diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Push.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Push.hs new file mode 100644 index 00000000000..fb91cd9cfc6 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Push.hs @@ -0,0 +1,82 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Wire.API.Golden.Manual.Push + ( testObject_Push_1, + testObject_Push_2, + ) +where + +import Data.Aeson qualified as A +import Data.Aeson.KeyMap qualified as KM +import Data.Id +import Data.List1 +import Data.Range +import Data.Set qualified as Set +import Data.UUID qualified as UUID +import Imports +import Wire.API.Push.V2 + +rcp1, rcp2, rcp3 :: Recipient +rcp1 = + Recipient + (Id . fromJust $ UUID.fromString "15441ff8-7f14-11ef-aeec-bbe21dc8a204") + RouteAny + RecipientClientsAll +rcp2 = + Recipient + (Id . fromJust $ UUID.fromString "2e18540e-7f14-11ef-9886-d3c2ff21d3d1") + RouteDirect + (RecipientClientsSome (list1 (ClientId 0) [])) +rcp3 = + Recipient + (Id . fromJust $ UUID.fromString "316924ee-7f14-11ef-b6a2-036a4f646914") + RouteDirect + (RecipientClientsSome (list1 (ClientId 234) [ClientId 123])) + +testObject_Push_1 :: Push +testObject_Push_1 = + Push + { _pushRecipients = unsafeRange (Set.fromList [rcp1]), + _pushOrigin = Nothing, + _pushConnections = mempty, + _pushOriginConnection = Nothing, + _pushTransient = False, + _pushNativeIncludeOrigin = False, + _pushNativeEncrypt = True, + _pushNativeAps = Nothing, + _pushNativePriority = HighPriority, + _pushPayload = singleton mempty + } + +testObject_Push_2 :: Push +testObject_Push_2 = + Push + { _pushRecipients = unsafeRange (Set.fromList [rcp2, rcp3]), + _pushOrigin = Just (Id . fromJust $ UUID.fromString "dec9b47a-7f12-11ef-b634-6710e7ae3d33"), + _pushConnections = Set.fromList [ConnId "sdf", ConnId "mempty", ConnId "wire-client"], + _pushOriginConnection = Just (ConnId "123"), + _pushTransient = True, + _pushNativeIncludeOrigin = True, + _pushNativeEncrypt = False, + _pushNativeAps = Just (apsData (ApsLocKey "asdf") ["1", "22", "333"]), + _pushNativePriority = LowPriority, + _pushPayload = + list1 + (KM.fromList [("foo" :: KM.Key) A..= '3', "bar" A..= True]) + [KM.fromList [], KM.fromList ["growl" A..= ("foooood" :: Text)], KM.fromList ["lunchtime" A..= ("imminent" :: Text)]] + } diff --git a/libs/galley-types/test/unit/Main.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/PushRemove.hs similarity index 75% rename from libs/galley-types/test/unit/Main.hs rename to libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/PushRemove.hs index 90b692813d3..bf4a823e47d 100644 --- a/libs/galley-types/test/unit/Main.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/PushRemove.hs @@ -15,14 +15,13 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Main - ( main, +module Test.Wire.API.Golden.Manual.PushRemove + ( testObject_PushRemove_1, ) where -import Imports -import Test.Galley.Types qualified -import Test.Tasty +import Test.Wire.API.Golden.Manual.Token (testObject_Token_1) +import Wire.API.Event.Gundeck -main :: IO () -main = defaultMain $ testGroup "Tests" [Test.Galley.Types.tests] +testObject_PushRemove_1 :: PushRemove +testObject_PushRemove_1 = PushRemove testObject_Token_1 diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/QualifiedUserClientPrekeyMap.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/QualifiedUserClientPrekeyMap.hs index 8f25987a882..0c3e30c2b7d 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/QualifiedUserClientPrekeyMap.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/QualifiedUserClientPrekeyMap.hs @@ -38,5 +38,6 @@ testObject_QualifiedUserClientPrekeyMap_2 = (Domain "epsilon.example.com", testObject_UserClientPrekeyMap_5), (Domain "zeta.example.com", testObject_UserClientPrekeyMap_6), (Domain "eta.example.com", testObject_UserClientPrekeyMap_7), - (Domain "theta.example.com", testObject_UserClientPrekeyMap_8) + (Domain "theta.example.com", testObject_UserClientPrekeyMap_8), + (Domain "meta.example.com", testObject_UserClientPrekeyMap_8) ] diff --git a/libs/gundeck-types/src/Gundeck/Types/Event.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SendActivationCode_user.hs similarity index 50% rename from libs/gundeck-types/src/Gundeck/Types/Event.hs rename to libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SendActivationCode_user.hs index a5d1ebf688e..c1d681df7f1 100644 --- a/libs/gundeck-types/src/Gundeck/Types/Event.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SendActivationCode_user.hs @@ -1,8 +1,6 @@ -{-# LANGUAGE OverloadedStrings #-} - -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2022 Wire Swiss GmbH +-- Copyright (C) 2024 Wire Swiss GmbH -- -- This program is free software: you can redistribute it and/or modify it under -- the terms of the GNU Affero General Public License as published by the Free @@ -17,27 +15,25 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Gundeck.Types.Event where +module Test.Wire.API.Golden.Manual.SendActivationCode_user where -import Data.Aeson -import Data.Aeson.KeyMap qualified as KeyMap -import Data.Json.Util -import Gundeck.Types.Push +import Data.ISO3166_CountryCodes +import Data.LanguageCodes qualified import Imports +import Wire.API.User +import Wire.API.User.Activation -newtype PushRemove = PushRemove PushToken - deriving (Eq, Show) - -instance FromJSON PushRemove where - parseJSON = withObject "push-removed object" $ \o -> - PushRemove <$> o .: "token" - -instance ToJSON PushRemove where - toJSON = Object . toJSONObject +testObject_SendActivationCode_1 :: SendActivationCode +testObject_SendActivationCode_1 = + SendActivationCode + { emailKey = unsafeEmailAddress "some" "example", + locale = Nothing + } -instance ToJSONObject PushRemove where - toJSONObject (PushRemove t) = - KeyMap.fromList - [ "type" .= ("user.push-remove" :: Text), - "token" .= t - ] +testObject_SendActivationCode_2 :: SendActivationCode +testObject_SendActivationCode_2 = + SendActivationCode + { emailKey = unsafeEmailAddress "some" "example", + locale = + Just (Locale {lLanguage = Language Data.LanguageCodes.CU, lCountry = Just (Country {fromCountry = VI})}) + } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs index 7ae86628304..9c8fa6666f5 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs @@ -33,6 +33,7 @@ module Test.Wire.API.Golden.Manual.UserEvent testObject_UserEvent_15, testObject_UserEvent_16, testObject_UserEvent_17, + testObject_UserEvent_18, ) where @@ -89,6 +90,7 @@ testObject_UserEvent_6 = ( UserUpdatedData (userId alice) (Just alice.userDisplayName) + alice.userTextStatus (Just alice.userPict) (Just alice.userAccentId) (Just alice.userAssets) @@ -98,6 +100,7 @@ testObject_UserEvent_6 = Nothing False (Just mempty) + Nothing ) ) @@ -107,7 +110,7 @@ testObject_UserEvent_7 = ( UserIdentityUpdated ( UserIdentityUpdatedData (userId alice) - (Just (Email "alice" "foo.example.com")) + (Just (unsafeEmailAddress "alice" "foo.example.com")) Nothing ) ) @@ -118,7 +121,7 @@ testObject_UserEvent_8 = ( UserIdentityRemoved ( UserIdentityRemovedData (userId alice) - (Just (Email "alice" "foo.example.com")) + (Just (unsafeEmailAddress "alice" "foo.example.com")) Nothing ) ) @@ -190,6 +193,27 @@ testObject_UserEvent_16 = testObject_UserEvent_17 :: Event testObject_UserEvent_17 = ClientEvent (ClientRemoved (ClientId 2839)) +testObject_UserEvent_18 :: Event +testObject_UserEvent_18 = + UserEvent + ( UserUpdated + ( UserUpdatedData + (userId alice) + (Just alice.userDisplayName) + alice.userTextStatus + (Just alice.userPict) + (Just alice.userAccentId) + (Just alice.userAssets) + alice.userHandle + (Just alice.userLocale) + (Just alice.userManagedBy) + Nothing + False + (Just mempty) + alice.userTeam + ) + ) + -------------------------------------------------------------------------------- alice :: User @@ -201,11 +225,13 @@ alice = qDomain = Domain {_domainText = "foo.example.com"} }, userIdentity = Nothing, + userEmailUnvalidated = Nothing, userDisplayName = Name "alice", + userTextStatus = rightToMaybe $ mkTextStatus "text status", userPict = Pict {fromPict = []}, userAssets = [], userAccentId = ColourId {fromColourId = 1}, - userDeleted = True, + userStatus = Deleted, userLocale = Locale { lLanguage = Language L.TN, @@ -214,7 +240,7 @@ alice = userService = Nothing, userHandle = Nothing, userExpire = Nothing, - userTeam = Nothing, + userTeam = Just $ Id (fromJust (UUID.fromString "bb843450-b2f5-4ec8-90bd-52c7d5f1d22e")), userManagedBy = ManagedByWire, userSupportedProtocols = defSupportedProtocols } @@ -228,11 +254,13 @@ bob = qDomain = Domain {_domainText = "baz.example.com"} }, userIdentity = Nothing, + userEmailUnvalidated = Nothing, userDisplayName = Name "bob", + userTextStatus = rightToMaybe $ mkTextStatus "text status", userPict = Pict {fromPict = []}, userAssets = [], userAccentId = ColourId {fromColourId = 2}, - userDeleted = False, + userStatus = Active, userLocale = Locale { lLanguage = Language L.CA, diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Protobuf.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Protobuf.hs index 81b85c39424..37f444be782 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Protobuf.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Protobuf.hs @@ -57,6 +57,9 @@ tests = ( testObject_QualifiedNewOtrMessage_user_9, "testObject_QualifiedNewOtrMessage_user_9.protobuf" ), + ( testObject_QualifiedNewOtrMessage_user_10, + "testObject_QualifiedNewOtrMessage_user_10.protobuf" + ), ( testObject_QualifiedNewOtrMessage_user_11, "testObject_QualifiedNewOtrMessage_user_11.protobuf" ), diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs index a7db1f7594d..d428e941c32 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs @@ -17,6 +17,7 @@ module Test.Wire.API.Golden.Runner ( testObjects, + testToJSON, protoTestObjects, testFromJSONFailure, testFromJSONFailureWithMsg, @@ -46,8 +47,19 @@ testObjects = fmap (\(obj, path) -> testCase path $ testObject obj path) testObject :: forall a. (Typeable a, ToJSON a, FromJSON a, Eq a, Show a) => a -> FilePath -> Assertion testObject obj path = do + assertJSONIsGolden obj path + assertEqual + (show (typeRep @a) <> ": FromJSON of " <> path <> " should match object") + (Success obj) + (fromJSON $ toJSON obj) + +testToJSON :: forall a. (Typeable a, ToJSON a) => a -> FilePath -> TestTree +testToJSON obj path = testCase path $ assertJSONIsGolden obj path + +assertJSONIsGolden :: forall a. (Typeable a, ToJSON a) => a -> FilePath -> Assertion +assertJSONIsGolden obj path = do let actualValue = toJSON obj :: Value - actualJson = encodePretty' config actualValue + actualJson = encodePretty' encodeConfig actualValue dir = "test/golden" fullPath = dir <> "/" <> path createDirectoryIfMissing True dir @@ -60,20 +72,17 @@ testObject obj path = do <> ": ToJSON should match golden file: " <> path <> "\n\nexpected:\n" - <> cs (encodePretty' config expectedValue) + <> cs (encodePretty' encodeConfig expectedValue) <> "\n\nactual:\n" - <> cs (encodePretty' config actualValue) + <> cs (encodePretty' encodeConfig actualValue) <> "\n\ndiff:\n" - <> cs (encodePretty' config (AD.diff expectedValue actualValue)) + <> cs (encodePretty' encodeConfig (AD.diff expectedValue actualValue)) ) (expectedValue == actualValue) - assertEqual - (show (typeRep @a) <> ": FromJSON of " <> path <> " should match object") - (Success obj) - (fromJSON actualValue) assertBool ("JSON golden file " <> path <> " does not exist") exists - where - config = defConfig {confCompare = compare, confTrailingNewline = True} + +encodeConfig :: Config +encodeConfig = defConfig {confCompare = compare, confTrailingNewline = True} protoTestObjects :: forall m a. diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_14.json b/libs/wire-api/test/golden/fromJSON/testObject_LockableFeature_team_14.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_14.json rename to libs/wire-api/test/golden/fromJSON/testObject_LockableFeature_team_14.json diff --git a/libs/wire-api/test/golden/fromJSON/testObject_NewUser_user_3-2.json b/libs/wire-api/test/golden/fromJSON/testObject_NewUser_user_3-2.json index 7e5b43e51f4..36d095318f1 100644 --- a/libs/wire-api/test/golden/fromJSON/testObject_NewUser_user_3-2.json +++ b/libs/wire-api/test/golden/fromJSON/testObject_NewUser_user_3-2.json @@ -1,6 +1,6 @@ { "assets": [], - "email": "]󸼰,\u00177%\u0004\u00145^5𗨙𣾿\u001d䍕@xW)\u0006", + "email": "some@mail", "expires_in": 378975, "name": "\u0000`)\u0007|>}\u00195z𑈣\t>w\u000e󵾱" } diff --git a/libs/wire-api/test/golden/fromJSON/testObject_Presence_3.json b/libs/wire-api/test/golden/fromJSON/testObject_Presence_3.json new file mode 100644 index 00000000000..1089a740c73 --- /dev/null +++ b/libs/wire-api/test/golden/fromJSON/testObject_Presence_3.json @@ -0,0 +1,6 @@ +{ + "client_id": "1", + "device_id": "wef3", + "resource": "http://example.com/3", + "user_id": "174ccaea-7f26-11ef-86cc-37bb6bf3b319" +} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_1.json b/libs/wire-api/test/golden/testObject_Activate_user_1.json index 1578b22ac2f..d4cc0474516 100644 --- a/libs/wire-api/test/golden/testObject_Activate_user_1.json +++ b/libs/wire-api/test/golden/testObject_Activate_user_1.json @@ -1,5 +1,5 @@ { - "code": "HUUpJQ==", - "dryrun": true, - "phone": "+45520903" + "code": "fg==", + "dryrun": false, + "key": "e3sm9EjNmzA=" } diff --git a/libs/wire-api/test/golden/testObject_Activate_user_10.json b/libs/wire-api/test/golden/testObject_Activate_user_10.json deleted file mode 100644 index 93b403e17a8..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_10.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "kcvCq2A=", - "dryrun": false, - "key": "1szizA==" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_11.json b/libs/wire-api/test/golden/testObject_Activate_user_11.json deleted file mode 100644 index 87251a5b113..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_11.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "MZpmmg==", - "dryrun": false, - "email": "\u00034\u001a@" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_12.json b/libs/wire-api/test/golden/testObject_Activate_user_12.json deleted file mode 100644 index 601ac6c7255..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_12.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "sScBopoNTb0=", - "dryrun": true, - "key": "V3mr5D4=" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_13.json b/libs/wire-api/test/golden/testObject_Activate_user_13.json deleted file mode 100644 index 638148bec02..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_13.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "ysvb", - "dryrun": false, - "key": "haH9_sUNFw==" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_14.json b/libs/wire-api/test/golden/testObject_Activate_user_14.json deleted file mode 100644 index fd2763b59c3..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_14.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "hQ==", - "dryrun": true, - "phone": "+13340815619" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_15.json b/libs/wire-api/test/golden/testObject_Activate_user_15.json deleted file mode 100644 index ce73a7296db..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_15.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "biTZ", - "dryrun": false, - "email": "圤W[󾒿G󳍬]{\n@ V8󲏽\u0015*" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_16.json b/libs/wire-api/test/golden/testObject_Activate_user_16.json deleted file mode 100644 index fcbcd59dbf7..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_16.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "5W4=", - "dryrun": true, - "phone": "+77635104433" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_17.json b/libs/wire-api/test/golden/testObject_Activate_user_17.json deleted file mode 100644 index 029ba0efa82..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_17.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "ShjEcgx6P0Hs", - "dryrun": false, - "phone": "+556856857856" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_18.json b/libs/wire-api/test/golden/testObject_Activate_user_18.json deleted file mode 100644 index f1fdb6c2dae..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_18.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "xRvktQ==", - "dryrun": false, - "email": "2􎖰B􌕾\u00032\u001f􇰋@v\u0001\u000e󶃯/e" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_19.json b/libs/wire-api/test/golden/testObject_Activate_user_19.json deleted file mode 100644 index 78325351be1..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_19.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "", - "dryrun": false, - "key": "1fCrdg==" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_2.json b/libs/wire-api/test/golden/testObject_Activate_user_2.json index d4cc0474516..849cc2f3b8a 100644 --- a/libs/wire-api/test/golden/testObject_Activate_user_2.json +++ b/libs/wire-api/test/golden/testObject_Activate_user_2.json @@ -1,5 +1,5 @@ { - "code": "fg==", + "code": "", "dryrun": false, - "key": "e3sm9EjNmzA=" + "email": "valid1j28hfna@iagh28nuwkas" } diff --git a/libs/wire-api/test/golden/testObject_Activate_user_20.json b/libs/wire-api/test/golden/testObject_Activate_user_20.json deleted file mode 100644 index 6e3a3bb7641..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_20.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "7PtclAevMzA=", - "dryrun": false, - "phone": "+893051142276" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_3.json b/libs/wire-api/test/golden/testObject_Activate_user_3.json index bc9eb55c56f..a54047d9a78 100644 --- a/libs/wire-api/test/golden/testObject_Activate_user_3.json +++ b/libs/wire-api/test/golden/testObject_Activate_user_3.json @@ -1,5 +1,5 @@ { - "code": "OAbwDkw=", + "code": "61wG", "dryrun": true, - "phone": "+44508058" + "key": "DkV9xQ==" } diff --git a/libs/wire-api/test/golden/testObject_Activate_user_4.json b/libs/wire-api/test/golden/testObject_Activate_user_4.json index fd7147e34c9..601ac6c7255 100644 --- a/libs/wire-api/test/golden/testObject_Activate_user_4.json +++ b/libs/wire-api/test/golden/testObject_Activate_user_4.json @@ -1,5 +1,5 @@ { - "code": "811p-743Gvpi", - "dryrun": false, - "phone": "+97751884" + "code": "sScBopoNTb0=", + "dryrun": true, + "key": "V3mr5D4=" } diff --git a/libs/wire-api/test/golden/testObject_Activate_user_5.json b/libs/wire-api/test/golden/testObject_Activate_user_5.json deleted file mode 100644 index 8975d735dc0..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_5.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "", - "dryrun": false, - "email": "󴴺\u0000􆞵@k\\\u0001a\u0016*𫅳" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_6.json b/libs/wire-api/test/golden/testObject_Activate_user_6.json deleted file mode 100644 index 844c5c091d7..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_6.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "FXrNll0Kqg==", - "dryrun": false, - "email": "􍧃i>󶃾Ha!@" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_7.json b/libs/wire-api/test/golden/testObject_Activate_user_7.json deleted file mode 100644 index 8622155b543..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_7.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "8yl3qERc", - "dryrun": false, - "key": "jQ==" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_8.json b/libs/wire-api/test/golden/testObject_Activate_user_8.json deleted file mode 100644 index 681812203b2..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_8.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "NF20Avw=", - "dryrun": true, - "phone": "+3276478697350" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_9.json b/libs/wire-api/test/golden/testObject_Activate_user_9.json deleted file mode 100644 index a54047d9a78..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_9.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "61wG", - "dryrun": true, - "key": "DkV9xQ==" -} diff --git a/libs/wire-api/test/golden/testObject_ActivationResponse_user_1.json b/libs/wire-api/test/golden/testObject_ActivationResponse_user_1.json index 1506d784408..5af83770fe9 100644 --- a/libs/wire-api/test/golden/testObject_ActivationResponse_user_1.json +++ b/libs/wire-api/test/golden/testObject_ActivationResponse_user_1.json @@ -1,5 +1,5 @@ { - "email": "𨠞\rZ\u0007\u001b@p𠋁", + "email": "some@example", "first": false, "sso_id": { "subject": "me@example.com", diff --git a/libs/wire-api/test/golden/testObject_ActivationResponse_user_10.json b/libs/wire-api/test/golden/testObject_ActivationResponse_user_10.json index b98164f3fec..db708bf2a76 100644 --- a/libs/wire-api/test/golden/testObject_ActivationResponse_user_10.json +++ b/libs/wire-api/test/golden/testObject_ActivationResponse_user_10.json @@ -1,4 +1,4 @@ { - "email": "\u00063@\u000c󾇏􅞻\u0004󴼐P", + "email": "some@example", "first": false } diff --git a/libs/wire-api/test/golden/testObject_ActivationResponse_user_2.json b/libs/wire-api/test/golden/testObject_ActivationResponse_user_2.json index 7f4dc0a99de..db708bf2a76 100644 --- a/libs/wire-api/test/golden/testObject_ActivationResponse_user_2.json +++ b/libs/wire-api/test/golden/testObject_ActivationResponse_user_2.json @@ -1,4 +1,4 @@ { - "email": "foo@example.com", + "email": "some@example", "first": false } diff --git a/libs/wire-api/test/golden/testObject_ActivationResponse_user_3.json b/libs/wire-api/test/golden/testObject_ActivationResponse_user_3.json index 06c370721d9..db708bf2a76 100644 --- a/libs/wire-api/test/golden/testObject_ActivationResponse_user_3.json +++ b/libs/wire-api/test/golden/testObject_ActivationResponse_user_3.json @@ -1,4 +1,4 @@ { - "email": "✯*;'R\u0019\u000f󼇭󾌏@Gw:[T8蚅", + "email": "some@example", "first": false } diff --git a/libs/wire-api/test/golden/testObject_ActivationResponse_user_4.json b/libs/wire-api/test/golden/testObject_ActivationResponse_user_4.json index 2fc240718b3..aebb0287470 100644 --- a/libs/wire-api/test/golden/testObject_ActivationResponse_user_4.json +++ b/libs/wire-api/test/golden/testObject_ActivationResponse_user_4.json @@ -1,4 +1,4 @@ { - "email": "h\nPr3@", + "email": "some@example", "first": true } diff --git a/libs/wire-api/test/golden/testObject_ActivationResponse_user_5.json b/libs/wire-api/test/golden/testObject_ActivationResponse_user_5.json index f49cd246eb0..db708bf2a76 100644 --- a/libs/wire-api/test/golden/testObject_ActivationResponse_user_5.json +++ b/libs/wire-api/test/golden/testObject_ActivationResponse_user_5.json @@ -1,4 +1,4 @@ { - "email": "7󾚲m𗑀\u0008􌐍@AJX*s&𪐽󱛆p", + "email": "some@example", "first": false } diff --git a/libs/wire-api/test/golden/testObject_ActivationResponse_user_7.json b/libs/wire-api/test/golden/testObject_ActivationResponse_user_7.json index 1ffefc7f84b..aebb0287470 100644 --- a/libs/wire-api/test/golden/testObject_ActivationResponse_user_7.json +++ b/libs/wire-api/test/golden/testObject_ActivationResponse_user_7.json @@ -1,4 +1,4 @@ { - "email": "𘅮@", + "email": "some@example", "first": true } diff --git a/libs/wire-api/test/golden/testObject_ActivationResponse_user_8.json b/libs/wire-api/test/golden/testObject_ActivationResponse_user_8.json index 38b2903f340..aebb0287470 100644 --- a/libs/wire-api/test/golden/testObject_ActivationResponse_user_8.json +++ b/libs/wire-api/test/golden/testObject_ActivationResponse_user_8.json @@ -1,4 +1,4 @@ { - "email": "bar@example.com", + "email": "some@example", "first": true } diff --git a/libs/wire-api/test/golden/testObject_ActivationResponse_user_9.json b/libs/wire-api/test/golden/testObject_ActivationResponse_user_9.json index 83a3641e055..db708bf2a76 100644 --- a/libs/wire-api/test/golden/testObject_ActivationResponse_user_9.json +++ b/libs/wire-api/test/golden/testObject_ActivationResponse_user_9.json @@ -1,4 +1,4 @@ { - "email": "\u0005?@", + "email": "some@example", "first": false } diff --git a/libs/wire-api/test/golden/testObject_CannonId_1.json b/libs/wire-api/test/golden/testObject_CannonId_1.json new file mode 100644 index 00000000000..e16c76dff88 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_CannonId_1.json @@ -0,0 +1 @@ +"" diff --git a/libs/wire-api/test/golden/testObject_CannonId_2.json b/libs/wire-api/test/golden/testObject_CannonId_2.json new file mode 100644 index 00000000000..5149c12b38f --- /dev/null +++ b/libs/wire-api/test/golden/testObject_CannonId_2.json @@ -0,0 +1 @@ +"sdfiou" diff --git a/libs/wire-api/test/golden/testObject_CannonId_3.json b/libs/wire-api/test/golden/testObject_CannonId_3.json new file mode 100644 index 00000000000..56bcab3744e --- /dev/null +++ b/libs/wire-api/test/golden/testObject_CannonId_3.json @@ -0,0 +1 @@ +"1!_*`'\"" diff --git a/libs/wire-api/test/golden/testObject_ClientCapabilityList_3.json b/libs/wire-api/test/golden/testObject_ClientCapabilityList_3.json new file mode 100644 index 00000000000..89c37fb330c --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientCapabilityList_3.json @@ -0,0 +1,5 @@ +{ + "capabilities": [ + "legalhold-implicit-consent" + ] +} diff --git a/libs/wire-api/test/golden/testObject_ClientCapabilityList_4.json b/libs/wire-api/test/golden/testObject_ClientCapabilityList_4.json new file mode 100644 index 00000000000..fe51488c706 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientCapabilityList_4.json @@ -0,0 +1 @@ +[] diff --git a/libs/wire-api/test/golden/testObject_ClientCapabilityList_5.json b/libs/wire-api/test/golden/testObject_ClientCapabilityList_5.json new file mode 100644 index 00000000000..e7711560546 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientCapabilityList_5.json @@ -0,0 +1,4 @@ +[ + "legalhold-implicit-consent", + "consumable-notifications" +] diff --git a/libs/wire-api/test/golden/testObject_ClientCapability_2.json b/libs/wire-api/test/golden/testObject_ClientCapability_2.json new file mode 100644 index 00000000000..2b3d761f339 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientCapability_2.json @@ -0,0 +1 @@ +"consumable-notifications" diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_1.json b/libs/wire-api/test/golden/testObject_ClientV6_user_1.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_1.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_1.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_10.json b/libs/wire-api/test/golden/testObject_ClientV6_user_10.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_10.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_10.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_11.json b/libs/wire-api/test/golden/testObject_ClientV6_user_11.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_11.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_11.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_12.json b/libs/wire-api/test/golden/testObject_ClientV6_user_12.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_12.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_12.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_13.json b/libs/wire-api/test/golden/testObject_ClientV6_user_13.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_13.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_13.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_14.json b/libs/wire-api/test/golden/testObject_ClientV6_user_14.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_14.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_14.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_15.json b/libs/wire-api/test/golden/testObject_ClientV6_user_15.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_15.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_15.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_16.json b/libs/wire-api/test/golden/testObject_ClientV6_user_16.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_16.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_16.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_17.json b/libs/wire-api/test/golden/testObject_ClientV6_user_17.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_17.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_17.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_18.json b/libs/wire-api/test/golden/testObject_ClientV6_user_18.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_18.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_18.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_19.json b/libs/wire-api/test/golden/testObject_ClientV6_user_19.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_19.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_19.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_2.json b/libs/wire-api/test/golden/testObject_ClientV6_user_2.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_2.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_2.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_20.json b/libs/wire-api/test/golden/testObject_ClientV6_user_20.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_20.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_20.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_3.json b/libs/wire-api/test/golden/testObject_ClientV6_user_3.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_3.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_3.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_4.json b/libs/wire-api/test/golden/testObject_ClientV6_user_4.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_4.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_4.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_5.json b/libs/wire-api/test/golden/testObject_ClientV6_user_5.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_5.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_5.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_6.json b/libs/wire-api/test/golden/testObject_ClientV6_user_6.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_6.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_6.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_7.json b/libs/wire-api/test/golden/testObject_ClientV6_user_7.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_7.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_7.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_8.json b/libs/wire-api/test/golden/testObject_ClientV6_user_8.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_8.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_8.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_9.json b/libs/wire-api/test/golden/testObject_ClientV6_user_9.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_9.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_9.json diff --git a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_1.json b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_1.json index 7328dbef7ee..9e0a77129bd 100644 --- a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_1.json +++ b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_1.json @@ -1,5 +1,5 @@ { "code": "uLtYG9FEhpfNHht=ndxYbhEsOaZJJTPjEsBsnJt0UngpmW5OvqpW2F9E5VuFikdraC8s1xMQs9yOzKlgdV4rf371UTMjWzc59HdqfqFDx8=ARnQtIJ8VyAnd784fYJv2A=IiSQJhfc=tc7UKa4n_TN9Hq7BvAQ0YLBFuCRsH1cBsr35-I0aEKev_48AFCC2r2LceURv7tsXpODU=pjneuYFQSD2u8GmiQ3NxRqJEiIfvj3IE_S2gFTqb3Qod=rvVoT7yAejNg=F89T6bacNnzM-sdRhB7ZoQrQYQRc7j7d_1hDOzKsmkBVqpZ3466SwlHld09GyIAYBOo7TipyvgBENFlXnor2sPS2TwCtzmMdyMxhEt780DAdUgiasCsS08_rFrx3j8_wNCBzYsWRTYi7LSaY_IxpcH-mOkH86L=8SAMcCs_pJpKsoWa1EY4Ep0h8jTspHT-6tKd2s0gT_v5GvTPEg8BZyz04gt6I5JgdSrOJ1A0=w_zy4O-KSS-ba73v2v4p3x-N19X88brW3VCwbqgS_G3DAMDEr76Ekn7q0UMAd2MR13SgKWjM35lFtS6vN6b5a4QVqIxOqAvA2EPHV2UY4zGhJsgl7KpgtCzUMKIl-mTyjXP_a_c9y0uu9u6I", - "email": "\u0002Q=萱k@", + "email": "some@example", "password": "Tu􆢵8\u0013\"9\u000cB]𫧆\u0010<􈋋.C<6W sgS8_􈓌\n\nD$A\u0010鰞I\u0003󳋷跌\u001d>𒑖S𒅘EI_󴽘 a[𤊰󲼶𝄖\u00170﮽\t󺉨~죻$`\u0011𠘄y.Q)\u0016a󵚭\u000f^𩗉󰑹+\u0008E𢷞P\u000f\u001a\u000eg}>🤷>\u0015&HS𫭱󱙼M\u0010\"~?󺜵gd刍\u0012\\)􃄘F\u001fI􎟛V🌘\u0006𮨕[T5'wP\u000f\u0014!y+d\u001f𬒦󶌬󴼺\u0007h\u00122r\u0016~󽇘I\u0016YO\u0004􋯶sT𢍨6q\u0010Xjp(ᰛ\u000ec\u001c\u0007􆇮fc>8?Sy\u001c\u000f몋\nN\u000b&O俶􀟉\u0006/\u00178𮜹􋷬𩌣𗽺k笷𗖩b\u0017]\\\u0018\u0011;3 j3F+􉓛.0冀(p􅅽H\u0017j⾙d\u000e𢽵W] Hz@Eb􄂄zW}\u001f칛!\u0016;󶶏T\u0013![*BS􅌧w]쎡3\u0006􀴕𫐚G.A\u0019}%\u0014\u000bk𪅓\u0013Tf\u0011\u0003󶔜\u0004&󳖞o.ﻧE?jK|6z􇭾BC\u0011ዏ𮔟!>\u0001󱊸\t;Y廒\u0001#𝨄\u0016`w%\u0002\u0018𢌉zT􏲈2􏬙\u0018>Ü𗋢𧔨&7 8U\u001e4籢􉓺􀃫--\"\u0017" } diff --git a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_12.json b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_12.json index 797d2e40a6f..016789da3a9 100644 --- a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_12.json +++ b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_12.json @@ -1,5 +1,5 @@ { "code": "GkNBvt5WkpZiqOtxpVKuBy8dXcbWuV8x4ejoV3EHdIAU=fZo3d_PjWQ36EzyO9eGbt5F8oQ=7vBzrTr9dpeETyJQWi9Vu38Efi7Dz-zsBvBp9p=AszTX69gzjPQ-xgcPvCw2Kvv6EStPojy", - "email": "(𢶈\u0019=]=\u0007@𪸹\u0016\u0008\nﮛ\u000b&􈝾", + "email": "some@example", "password": "\u00118莱\u0010\u0011꡶~KSy0􌍉\u001f𥌴lR􍴸+!\u0018\u0018[kv\u0016<\u0018􀘑kW󴚛𧽺+刻i[癭e2[u#\u0018 " } diff --git a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_14.json b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_14.json index 6d6080bb458..c64db5a3a88 100644 --- a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_14.json +++ b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_14.json @@ -1,5 +1,5 @@ { "code": "FSTMhXuS1rYF_f_3aJfy8sn7CaY7BMCg6onJCAqtnt54fEvCkS40ml06ufrX9wvy192yCErw5Xei33_FoSQmC0RAjRN9eLFSBq15MclWbPrIsrwluYCiLmIB72IaR7ig8xGPv3-H8v=J_5xfvvpYRYSFZMZvTwTHKqaRL_uF8r=JULb6AQnLUG6__-nBrCq=91TRJ26VknMDuFrk-0Tfu72OJ73LrGfJqmWCR7gcFeyACyR17n3FI4GQquQ5Bb5qbfl5KZc7W_E3H=5sScZCa9r2Hj9ot5noSq-9nq2NlptoDc4mYTaWklhfbNCT8Wn2=3T8GfAx9nYW__2ZyAPlW9NPmbRSj5FYqqJAprLVa4GrT=PELXTFIba3inReJYtM4thgQ2LAgZYew4L0YGpIMOgr=uFKs3I3u4Bgd_77uNR-wayH3ENL0A97aV7p9DLLC6A2FeVugc2jMn1wViS06PkxJoM5ZtGZkibUTuycstG3VmGtC8ZMR3q2lAVNsfsiugBUZLg=MtzPz2Pqe=QaxCNq5N04ekL", - "email": "󿦘Q?􇩑􌾱@𫪞󴩴䤐", + "email": "some@example", "password": "A􈉖9YM4fO􊈾s􋍂\u0005\u0012\u00081𫸍\u0000#􉥳𬶼󸻰f󾏞ញ,z$h#\u000c\u0003࣫\u001f5󳑍识.𩹯 􄳩\tiW\u001a𭳌V\u001a5\u001b\u001a\u0001􇱒\u0013\u001df-J𡯠}0\u0012(&R*}𦗂6\u0019\u0012$\u0012e\u0017𪎏@ 󰺑M5N狭􆇳O㒅\u000f:]䝃\u0003󻻌됣*󸎅qGﴬ<&􏭝罺\u0006􄓃\u001b{\r󵼱l󲎿\\\u0007\u0019\t.𨩢󿸈\u0006{4g\u0005\u0017\u0005𝔎\u0018)a{=J#XSJ\t戍/\u000cu󷙞v2YZY-k$ #4𥵽^\\􎊶\u0013>b/5-Z}𢬢\r>󼇈\u0003\u0008\u001cRS9\u000bE&\u0018\u000fz#m5z\u0006\u0012pe𧱻0𩭠픝Q𣗚f,sH\u0015\u0013@󱯄󳴈&l󻅮\u0015\u0005.f$s\u000c\u0007\u0000趌F\"\u001d\u0011\u0005H\r𢾱j󰯜?\r𝀿𗎡lf􏗑􈩇s'󹭞􊪜𭬕t-\u0007sh\u001f󿼛w󶦖\u001bj𡘝L􅎿~H\u0017-🖎\u0019vml󷚍Eug\u000e\u000bDTR\u0005TO\u0016󷞟|\u0005􌺊\u0001\\\u00040Q*𨖃Ly4󵦨It恭w" } diff --git a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_15.json b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_15.json index c0b383904c7..99e2dadb4d5 100644 --- a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_15.json +++ b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_15.json @@ -1,5 +1,5 @@ { "code": "hmgODqiry_V_t87ih4Ezo7GS8C38DYKENIE2t5nRiJMdagPBW-lTEhID3_8_ApDfxAfSNxAF03y2L8MCLqWWsX_wxkaLYtAI39FLtZZAwxHkSRRazNp7LAc_3QzGXR4O_iFiCqo0f3ZbmODskuoeNVUGBBPJhQ=uw1yVKyMVHojWD16khERjcHww2=hSmqUdh3W-46WPWaZe7IRN0_gk_UaBGwdMb4aDcTHJ6jIaTfQ58djcLVGrKpuO1xO=eQ2BjLJiK6Ik30JgICpvS5ZuumMjgkNFKtHwCu0C-E-oUDUmi3sWKkFQPCxpIy0Ol0SAyN2llCWAADjTR6SW-zRT4qDQNbtDe8nKWpJxZYjFj=IvyBHaK1q6NjPsrXQBEUfajtkh7OwbQwqOOBk5nt8RPP7xwUewzHEtkQUJUjbgGh80nuOdC7sMa2zOSEOy33oC1bjncA23BsaJoisQbFfju_UWiCSyDD-oUXsWkKR1cMGmwyVpf1IpZRnQq_8dwpgMKL4j4ehPxPrVBefQPmzdoK4nncLDB_zDKBBn4M5nbqDsLmO9OqSKeDH6tg=uKTaftrDK2w6Mhfo_fSZOsJAEouS02TJwr6vE_VlJbiOCPysMdVmCdn6Ai2n-p_WlwFoBIHLPkVnx7yYyskHuUMhYQfaq8=CHCwa8CDyOGu=cZVxOd6mTHRD=mXc2_cgkYJ94pdZOL0", - "email": "6\u000bF\u0004]\u001b􉡴.'@JEe􊐼􈼡2dK󳘱", + "email": "some@example", "password": "\u000e䆛@?󼡏\u0013\tMt\u001d\u001cD!Y𦷤􈭕9\u0013𓍟􌈯􉕺 _P󶐹/3𫑳bZ7鑴.u\u000ej㒛[𒎊\u0014𣺨%7Y󲝈󲷳)ﳀh𧁁\u000cYt𨥔󼽈#%y\u0003O\u0000옛Qo%E]𬫪6컅)\u0018\u0016,􂲁󸡜􈴸󻣝􎝀>砯\u0006dSfz\u001cX󶿪TU∃jM𧟥S🅞\u0014qE\u001cJ\u0016\u0000􁧥\u001aFg9󰘵x󽠏􏞞\u000f\u00019\u0014􌞒􀔶厡󽭶aSIN1s\t🜯\u001c y\u0008\u0002+\\G𬄫⪕9희 M#|=Bhd\u000f\u001et{a\u0002Az.𫮗㥬\"􏠤\r�I[\u0007T\u0017`ㄌ~3􉙟\u000fn󲸣}c;􀟺9\u000f󰡣/n󿾴5-\u0006Z=󻩎35𣮯]B\u0008\u001c7\u0005\u0010Gl'y\u0015b|4󱦦=\u0010g*y􂃡󰈗3Ej\u001e􏥯􉫯:𨵈􍥧p\u0005V\u0016𖧰\u001a\u0007DBA\u0018􁷡𨥬𬠨i_x𡰊#\u000bh5􈹓d􍺲𮜑`𮢊\u0011h\u0019K\nW\u0007ꊊ⢨Y\u000e#3`\u001f\u0018l􃁞\u001a[pr\u000c\u001fX)\u0018\u001a􍊩\u00183NrV3\r}\r𨉦\n\u0012h\\􀍂󼧸=쁐%*^|vp咝ix\u0011\u001bi𩂎78\u0007𓇉 g󹢷`\t?󸹮'W=􈮵\u001ej0\u0016k`Y\u0006\u0001\u000f4!𭮤𫕼90󸒿𭾭䙣FD!􆫐&𦩫)F\u0017&𣏩\u0007쟼\u0007K].^9~\u0012J6󷕕7𥴀}\n]W𫔟0e\u0005L𦕽a\"䗒ꈵ;\u00198ꃽ]J,m>\u0008\u000b^󹁲j60G󳟰1fMGY;[<2q󸡫􍗋󸌪O\u0006\u0014. +Q𣴻L\"\u0000QQ􈃎pfK𦅹.i􏅥\u0006\u0016𣊧g4Z\u001b􋄞yh\u0017 􍸱껷\u0012,\u000b}\u000f\u0015oSV𭉆lUZ?Fi^H𘖧\u001e􎞳oR\\\u000f'b\u0013a\u0002\u000eT\u001f𭀳Xm\u0004󹑸{Y\u000fQDO1OZ@􋯉􌋨)􂱾V\\F\u0010󵥀􃺚{0D\u0008j\u0012l-\u0003󿥵󻌷􊍨禲O\u0019a􍄩Jẻ^\u0018?:󾌫󳙷󻽜gz𣮸\u0014瘷\u0002🅈􁒊]N`􏼢𡷈\u0016t2nqJ!p\u000bI𖡛H\"5\u0011y7l􂯮EZ|kA\t㲺l\u001c7t\n\u0001?t􏥙\u0019.𮭖zJ󱦌󿀙WA{􁕵􊥫m\r\u000b4:㫡{􇆓\u0005권祱;i雐𤊔􈭌\u0019~󻨇𗑊\n,y7D\u0004⎭\u0010􀢷Zo􏵲MUD𣼄􆇆\u0011|'Q뎜\n2󴑥𢊗Nk󹽩@\u000eDia-0\tD\u0011󼻃􂇵\u0007*L􍿧\u001c\u0019󻨡\n\u00059󾒴\u001eF\u001dK𠯙\u0000\rA_􎜼{\\\"𔓷󱩯T@𗉉\u001bV戴󷝔l\u0012Ea🄵􉐫\u0017z7󵯓P`䙂\u000f𔘢S`u󿅋\n􉌯t\\wfㅐm\u0019 󶋪􅌲R􄄦\">[N\n\"ǧIQ\u001e2s\u0003J6\u001cU%錬HnDm\u0002@\u0004T 檻imm\u000ek\u0000J?*\u0002\u0012+hhIj$2𔓼𩜤􎍨\r.􎈨𣎱􌕭+o\u001ag􊕺o\\\u0007,\u0002\\𧣡\u001a󸮌$[􅒮Q\u0018󼥯#\u0010\u000f󳻺􊇻𤠏\\谠B\u0016s_ꗑ9\u000bZ𘣙avv:􅄱\u0019𐔄rv󰯚󾒜&𗼼,'/)􅊫'6􏩚5)\u0018꾜1XG􇃴>&󳋸0\t,N\u00014H􊦑\u001aR\tNT^;\u000fs;欐 󰹰#𨠢|uKo󽝘𡖮\u0016.􂏝af󼵒vu%󽁆adVJ󼸑r𦼩㩻#\u0018󼚿z􃍛\u001d_d\u0015\u0003Ql+&fe&2󶟢hbj􌗌[b\u0001kXF󺱱QZ\u0007*(\u000cOqN3f\u001b" } diff --git a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_17.json b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_17.json index 6c4c26edd0b..aba3cd9ee37 100644 --- a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_17.json +++ b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_17.json @@ -1,5 +1,5 @@ { "code": "3gq6=cswHZ9ri64_HJPb0GHqnIvQsgakJ=HkufysG_pLk8piT7CmIFMoO0lif83sPks6mv-UWRbQCOyTECbFMlPIR57uJSHFmxolrFw", - "email": "퉁[僥@靖wC\u001aE䕣𫶙", + "email": "some@example", "password": "\u0015\u0003\u0015\u001eVB-^K[QI!h𥵶𑗍􈯞Z豂,\r𒂩>\u001cMI𝨝󾎴㌉44\u000fiw\u00157(\u000bu𮬯oy\u0012\u0015,GLefv}q2Zn@jp\u001aRucnj쏩1𬂡BC`\u0019󺆴\u0011dA\u0007w踇\\)yrkSp􀿗䪑􈤰d\u0006b􍥮V'\u0008{\u001b\u000b5𭤼\u0001Lp􃐝\u0002b𘆞69\u0007o\u000b󽠪䷙7㜹\u0011𭜣\u00179qT\u0000\u0015\u001fN{=w󱋏P􅄿>􈍆􂘾U\u0011L𓋧\u000fqV\u0005<\u0004\u001d?􅪺X􈂆rtX$F!R\u0010\u0002𢴜☸H𐁒*\u0012弥Z!Sq%3蠑\"r􎚼WCc⁆P]𩇱i󿧫e\u0016󿾜L센􆣌27V硤𗀍L􄙻P곻m󲑡𩀁S􃕱s!\u001d󷐍Sa𨉇(􎼄\u001f􂒦󴆵\u00061\u000b\u0006󰣟􌑚󻔤\r5N53\u0016k𭬵=<1\u0016H𗉻Mᚚ訰E𤗦QSy*fwg]Q]Y\"s󹷏\u0015􏦎!𣣭\u0000\u0017n􍞠\u001f\u0002!g랑[翍ഖ􎻤_􋝘\u0005\u001c\u000b$\nth𫍬\u001c\"y\u0001􅜂\u001b:6ZfF.Fs\u0018𫞐jnSm\tO9\u0017P@;\rO\u001eM\u0010󻒝\u0015󶷁𝙠쌃󽨴&5𗮣􂾎𬖢\u001b􀷉\u0014k0𧆡\u0014OcMAZy􄅶5\u001c}t8rUzI袷\u000fIL\n|q|\\W\u0014c􍌼=𢩃S(𝥈K󹗥Oꃡ\"7㒼Axp\u0007F\u0018$H6E\u0012{𫢶\u0013𣢋\u0015揂󻐾'࠶\u0012j𢙆W?&\u0014nF^s󱗳􅃽𢧢\u001a\u000esV㉺\u00157𭷿{\u001eꆙ\u001bꂵW𒋙\u000e\u0004g\u0000n\u0001}\u001e\u0003T􌷲\u0007\u000c𦗡ne𛈃NP" } diff --git a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_9.json b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_9.json index 79a5551e162..5997b8751b5 100644 --- a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_9.json +++ b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_9.json @@ -1,5 +1,5 @@ { "code": "uyqP0_aQl3yI0f7i0fpyL6quXIf6WSJRbPrU6Z0j2gElHzfIXLenrK4ZwQl42i99XCnAjLGA2=sQczG10h7DBcYH4TmbO-li6YDpcduZ3XkbGQ=EalL5L2xZbwUpVFGp5J5e=yea3gDvfUwq0sdTrRFCFbTJBG5cU9K_5zQMB=DTFJoHAh=L_0uTZCRF_bj36cGxLegs42ji4GGO3kG4kcvpSCMpJV20a47V7GbqfEdQ3HV2gdN5CXpWXxRu71Y2XvAMijj8O-ciqslgJCveAgm6JlkZJf8-Cbj3tmBD1xYveBLOBVOW1=vaD23ST6FDLpzbRJslhJzwInpu5AaIxndPmLzeXH3I5mfrMBFyGO6e9Pro51aJPGV5COmIinyjxcM-vEmWYYkLy7owuVyswR89m--SRwgOWL5UtF-QbkS5bpltl6BmnrTEeaZNMQRPrcpPL4RT=0GFy=ka7Oq1Ixi5OR5EDYgIa_Rl3I9jq034w6wCQjW=33Z5wFRWcdX4lfqvA-66Huc--Xk3hAKScqNeL3Xre5eN1pwOrEFsMhncwuGoFZoXaHSMrQZEqVhVJcFA8afI_vpIk0Ft6NMcS3AtYLQgdqrvaBe42_s", - "email": "A@9L\u0008󹒲鏠", + "email": "some@example", "password": "t%􎈲'􈽛G\u0000G\u0011􄄕V𖨱H헣󿹑[3𤩜\u0005𢹖Y륓\nm\u000b󴧕d5􊓫O:\u0011􌛀\u001fb`🧎\nW\u00198\u0000\u0002𣯭W!mX𠎬z-􆚷ミ 󸘞M\u0007xT(\u000b-\"3Xx\u0007\u000c󱷮b7*P󹒝.\n\u0006fz\u0004N𝦺\u0019弴Z𔒷[7󾟖luXV\u0017Rh\u0019U5}y󳒹&􁍡UO𥼑L\u000ez󵓖\u0013G奣d邗5(iM\u001a\u0000\"󹧡QKY]_\u000b-󵰔𣛡1a\u0001}5]'L%s7􋼓5u\u000b9|F\u0005𫰔j\u001f󼧴L𪣳\u0014o뷃󼞩􌖦lqDs􀺛 ]\u0010\u0003f\u0001bO.\u001e\u001dj󴫟E\"\r󼷥𧮶Ig;~\u001f霙\u0017WQ󽢯𘢚?􌙰\\e:@ჼ󰵗Cq\u0010♞>\u0018zT]\u001c\u0011過\u0005)\u0003󻊛7𪄵\u0001\u0017\u0004𠽘r6;D%FM\":􁅞\u000e-S\u0015c`]쑔\u0018O\u000b$􍹁\u0005\\i󼴋\tk𠩘nME\u001d\u0018mi󺐛~K[K󶍼4\u0012jl\u00132𧹎w.\u0019O󽄜x󹮜榪\u0001􅴂蛂:JY\u0008榊\u0003DPH5􅫨Hh\u0002j!y󲵥􃹶\u001c\u001a0\u0015g\t􄇬q\u000b(c\u0008R=\"󶇑\u0014\u0012\u0015\u001ai󳴯M􃶪𫖿(幦l𫬿\u0012𡙎*聭Y󾊪DžW\t\u0000_pk#=r󼇕{HN􏙇󹓑W3 <%*0𢙮?\u0018|Q𤻀oE*\u000fx,\u0003㷥𒉀L{NC􈒵t𬘨I\u0010{\u0015M\u0011<󶮍efpSGC󱚵Cl\u0001-+\t\u000c􊓉\u0011'uWmR􉴝𠤛9󿌐>𢸀7\u0012s{󴊡my|􏹣\u0007\u0000+󼵙t'N\u0014\u000f礤7􈳣󺵧o􈊞􋯚Yx𠣿5{;𣷳0XM#󷩠Q瘗􈅞\tx􄷧M𧹔^-[a|󲆍聧\u0019\\=\tH􄈌2\u0010W􈲚#h'K󴎄\u000bpX\u0019\u000e\t𘇚_~𫁃󺕵G\u001fwX텣􈒼씚@/E\u000f󵩤\u0010Q𦙊p#pR󶃁\u0012󾫫$\n`l94.𩾱gun\u0004RG\u001bF󱨄Hbf㎕e^󵼝9R.\u0008\u0003;\u0000N$4\u000cVy𧉽,h贚𩍿󱾙\"^莱J\u0000#𭀤ᾣ𓏞z{|\u0006)g􉀃[DK\u0001:(jNn\r:D􏪫dM􁛄G,􉫘𤓀s9\u000eoy􎓰󱶄5Y&𨈘\u000b𥗖\u0016\"Ob50𡝮Q_W+'\"!𒍉\t5𥼍􃂽㿄>\u001a𤗗8𗪬y:U􀦵\u0010\u0001𮤑𩠛\"\u001f,q\u0001a􏯗\u001a_(j 𡕾B􎖩B!􊍔RL^𢶓\u0008𗵍쥻\u0007f\u0018󸸅\niK𠣾\u0004_㫪췝\u0016󲗔\u0007왑\u000c\u001c;\u001f|V󳚅\u0013\u0014󶒯Km\u001e\u0005𠷁􁟠\u001bnz\u0012wy𢡇􇠛𦋎5Q\u001c𑙑J\u0001􂡮/9Ew\u0000Sbe\u0007覿𤝤\u0017\rd1󵶒侜Rᮜ\u000f🟧\u0001v \\𬜗v`iF𬭓93\u0006dSl^I2.W洞󷎪V%*Nv􋉯󿯔]濺xi󶻔󠄝H殈\u001fJ>M􂕓𭜫_&𮚁󱺩󷌾딟.unqC\u0004y󿬔\u0011i#}􃃶𣿈`^󾲸Q\u001b.\u001e)f󱵙𣸍\u0000^\u0000i!\u001f쁏~\\󽮘t􋨚⑿\u0013\n\u0002;E\u000b\u0005z3󹮠2\u0002v\u00175k󹲋𫪢\u001bs\u0000\u000bꢱ9Y𖠺\u0016Q􉝖\u001b\\_7􎂅􊩧J\u00197󷦐v\u0003댡\u0001G󿿤{\u0018PE􆁌𤯦\u0014㕷:N/􌜊􈨫􂅔󻽶V\u0013}𪡿m>{𣼸4\u0000I\u0005y^$\n=)S; R`𢋐@xtE\u000f3" } diff --git a/libs/wire-api/test/golden/testObject_ConversationCoverView_1.json b/libs/wire-api/test/golden/testObject_ConversationCoverView_1.json index 917cfe4360a..28e2621bb1b 100644 --- a/libs/wire-api/test/golden/testObject_ConversationCoverView_1.json +++ b/libs/wire-api/test/golden/testObject_ConversationCoverView_1.json @@ -1,5 +1,5 @@ { + "has_password": false, "id": "00000018-0000-0020-0000-000e00000002", - "name": null, - "has_password": false + "name": null } diff --git a/libs/wire-api/test/golden/testObject_ConversationCoverView_2.json b/libs/wire-api/test/golden/testObject_ConversationCoverView_2.json index c36128fa055..c213fe47c58 100644 --- a/libs/wire-api/test/golden/testObject_ConversationCoverView_2.json +++ b/libs/wire-api/test/golden/testObject_ConversationCoverView_2.json @@ -1,5 +1,5 @@ { + "has_password": false, "id": "00000018-0000-0020-0000-000e00000002", - "name": "conversation name", - "has_password": false + "name": "conversation name" } diff --git a/libs/wire-api/test/golden/testObject_ConversationCoverView_3.json b/libs/wire-api/test/golden/testObject_ConversationCoverView_3.json index 453b2e9b2d4..234dc11a4cd 100644 --- a/libs/wire-api/test/golden/testObject_ConversationCoverView_3.json +++ b/libs/wire-api/test/golden/testObject_ConversationCoverView_3.json @@ -1,5 +1,5 @@ { + "has_password": true, "id": "00000018-0000-0020-0000-000e00000002", - "name": "", - "has_password": true + "name": "" } diff --git a/libs/wire-api/test/golden/testObject_Conversation_v2_user_4.json b/libs/wire-api/test/golden/testObject_Conversation_v2_user_4.json index 6609a65f3af..cda477d4510 100644 --- a/libs/wire-api/test/golden/testObject_Conversation_v2_user_4.json +++ b/libs/wire-api/test/golden/testObject_Conversation_v2_user_4.json @@ -22,6 +22,7 @@ "cipher_suite": 1, "creator": "00000000-0000-0000-0000-000200000001", "epoch": 0, + "epoch_timestamp": null, "group_id": "dGVzdF9ncm91cA==", "id": "00000000-0000-0000-0000-000000000002", "last_event": "0.0", diff --git a/libs/wire-api/test/golden/testObject_Conversation_v5_user_4.json b/libs/wire-api/test/golden/testObject_Conversation_v5_user_4.json index 430ac682cee..e7351a003ce 100644 --- a/libs/wire-api/test/golden/testObject_Conversation_v5_user_4.json +++ b/libs/wire-api/test/golden/testObject_Conversation_v5_user_4.json @@ -21,6 +21,7 @@ "cipher_suite": 1, "creator": "00000000-0000-0000-0000-000200000001", "epoch": 0, + "epoch_timestamp": null, "group_id": "dGVzdF9ncm91cA==", "id": "00000000-0000-0000-0000-000000000002", "last_event": "0.0", diff --git a/libs/wire-api/test/golden/testObject_CreateScimTokenResponse_1.json b/libs/wire-api/test/golden/testObject_CreateScimTokenResponse_1.json new file mode 100644 index 00000000000..3896abc8201 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_CreateScimTokenResponse_1.json @@ -0,0 +1,10 @@ +{ + "info": { + "created_at": "2024-10-22T18:04:50Z", + "description": "description", + "id": "e25faea1-ee2d-4fd8-bf25-e6748d392b23", + "team": "2853751e-9fb6-4425-b1bd-bd8aa2640c69", + "name": "token name" + }, + "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 a79a8f35565..cd71c759b31 100644 --- a/libs/wire-api/test/golden/testObject_CreateScimToken_4.json +++ b/libs/wire-api/test/golden/testObject_CreateScimToken_4.json @@ -1,5 +1,6 @@ { "description": "description4", "password": null, - "verification_code": null + "verification_code": null, + "name": "scim connection name" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_1.json b/libs/wire-api/test/golden/testObject_EmailUpdate_provider_1.json index ced6ffcdc2c..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_1.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_provider_1.json @@ -1,3 +1,3 @@ { - "email": "sL𘇍@%" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_10.json b/libs/wire-api/test/golden/testObject_EmailUpdate_provider_10.json deleted file mode 100644 index a9f3b4b7b7f..00000000000 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_10.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "󱬌g鯪Ns&N6r옂U^􊌕􅏔O=;\u0006~g@C3󱸇od𢹓󵏌𦷂P𬿣=𭴟\u0013\u001a󵯰" -} diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_11.json b/libs/wire-api/test/golden/testObject_EmailUpdate_provider_11.json deleted file mode 100644 index 731d7437a1f..00000000000 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_11.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "\u0019h𬨑V.JAq-󹣮𬆱46媅=􏛄󸏇@}=􈒽Y\u000c_O}:M\"󸻿" -} diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_12.json b/libs/wire-api/test/golden/testObject_EmailUpdate_provider_12.json deleted file mode 100644 index 4d7e1d26c35..00000000000 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_12.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "匣\u0005矹B{󼆎􁭱~@󳖼𨾪\u001d 􅟮" -} diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_2.json b/libs/wire-api/test/golden/testObject_EmailUpdate_provider_2.json deleted file mode 100644 index fc2647a9e4f..00000000000 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_2.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "7𧒽>t劭\u0006𐿳n9\u0008\u001fskT.\"􎏸\r\u0014`@^/>1Rp<\u0019􏃵􉡁\u0002#\u0007[E\u0003#碑𧧙ീeJ " -} diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_20.json b/libs/wire-api/test/golden/testObject_EmailUpdate_provider_20.json deleted file mode 100644 index 9fa5fe447dd..00000000000 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_20.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "o\u0001󴪚\u0007LL$\u000eᅭ􌡷l*p󰘟\u001a@q矛\u0013ㄭ󴠅󸂢q󴮢𣠈􁻠&^𫋐Z" -} diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_3.json b/libs/wire-api/test/golden/testObject_EmailUpdate_provider_3.json deleted file mode 100644 index c62559456ee..00000000000 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_3.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "1[Z𐲪\r语3􉝰|u\u0008x;2𤔴0\u001f@ {\"_\u000b􊧁󾘨􄜓􉴁\u0019J%󵛃yy\u001a󾁧t>xoC" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_user_19.json b/libs/wire-api/test/golden/testObject_EmailUpdate_user_19.json index 0a0c16440de..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_user_19.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_user_19.json @@ -1,3 +1,3 @@ { - "email": "𫬝󽻼E\u001c\u001a𞴇\u0015C%󱏱\u0006\u001f\u0011a8\rﵷE󱺣%𫧚𭖕;|\u001d@1+,𡂌83" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_user_2.json b/libs/wire-api/test/golden/testObject_EmailUpdate_user_2.json index 94a61ed1025..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_user_2.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_user_2.json @@ -1,3 +1,3 @@ { - "email": "C𓌇|g󼹸(4@" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_user_20.json b/libs/wire-api/test/golden/testObject_EmailUpdate_user_20.json index 4a3b7ef96db..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_user_20.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_user_20.json @@ -1,3 +1,3 @@ { - "email": "e\u0015V\u0008D\u000188Kh\u001c𩙝D4􊇉軀zg\u001e@:𪤬D\u0005y+\u0014k>]\u0017\u001cr󶥱aWSw\u0007ជ6\u001e𘑑f" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_user_3.json b/libs/wire-api/test/golden/testObject_EmailUpdate_user_3.json index 76ae2d73222..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_user_3.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_user_3.json @@ -1,3 +1,3 @@ { - "email": "uA76􂎥c𡖝\u0013𤋺\u0001U0]Ds$L@/㺚 u􏠐\u0013Pq\u001dev懪󻛣㺈" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_user_4.json b/libs/wire-api/test/golden/testObject_EmailUpdate_user_4.json index b22ad68e944..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_user_4.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_user_4.json @@ -1,3 +1,3 @@ { - "email": ":|𪀧WYA\u0007`OS\u0013\u0015􂴠􎶋u\u000b-\u0013F2B󶡙'z\u0005}4[@6𣾷C𥠃󸆝V􊙮8\"\u0001M𩌻l" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_user_5.json b/libs/wire-api/test/golden/testObject_EmailUpdate_user_5.json index 35da92953a9..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_user_5.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_user_5.json @@ -1,3 +1,3 @@ { - "email": "0a⪨\u0012n\u001c!a;*l흣Z\u0008\u0019\u0000\u0000􂻂\u001ej\\𗖸_;\u0002@I︳j󷥽byd𥼛K🡴𒍟􌎍mwP" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_user_6.json b/libs/wire-api/test/golden/testObject_EmailUpdate_user_6.json index ea0097da53d..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_user_6.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_user_6.json @@ -1,3 +1,3 @@ { - "email": "com0$p얊/\u001c󿂈󷶆\u000fN􅮊,@\u001d󵖩𥶾,ꆶ/qeT&𥑏\u0007댯}\u0017뉴􏵹󴫸#𬰮0冗9),4F" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_user_7.json b/libs/wire-api/test/golden/testObject_EmailUpdate_user_7.json index fbdd4873e81..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_user_7.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_user_7.json @@ -1,3 +1,3 @@ { - "email": "-튄@\u0007$㇠Be􅱑\u000cS7.〢\u0000ହ\r+k>Z:E\u0003hX$?" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_user_8.json b/libs/wire-api/test/golden/testObject_EmailUpdate_user_8.json index 74c30a84d4a..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_user_8.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_user_8.json @@ -1,3 +1,3 @@ { - "email": "\u0019f\\\u000edD9#XfnL!󲻀\u0006\u001cZ퀆U@)蟍󸩤x9~t)Dd;P\u001b󺅩.M(p􀜛pCz􍜾󴝄\u000f\u0005\u0006{󸋛􌴰" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_user_9.json b/libs/wire-api/test/golden/testObject_EmailUpdate_user_9.json index 43ab8e01e93..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_user_9.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_user_9.json @@ -1,3 +1,3 @@ { - "email": "\u001c\u000f,n}㑉@x8\u001d" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_Email_user_1.json b/libs/wire-api/test/golden/testObject_Email_user_1.json index a4a7b273859..4a586896d22 100644 --- a/libs/wire-api/test/golden/testObject_Email_user_1.json +++ b/libs/wire-api/test/golden/testObject_Email_user_1.json @@ -1 +1 @@ -"𥆯fa섒\u00012\u000b.\u001c<\u001b\u0017#\t-􍴢`2@\u0004\"殭\u0010n\u001d]\u0007hDzP\u0018p㫾T𠤰\u0014d" +"some@example" diff --git a/libs/wire-api/test/golden/testObject_Email_user_10.json b/libs/wire-api/test/golden/testObject_Email_user_10.json deleted file mode 100644 index d30c881dd46..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_10.json +++ /dev/null @@ -1 +0,0 @@ -"\"K􃘌􅒈$s󱐔􇕿V389V\u0007\u0017\u001cH􁮉)滱Dg\u0017@􏺑􂊪k\u000ci\n󿞆𢈬{\u001dl㠃YPbV-ZZ􂶖LK跓􁒥Y" diff --git a/libs/wire-api/test/golden/testObject_Email_user_11.json b/libs/wire-api/test/golden/testObject_Email_user_11.json deleted file mode 100644 index b66ec316746..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_11.json +++ /dev/null @@ -1 +0,0 @@ -"@+zGJ\u0008_t/N\u00003S􃂕M𣮑􆰠z􌚏􎊆SJD" diff --git a/libs/wire-api/test/golden/testObject_Email_user_12.json b/libs/wire-api/test/golden/testObject_Email_user_12.json deleted file mode 100644 index 074eda1c716..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_12.json +++ /dev/null @@ -1 +0,0 @@ -"\u00041[G󷭮󰄵9􉐛:uJ𣒰\u001cMF𞄝󰫽𭸓\u001f6|󳘏\u0015􆧐@𗧟_􃱰" diff --git a/libs/wire-api/test/golden/testObject_Email_user_13.json b/libs/wire-api/test/golden/testObject_Email_user_13.json deleted file mode 100644 index 4c62fda0c06..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_13.json +++ /dev/null @@ -1 +0,0 @@ -"\u0005\rt\u001cA#}\u001en𫊈OA\u0016\u000e󽼭\t2q\u0013n𧙛𭍩\u000c+@]牭𦷮na[\u001f'h𠴗\u0012󵹌𤅣􏂫" diff --git a/libs/wire-api/test/golden/testObject_Email_user_14.json b/libs/wire-api/test/golden/testObject_Email_user_14.json deleted file mode 100644 index 00afac0771b..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_14.json +++ /dev/null @@ -1 +0,0 @@ -"X\u0010E\u0014󷘞󿒐􎒂YMU[\n}󲈖\r7󶨐\u0018\\@𨓫󼽯M\u001d!𢶞T%F]" diff --git a/libs/wire-api/test/golden/testObject_Email_user_15.json b/libs/wire-api/test/golden/testObject_Email_user_15.json deleted file mode 100644 index ac14ced80d4..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_15.json +++ /dev/null @@ -1 +0,0 @@ -"{C𪮆󾞐eU\u0014w@`\u0013>" diff --git a/libs/wire-api/test/golden/testObject_Email_user_16.json b/libs/wire-api/test/golden/testObject_Email_user_16.json deleted file mode 100644 index d47fad630cb..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_16.json +++ /dev/null @@ -1 +0,0 @@ -"IO\u00192>􁍸+~@\u000e" diff --git a/libs/wire-api/test/golden/testObject_Email_user_17.json b/libs/wire-api/test/golden/testObject_Email_user_17.json deleted file mode 100644 index 4dc0b271fc6..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_17.json +++ /dev/null @@ -1 +0,0 @@ -"\u001e􋺥\u0016\u0001>줂V7C-asF􁩬IfrYTM;󷲆􂧽*l(d@.53Q􋻗26bfw𪷁𒂅~𨚃𠌬m\u001d\u0015\u000e}𥕟~􀩻R" diff --git a/libs/wire-api/test/golden/testObject_Email_user_18.json b/libs/wire-api/test/golden/testObject_Email_user_18.json deleted file mode 100644 index ebc1ece9160..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_18.json +++ /dev/null @@ -1 +0,0 @@ -"𬒂|\u0000\u00023\u0012􌝘,:\u0017JuF\u0010*㶮\u0011\"\u0016kU!󱩝8T\u00192@T\"q\u0011𑣤}\u0013Z~🖟)" diff --git a/libs/wire-api/test/golden/testObject_Email_user_19.json b/libs/wire-api/test/golden/testObject_Email_user_19.json deleted file mode 100644 index 33346b6153c..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_19.json +++ /dev/null @@ -1 +0,0 @@ -"\t\u0006N~\u001eWy5'\u0018q:_K󹫜\"+WM瑳S.\u0012D`\u0018@DG𡏝AE=W􆫔" diff --git a/libs/wire-api/test/golden/testObject_Email_user_2.json b/libs/wire-api/test/golden/testObject_Email_user_2.json deleted file mode 100644 index 070584c3307..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_2.json +++ /dev/null @@ -1 +0,0 @@ -"󰨊\u0013F𭟍􈶇n:@8􅻻ಏ\u0016s" diff --git a/libs/wire-api/test/golden/testObject_Email_user_20.json b/libs/wire-api/test/golden/testObject_Email_user_20.json deleted file mode 100644 index 232b97cdc8e..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_20.json +++ /dev/null @@ -1 +0,0 @@ -"/𡯐ZF󵃹󺲛@𭵥I􎾁~FN󸋄\u00053PH\u0014k󽃿𠉚{󿙾\u0017H󹇢\u0012IR" diff --git a/libs/wire-api/test/golden/testObject_Email_user_3.json b/libs/wire-api/test/golden/testObject_Email_user_3.json deleted file mode 100644 index 1c831e16e3a..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_3.json +++ /dev/null @@ -1 +0,0 @@ -"􎟘o #𩃰GῩ%/]\u001ck􁻪t􍜮)\u0013\u0015*\u001bE}>\u001a\u0001En==8@昸\u0018N\u0007;_l$*䇱P\u0002\u0008K" diff --git a/libs/wire-api/test/golden/testObject_Email_user_4.json b/libs/wire-api/test/golden/testObject_Email_user_4.json deleted file mode 100644 index ceb8c7e0a35..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_4.json +++ /dev/null @@ -1 +0,0 @@ -"!𢢼\u000f&\u0007o88i󱨨(\u001al1_􇐬-\u000c\t&\u001c9􊉳@ef&󻣵𬋾W(󳻠cS\u0016\u0004𗴯GLi" diff --git a/libs/wire-api/test/golden/testObject_Email_user_5.json b/libs/wire-api/test/golden/testObject_Email_user_5.json deleted file mode 100644 index 2e5781ed517..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_5.json +++ /dev/null @@ -1 +0,0 @@ -"@洸\u0011􉼗󺳩jh󷾴o\u001dH0bW7^䗕B" diff --git a/libs/wire-api/test/golden/testObject_Email_user_6.json b/libs/wire-api/test/golden/testObject_Email_user_6.json deleted file mode 100644 index 918f37c1212..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_6.json +++ /dev/null @@ -1 +0,0 @@ -"eM'5\u0008r>𖥝󳔈4𠟐r\t@󳗧[n\u0005 )&D=󹆉\u0006\u0008" diff --git a/libs/wire-api/test/golden/testObject_Email_user_7.json b/libs/wire-api/test/golden/testObject_Email_user_7.json deleted file mode 100644 index 65b5478a87a..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_7.json +++ /dev/null @@ -1 +0,0 @@ -"􎦮𩔛a\u0003'𗅼L\u0016醍󲂢Q󴝊󳸯Fc􋒕T𮮲\u000eTD@𫌐?1􌰨DT" diff --git a/libs/wire-api/test/golden/testObject_Email_user_8.json b/libs/wire-api/test/golden/testObject_Email_user_8.json deleted file mode 100644 index 26f013774c1..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_8.json +++ /dev/null @@ -1 +0,0 @@ -" \u001b{􎎍EZ_\t+E\u000bE@h𖡚%펽g㤣Lu\u0012𫥦J} Aq\"#f\u001b \u0004O\u0012" diff --git a/libs/wire-api/test/golden/testObject_Email_user_9.json b/libs/wire-api/test/golden/testObject_Email_user_9.json deleted file mode 100644 index 327f67fd21c..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_9.json +++ /dev/null @@ -1 +0,0 @@ -"󶤾󼉱5M6d*~-\u0013\u0017?罆@g𛆟R􊸢|󰸌󻥤" diff --git a/libs/wire-api/test/golden/testObject_Event_conversation_3.json b/libs/wire-api/test/golden/testObject_Event_conversation_3.json index 95ff02e8ca8..9e00bb1f2fc 100644 --- a/libs/wire-api/test/golden/testObject_Event_conversation_3.json +++ b/libs/wire-api/test/golden/testObject_Event_conversation_3.json @@ -2,9 +2,9 @@ "conversation": "2126ea99-ca79-43ea-ad99-a59616468e8e", "data": { "code": "7d6713", + "has_password": false, "key": "CRdONS7988O2QdyndJs1", - "uri": "https://example.com", - "has_password": false + "uri": "https://example.com" }, "from": "2126ea99-ca79-43ea-ad99-a59616468e8e", "qualified_conversation": { diff --git a/libs/wire-api/test/golden/testObject_Event_user_14.json b/libs/wire-api/test/golden/testObject_Event_user_14.json index 1657df9614d..20ebd595043 100644 --- a/libs/wire-api/test/golden/testObject_Event_user_14.json +++ b/libs/wire-api/test/golden/testObject_Event_user_14.json @@ -2,8 +2,8 @@ "conversation": "00000838-0000-1bc6-0000-686d00003565", "data": { "code": "lLz-9vR8ENum0kI-xWJs", - "key": "NEN=eLUWHXclTp=_2Nap", - "has_password": false + "has_password": false, + "key": "NEN=eLUWHXclTp=_2Nap" }, "from": "0000114a-0000-7da8-0000-40cb00007fcf", "qualified_conversation": { diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_1.json b/libs/wire-api/test/golden/testObject_Feature_team_1.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_1.json rename to libs/wire-api/test/golden/testObject_Feature_team_1.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_10.json b/libs/wire-api/test/golden/testObject_Feature_team_10.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_10.json rename to libs/wire-api/test/golden/testObject_Feature_team_10.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_11.json b/libs/wire-api/test/golden/testObject_Feature_team_11.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_11.json rename to libs/wire-api/test/golden/testObject_Feature_team_11.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_12.json b/libs/wire-api/test/golden/testObject_Feature_team_12.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_12.json rename to libs/wire-api/test/golden/testObject_Feature_team_12.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_13.json b/libs/wire-api/test/golden/testObject_Feature_team_13.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_13.json rename to libs/wire-api/test/golden/testObject_Feature_team_13.json diff --git a/libs/wire-api/test/golden/testObject_Feature_team_14.json b/libs/wire-api/test/golden/testObject_Feature_team_14.json new file mode 100644 index 00000000000..99c386e30af --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Feature_team_14.json @@ -0,0 +1,7 @@ +{ + "config": { + "useSFTForOneToOneCalls": true + }, + "status": "disabled", + "ttl": "unlimited" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_15.json b/libs/wire-api/test/golden/testObject_Feature_team_15.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_15.json rename to libs/wire-api/test/golden/testObject_Feature_team_15.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_14.json b/libs/wire-api/test/golden/testObject_Feature_team_16.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_14.json rename to libs/wire-api/test/golden/testObject_Feature_team_16.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_17.json b/libs/wire-api/test/golden/testObject_Feature_team_17.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_17.json rename to libs/wire-api/test/golden/testObject_Feature_team_17.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_2.json b/libs/wire-api/test/golden/testObject_Feature_team_2.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_2.json rename to libs/wire-api/test/golden/testObject_Feature_team_2.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_3.json b/libs/wire-api/test/golden/testObject_Feature_team_3.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_3.json rename to libs/wire-api/test/golden/testObject_Feature_team_3.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_4.json b/libs/wire-api/test/golden/testObject_Feature_team_4.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_4.json rename to libs/wire-api/test/golden/testObject_Feature_team_4.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_5.json b/libs/wire-api/test/golden/testObject_Feature_team_5.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_5.json rename to libs/wire-api/test/golden/testObject_Feature_team_5.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_6.json b/libs/wire-api/test/golden/testObject_Feature_team_6.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_6.json rename to libs/wire-api/test/golden/testObject_Feature_team_6.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_7.json b/libs/wire-api/test/golden/testObject_Feature_team_7.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_7.json rename to libs/wire-api/test/golden/testObject_Feature_team_7.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_8.json b/libs/wire-api/test/golden/testObject_Feature_team_8.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_8.json rename to libs/wire-api/test/golden/testObject_Feature_team_8.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_9.json b/libs/wire-api/test/golden/testObject_Feature_team_9.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_9.json rename to libs/wire-api/test/golden/testObject_Feature_team_9.json diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_10.json b/libs/wire-api/test/golden/testObject_InvitationList_team_10.json index c06f56c3cf3..03f8201ebf0 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_10.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_10.json @@ -4,10 +4,9 @@ { "created_at": "1864-05-08T17:28:36.896Z", "created_by": "00000001-0000-0001-0000-000000000000", - "email": "}@", + "email": "some@example", "id": "00000000-0000-0000-0000-000000000000", "name": "P𥖧\u0006'e\u0010\u001d\"\u0011K󽗨Fcvm[\"Sc}U𑊒􂌨󿔟~!E􀖇\u000bV", - "phone": null, "role": "member", "team": "00000000-0000-0001-0000-000100000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_11.json b/libs/wire-api/test/golden/testObject_InvitationList_team_11.json index 3b5e6bce7fb..35f94e7190f 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_11.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_11.json @@ -4,10 +4,9 @@ { "created_at": "1864-05-08T01:33:08.374Z", "created_by": "00000001-0000-0000-0000-000000000000", - "email": "@Z", + "email": "some@example", "id": "00000001-0000-0000-0000-000000000001", "name": "G\\,\u0000=ෝI-w󠀹}𠉭抳-92\u0013@\u0006\u001f\\F\u001a\"-r꒫6\u000fඬ\u001f*}c󼘹\u001f\u0007T8m@旅M\u0012#MIq\r4nW􍦐y\u0005Ud룫#𫶒5\n\u0002V]𨡀\"󶂃𩫘0:ﲼ𮭩+\u0001\u000bP󹎷X镟􅔧.\u0019N\"𬋻", - "phone": "+872574694", "role": "admin", "team": "00000000-0000-0001-0000-000100000000", "url": null @@ -15,10 +14,9 @@ { "created_at": "1864-05-09T23:06:13.648Z", "created_by": null, - "email": "@", + "email": "some@example", "id": "00000000-0000-0000-0000-000100000000", "name": "叕5q}B\u0001𦌜`イw\\X@󼶝𢼈7Mw,*z{𠚷&~", - "phone": "+143031479742", "role": "partner", "team": "00000000-0000-0001-0000-000000000001", "url": null @@ -26,10 +24,9 @@ { "created_at": "1864-05-09T10:37:03.809Z", "created_by": "00000001-0000-0000-0000-000000000001", - "email": "@", + "email": "some@example", "id": "00000000-0000-0001-0000-000000000000", "name": "V􈫮\u0010qYヒCU\u000e􄕀fQJ\u0005ਓq+\u0007\u0016󱊸\u0011@𤠼`坟qh+𬾬A7𦄡Y \u0011Tㅎ1_􈩇#B<􂡁;a6o=", - "phone": "+236346166386230", "role": "partner", "team": "00000001-0000-0000-0000-000000000000", "url": null @@ -37,10 +34,9 @@ { "created_at": "1864-05-09T04:46:03.504Z", "created_by": null, - "email": "@", + "email": "some@example", "id": "00000001-0000-0001-0000-000100000000", "name": ",􃠾{ս\u000c𬕻Uh죙\t\u001b\u0004\u0001O@\u001a_\u0002D􎰥𦀛\u0016g}", - "phone": "+80162248", "role": "admin", "team": "00000001-0000-0001-0000-000100000001", "url": null @@ -48,10 +44,9 @@ { "created_at": "1864-05-09T12:53:52.047Z", "created_by": "00000000-0000-0000-0000-000000000001", - "email": "@", + "email": "some@example", "id": "00000000-0000-0001-0000-000100000000", "name": null, - "phone": null, "role": "owner", "team": "00000000-0000-0001-0000-000100000001", "url": null diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_16.json b/libs/wire-api/test/golden/testObject_InvitationList_team_16.json index 535fe0678e2..ae37c5ce5e1 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_16.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_16.json @@ -4,10 +4,9 @@ { "created_at": "1864-05-09T15:25:30.297Z", "created_by": "00000001-0000-0001-0000-000000000000", - "email": "\u000f@", + "email": "some@example", "id": "00000001-0000-0000-0000-000100000001", "name": "E𝘆YM<󾪤j􆢆\r􇳗O󴟴MCU\u001eI󳊃m𔒷hG\u0012|:P􅛽Vj\u001c\u0000ffgG)K{􁇏7x5󱟰𪔘\n\u000clT􆊞", - "phone": "+36515555", "role": "owner", "team": "00000001-0000-0001-0000-000100000001", "url": null diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_17.json b/libs/wire-api/test/golden/testObject_InvitationList_team_17.json index eba7991502c..d36b7d8bdc6 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_17.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_17.json @@ -4,10 +4,9 @@ { "created_at": "1864-05-08T10:54:19.942Z", "created_by": "00000001-0000-0001-0000-000100000000", - "email": "&@𫳦", + "email": "some@example", "id": "00000001-0000-0001-0000-000000000001", "name": null, - "phone": null, "role": "partner", "team": "00000001-0000-0000-0000-000100000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_2.json b/libs/wire-api/test/golden/testObject_InvitationList_team_2.json index 076b78a0d43..fb720150e22 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_2.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_2.json @@ -4,10 +4,9 @@ { "created_at": "1864-05-08T09:28:36.729Z", "created_by": "00000001-0000-0001-0000-000000000000", - "email": "𥝢@w", + "email": "some@example", "id": "00000001-0000-0001-0000-000000000000", "name": "fuC9p􌌅A𧻢\u000c\u0005\u000e刣N룞_?oCX.U\r𧾠W腈󽥝\u0013\t[錣\u0016/⃘A𣚁𪔍\u0014H𠽙\u0002𨯠\u0004𨒤o\u0013", - "phone": "+851333011", "role": "owner", "team": "00000000-0000-0000-0000-000000000001", "url": "https://example.com/inv14" diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_20.json b/libs/wire-api/test/golden/testObject_InvitationList_team_20.json index 26a5ab01344..f5b5cac7035 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_20.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_20.json @@ -4,10 +4,9 @@ { "created_at": "1864-05-09T07:22:02.426Z", "created_by": "00000001-0000-0001-0000-000100000001", - "email": "@", + "email": "some@example", "id": "00000001-0000-0000-0000-000100000000", "name": null, - "phone": "+745177056001783", "role": "partner", "team": "00000001-0000-0001-0000-000000000000", "url": null @@ -15,10 +14,9 @@ { "created_at": "1864-05-09T18:56:29.712Z", "created_by": null, - "email": "@", + "email": "some@example", "id": "00000001-0000-0001-0000-000000000000", "name": "YPf╞:\u0005Ỉ&\u0018\u0011󽧛%ꦡk𪯋􅥏:Q\u0005F+\u0008b8Jh􌎓K\u0007\u001dY\u0004􃏡\u000f󽝰\u0016 􁗠6>I󾉩B$z?𤢾wECB\u001e𥼬덄\"W𗤞󲴂@\u001eg)\u0001m!-U􇧦󵜰o\u0006a\u0004𭂢;R􂪧kgT􍆈f\u0004\u001e\rp𓎎󿉊X/􄂲)\u00025.Ym󵳬n싟N\u0013𫅄]?'𠴺a4\"󳟾!i5\u001e\u001dC14", - "phone": null, "role": "owner", "team": "00000001-0000-0000-0000-000100000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_4.json b/libs/wire-api/test/golden/testObject_InvitationList_team_4.json index 3063b4fdeb3..f53ae2ac79a 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_4.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_4.json @@ -4,10 +4,9 @@ { "created_at": "1864-05-09T19:46:50.121Z", "created_by": "00000001-0000-0001-0000-000000000000", - "email": "@", + "email": "some@example", "id": "00000001-0000-0001-0000-000100000000", "name": "R6𠥄𠮥VQ𭴢\u001a\u0001𬄺0C􉶍\u001bR𭗈𞡊@韉Z?\u0002𩖫􄭦e}\u0001\u0017\u0004m𭉂\u001f]󰺞𮉗􂨮󰶌\u0008\u0011zfw-5𝝖\u0018􃸂 \u0019e\u0014|㡚Vo{􆳗\u0013#\u001fS꿻&zz𧏏9𢱋,\u000f\u000c\u0001p󺜰\u0010𧵪􂸑.&󳢨kZ쓿u\u0008왌􎴟n:􍝋D$.Q", - "phone": "+60506387292", "role": "admin", "team": "00000000-0000-0001-0000-000000000000", "url": null @@ -15,10 +14,9 @@ { "created_at": "1864-05-09T09:00:02.901Z", "created_by": "00000000-0000-0001-0000-000000000000", - "email": "@", + "email": "some@example", "id": "00000001-0000-0000-0000-000100000000", "name": "\u0012}q\u0018=SA\u0003x\t\u0003\\\u000b[\u0008)(\u001b]𡋃Y\u000b@pꈫl뀉𦛌\u0000\t􌤢\u00011\u0011\u0005󹝃\"i猔\u0019\u0008\u0006\u000f\u0012v\u0006", - "phone": "+913945015", "role": "admin", "team": "00000000-0000-0001-0000-000100000000", "url": null @@ -26,10 +24,9 @@ { "created_at": "1864-05-09T11:10:31.203Z", "created_by": "00000001-0000-0001-0000-000000000000", - "email": "@", + "email": "some@example", "id": "00000001-0000-0001-0000-000100000001", "name": "&􂧽Ec\u0000㼓}k󼾘l𪍯\u001fJ\u00190^.+F\u0000\u000c$'`!\u0017[p󾓉}>E0y𗢸#4I\u0007𐐡jc\u001bgt埉􊹘P\u0014!􋣥E93'Y$YL뜦b\r:,𬘞\u000e𥚟y\u0003;􃺹􌛖z4z-D􋰳a𡽜6𨏝r󼖨󱌂J\u0010밆", - "phone": "+17046334", "role": "member", "team": "00000001-0000-0000-0000-000000000001", "url": null @@ -37,10 +34,9 @@ { "created_at": "1864-05-09T23:41:34.529Z", "created_by": "00000001-0000-0001-0000-000100000000", - "email": "@", + "email": "some@example", "id": "00000001-0000-0000-0000-000000000000", "name": "Ft*O1\u0008&\u000e\u0018<𑨛􊰋m\n\u0014\u0012; \u0003󱚥\u0011􂬫\"k.T󹴑[[\u001c\u0004{j`\u001d󳟞c􄖫{\u001a\u001dQY𬨕\t\u0015y\t𠓳j󼿁W ", - "phone": null, "role": "owner", "team": "00000000-0000-0000-0000-000000000000", "url": null @@ -48,10 +44,9 @@ { "created_at": "1864-05-09T00:29:17.658Z", "created_by": "00000000-0000-0000-0000-000000000001", - "email": "@", + "email": "some@example", "id": "00000001-0000-0000-0000-000000000000", "name": null, - "phone": "+918848647685283", "role": "admin", "team": "00000001-0000-0000-0000-000100000000", "url": null @@ -59,10 +54,9 @@ { "created_at": "1864-05-09T13:34:37.117Z", "created_by": "00000000-0000-0000-0000-000100000001", - "email": "@", + "email": "some@example", "id": "00000000-0000-0000-0000-000100000001", "name": "Lo\r􎒩B𗚰_v󰔢􆍶󻀬􊽦9\u0002vyQ🖰&W󻟑𠸘􇹬'􁔫:𤟗𡶘􏹠}-o󿜊le8Zp󺩐􋾙)nK\u00140⛟0DE\u0015K$io\u001e|Ip2ClnU𬖍", - "phone": "+2239859474784", "role": "owner", "team": "00000001-0000-0001-0000-000100000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_6.json b/libs/wire-api/test/golden/testObject_InvitationList_team_6.json index 03aa3d04857..481105a9d8d 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_6.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_6.json @@ -4,10 +4,9 @@ { "created_at": "1864-05-09T06:42:29.677Z", "created_by": "00000000-0000-0000-0000-000100000001", - "email": "@", + "email": "some@example", "id": "00000001-0000-0000-0000-000000000000", "name": null, - "phone": null, "role": "admin", "team": "00000001-0000-0001-0000-000100000000", "url": null @@ -15,10 +14,9 @@ { "created_at": "1864-05-09T11:26:36.672Z", "created_by": "00000001-0000-0000-0000-000000000000", - "email": "@", + "email": "some@example", "id": "00000000-0000-0001-0000-000000000000", "name": null, - "phone": "+85999765", "role": "admin", "team": "00000000-0000-0000-0000-000100000000", "url": null @@ -26,10 +24,9 @@ { "created_at": "1864-05-09T00:31:56.241Z", "created_by": "00000001-0000-0001-0000-000100000001", - "email": "@", + "email": "some@example", "id": "00000000-0000-0001-0000-000000000001", "name": null, - "phone": "+150835819626453", "role": "owner", "team": "00000001-0000-0000-0000-000100000000", "url": null @@ -37,10 +34,9 @@ { "created_at": "1864-05-09T21:10:47.237Z", "created_by": "00000001-0000-0000-0000-000100000001", - "email": "@", + "email": "some@example", "id": "00000001-0000-0000-0000-000100000000", "name": "YBc\r웶8{\\\n􋸓+\u0008\u0016'<\u0004􈄿Z\u0007nOb􋨴􌸖𩮤}2o@v/", - "phone": "+787465997389", "role": "member", "team": "00000000-0000-0001-0000-000100000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_1.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_1.json index ffd2be10c76..7948f9021bf 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_1.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_1.json @@ -1,7 +1,6 @@ { - "email": "/Y𨎂\u000b}?@󲚚󾋉𫟰\u000e󽈝", + "email": "some@example", "locale": "nn", "name": null, - "phone": null, "role": "owner" } diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_10.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_10.json index ec3bbd9af53..bf9390b4067 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_10.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_10.json @@ -1,7 +1,6 @@ { - "email": "󶩭\u000c\u0006\u0010^s@d", + "email": "some@example", "locale": "ny-OM", "name": "H󶌔\u001e댥𖢯uv󿊧\u0012󿕜\u001a 𧆤=a\u001b4H,B\u0018󽲴GpV0󿇇;_\u0000𪔺Z\u0011滘\u00156耐'W9z⻒\tr𤭦􂃸\u0016_ge豍\u0004D𗈌o\u0007n>󲤯", - "phone": "+3547398978719", "role": "member" } diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_11.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_11.json index 165dc4525b6..4d0cd645404 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_11.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_11.json @@ -1,7 +1,6 @@ { - "email": "\u0001\u0000󸊱nJ@t\u0002.", + "email": "some@example", "locale": "si", "name": "𨱜ꇙⴹ𒑐h_5bb2}뛹𨰗P\u0000\u000eT*\u001f`b𩯔\u000f:4\n5\u001a\u001d*T󸅕Bv\u001b\u0003\u001d􀢕𪼏Uu\r_\u0010)y𥦆\u0004\u0008\u001f\u0014\u001c\u0018?􀖫𤣔坾\u0015\u001a4\u000b 5\u0000iꡩo=\tnG鉘\u0017iC\u00139\u000eP󺬘\n\u000b\u0019\u0016UṸ%삶\u0012\u001fF\u001c", - "phone": "+861174152363", "role": "owner" } diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_12.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_12.json index e4109225571..4ea54084e75 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_12.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_12.json @@ -1,7 +1,6 @@ { - "email": "􎪠􇿸@", + "email": "some@example", "locale": "ar-PA", "name": "_\u0019@\u001d0춲󾌹󷱿\u001c\u0010􌩶!􈇮\u000ec\u001f\u0000\u0001>􆖳𩈈\u0019𪶲1}!h0\u0010􁈑w\u0004􆈑1aJ6c\u001d󰼊b𠍕{󳔞𠅳\u0007􋊉", - "phone": "+498796466910243", "role": null } diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_13.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_13.json index 70e6af591fc..af76a81bc0a 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_13.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_13.json @@ -1,7 +1,6 @@ { - "email": "\u0000r@c,", + "email": "some@example", "locale": null, "name": "C󱷈+󼗇\n#s􅺭\u001cpb\u0001󷷁􆂖1\u0017E_\u0018j\u0019V\u001f􃣖㱇􌛎lO8\u0006􁼲\u001c\u0016\u0018\u00106𡪆-beR!s뷈\u0017\u000b􀌟󰏐xt\u000fRf~w󻢹+_𑆞91:,󼜮#cf􁸗ศ৴ᬯB\"􋿺F\t􎾚􅋖/\u0010'󵒫*𩳾7𦈨w􃈢Hx\u00132\u0019t𧽔o6\u0014F%=t󴼼􋹸=\u0000\u0005A􌿋󷃓\u0000\u0004[i󲔇@\u0008\u001c\u000c", - "phone": "+82438666720661", "role": null } diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_14.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_14.json index a53561f331f..8aa099fd0d0 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_14.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_14.json @@ -1,7 +1,6 @@ { - "email": "@\u000b", + "email": "some@example", "locale": "dv-LB", "name": "\u0015wGn󳔃𤠘1}\u0004gY.>=}", - "phone": "+08345603", "role": "admin" } diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_15.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_15.json index 76b0fcf6745..d4ac032590e 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_15.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_15.json @@ -1,7 +1,6 @@ { - "email": "U@􈘸", + "email": "some@example", "locale": null, "name": "y􍭊5󴍽ˆS󸱽\u0014\rH/_\u0013A\u0003𝈯0w\u001d?TQd*1&[?cHW}只󹔖\u0018𬅖Q+\u0003mh󳀫X\u000e\u0005\u0011^g𣐎\u0008qrNV\u000e􋖒WMe\u0007\u0005", - "phone": "+19939600", "role": "owner" } diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_16.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_16.json index 80a2ed29456..89de798ef5c 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_16.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_16.json @@ -1,7 +1,6 @@ { - "email": "壧@\u0001", + "email": "some@example", "locale": "om-BJ", "name": null, - "phone": "+3394446441", "role": "admin" } diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_17.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_17.json index 10db3ce14b1..8c154e43f07 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_17.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_17.json @@ -1,7 +1,6 @@ { - "email": "3\u000cC\u0017\"@\u00010x𝗢", + "email": "some@example", "locale": "kj-TC", "name": null, - "phone": "+403706662", "role": "partner" } diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_18.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_18.json index bebfad27b85..9021ab1aa62 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_18.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_18.json @@ -1,7 +1,6 @@ { - "email": "\u0008\u0006b\n0@UJj&鞱", + "email": "some@example", "locale": "ku", "name": "8VPAp𡧑2L}𫙕", + "email": "some@example", "locale": null, "name": "kl\u0003\u0004\u0016%s7󻼗fX󲹙A\u00087\u0011D\u0004\u0011𨔣sg)dD𦙚Rx[󺭌Tw𐨕\u001e\u001a􀑔z\\\u000f\u0005䊞l􉾾l|oKc\\(𭬥􌵬=脜2VI*􋖛2oTh&#+;o᎙dXA⽇=*􆗾Q󼂨{󲺕󠁑5}\u001d9D𭟸􃿙r􇸖P:󳓗䏩𝓖\u0008\u001a\u001c\u000fF%<𞢹\u000fh\u001b\u0003\u000f󲶳\u001fO\u0000g_𤻨뢪󺥟\u0004􂔤􊃫z~%IA'R\u0008󶽴Hv^󾲱wrjb\t𨭛\u0003", - "phone": "+518729615781", "role": "admin", "team": "00000001-0000-0001-0000-000100000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_12.json b/libs/wire-api/test/golden/testObject_Invitation_team_12.json index ece82b4d173..efd46107985 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_12.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_12.json @@ -1,10 +1,9 @@ { "created_at": "1864-05-12T22:47:35.829Z", "created_by": "00000002-0000-0002-0000-000000000000", - "email": "󸐞𢜑\u001e@", + "email": "some@example", "id": "00000000-0000-0000-0000-000100000002", "name": "\u0010Z+wd^𐘊􆃨1\u0002YdXt>􇺼LSB7F9\\𠿬\u0005\n󱂟\"🀡|\u0007𦠺'\u001bTygU􎍔R칖􅧠O4󼷁E9\"󸃐\u0012Re\u0005D}􀧨𧢧􍭝\u0008V𫋾%98'\u001e9\u00064yP𔗍㡀ř\u0007w\t􌄦\u000b􇋳xv/Yl󵢬𦯯", - "phone": "+68945103783764", "role": "admin", "team": "00000000-0000-0000-0000-000000000002", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_13.json b/libs/wire-api/test/golden/testObject_Invitation_team_13.json index f12163f667d..34387a9fbeb 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_13.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_13.json @@ -1,10 +1,9 @@ { "created_at": "1864-05-08T01:18:31.982Z", "created_by": "00000001-0000-0002-0000-000100000002", - "email": "@r", + "email": "some@example", "id": "00000002-0000-0000-0000-000200000002", "name": "U", - "phone": "+549940856897515", "role": "member", "team": "00000002-0000-0001-0000-000000000001", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_14.json b/libs/wire-api/test/golden/testObject_Invitation_team_14.json index 7b5764a6871..e7db3eaa41b 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_14.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_14.json @@ -1,10 +1,9 @@ { "created_at": "1864-05-12T23:54:25.090Z", "created_by": "00000002-0000-0002-0000-000200000000", - "email": "EI@{", + "email": "some@example", "id": "00000001-0000-0000-0000-000200000002", "name": null, - "phone": "+89058877371", "role": "owner", "team": "00000002-0000-0002-0000-000100000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_15.json b/libs/wire-api/test/golden/testObject_Invitation_team_15.json index 7d5215c7822..3409278eda5 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_15.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_15.json @@ -1,10 +1,9 @@ { "created_at": "1864-05-08T22:22:28.568Z", "created_by": null, - "email": ".@", + "email": "some@example", "id": "00000001-0000-0001-0000-000200000001", "name": "𑜘\u001f&KIL\u0013􉋏![\n6􏙭HEj4E⽨UL\u001f>2􅝓_\nJ킢Pv\u000e\u000fR碱8\u0008mS뇆mE\u0007g\u0016\u0005%㣑\u000c!\u000b\u001f𝈊\u0005𭇱󿄈\u000e83!j𒁾\u001d􅣣,\u001e\u0018F􃞋􏈇U\u0019Jb\u0011j\u0019Y𖢐O󶃯", - "phone": "+57741900390998", "role": "owner", "team": "00000000-0000-0002-0000-000100000001", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_16.json b/libs/wire-api/test/golden/testObject_Invitation_team_16.json index 853aab3be71..9b236a6526c 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_16.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_16.json @@ -1,10 +1,9 @@ { "created_at": "1864-05-09T09:56:33.113Z", "created_by": "00000001-0000-0000-0000-000100000001", - "email": "\\@\"{", + "email": "some@example", "id": "00000001-0000-0002-0000-000200000001", "name": "\u001d\u0014Q;6/_f*7􋅎\u000f+􊳊ꋢ9", - "phone": null, "role": "partner", "team": "00000001-0000-0001-0000-000100000002", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_17.json b/libs/wire-api/test/golden/testObject_Invitation_team_17.json index d7ae310a544..036e17f9aff 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_17.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_17.json @@ -1,10 +1,9 @@ { "created_at": "1864-05-08T06:30:23.239Z", "created_by": "00000000-0000-0001-0000-000000000001", - "email": "@\u0001[𗭟", + "email": "some@example", "id": "00000001-0000-0001-0000-000100000001", "name": "Z\u001b9E\u0015鍌𔗕}(3m𗮙𗷤'􅺒.WY;\u001e8?v-􌮰\u0012󸀳", - "phone": null, "role": "admin", "team": "00000000-0000-0001-0000-000100000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_19.json b/libs/wire-api/test/golden/testObject_Invitation_team_19.json index aaa9b35ce06..281f50e5b76 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_19.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_19.json @@ -1,10 +1,9 @@ { "created_at": "1864-05-07T15:08:06.796Z", "created_by": null, - "email": "󸽎𗜲@(S\u0017", + "email": "some@example", "id": "00000001-0000-0002-0000-000000000001", "name": "靸r𛋕\u0003Qi󴊗􌃗\u0019𩫻𒉓+􄮬Q?H=G-\u001e;􍝧\u000eq^K;a􀹚W\u0019 X𔖸􆂨>Mϔ朓jjbU-&󽼈v\u0000y𬙼\u0007|\u0016UfJCHjP\u000e􏘃浍DNA:~s", - "phone": "+05787228893", "role": "member", "team": "00000000-0000-0000-0000-000200000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_2.json b/libs/wire-api/test/golden/testObject_Invitation_team_2.json index 393eaccd4f2..c03242304f4 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_2.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_2.json @@ -1,10 +1,9 @@ { "created_at": "1864-05-12T14:47:35.551Z", "created_by": "00000002-0000-0001-0000-000200000001", - "email": "i@m_:", + "email": "some@example", "id": "00000002-0000-0001-0000-000100000002", "name": "􄭇} 2pGEW+\rT𩹙p𪨳𦘢&𣫡v0\u0008", - "phone": null, "role": "partner", "team": "00000000-0000-0001-0000-000000000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_20.json b/libs/wire-api/test/golden/testObject_Invitation_team_20.json index 653fafc89ea..e161b23730a 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_20.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_20.json @@ -1,10 +1,9 @@ { "created_at": "1864-05-12T08:07:17.747Z", "created_by": "00000000-0000-0001-0000-000100000001", - "email": "b@u9T", + "email": "some@example", "id": "00000002-0000-0001-0000-000000000001", "name": null, - "phone": "+27259486019", "role": "partner", "team": "00000001-0000-0000-0000-000000000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_3.json b/libs/wire-api/test/golden/testObject_Invitation_team_3.json index 6222659d12a..e3d098d2bdb 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_3.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_3.json @@ -1,10 +1,9 @@ { "created_at": "1864-05-08T22:07:35.846Z", "created_by": "00000001-0000-0002-0000-000200000001", - "email": "@秕L", + "email": "some@example", "id": "00000002-0000-0001-0000-000100000002", "name": null, - "phone": null, "role": "partner", "team": "00000002-0000-0001-0000-000100000001", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_4.json b/libs/wire-api/test/golden/testObject_Invitation_team_4.json index 8e8dedc4a4d..68f5b40a53a 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_4.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_4.json @@ -1,10 +1,9 @@ { "created_at": "1864-05-09T09:23:58.270Z", "created_by": "00000000-0000-0000-0000-000200000001", - "email": "^@e", + "email": "some@example", "id": "00000001-0000-0001-0000-000000000001", "name": null, - "phone": null, "role": "admin", "team": "00000000-0000-0000-0000-000100000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_5.json b/libs/wire-api/test/golden/testObject_Invitation_team_5.json index ce4196efbb0..dc84169a3af 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_5.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_5.json @@ -1,10 +1,9 @@ { "created_at": "1864-05-09T03:42:15.266Z", "created_by": null, - "email": "\u0001V@f􉌩꧆", + "email": "some@example", "id": "00000000-0000-0002-0000-000000000002", "name": "}G_𤃊`X󻋗𠆝󷲞L\"󿶗e6:E쨕󲟇f-$𠬒Z!s2p?#\tF 8𭿰𨕿󹵇\u0004􉢘*󸚄\u0016\u0010%Y𩀄>􏘍󾨶󺶘g\"􁥰\u001a\u001a𬇟ꦛ\u0004v𭽢,𩶐(\u001dQT𤪐;􃨚\u0005\u0017B􎇮H𩣓\\󾃾,Y", - "phone": "+45207005641274", "role": "owner", "team": "00000002-0000-0000-0000-000000000001", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_6.json b/libs/wire-api/test/golden/testObject_Invitation_team_6.json index 37e3f45bdcd..a9685bb1784 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_6.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_6.json @@ -1,10 +1,9 @@ { "created_at": "1864-05-09T08:56:40.919Z", "created_by": "00000001-0000-0001-0000-000200000000", - "email": "@OC", + "email": "some@example", "id": "00000001-0000-0002-0000-000100000000", "name": "O~\u0014U\u001e?V3_𮬰Slh􅱬Q1󶻳j|~M7􊲚􋽼𗆨\u0011K􇍼Afs𫬇lGV􏱇]`o\u0019f蓤InvfDDy\\DI𧾱􊥩\u0017B𦷬F*X\u0001\u001a얔\u0003\u0010<\u0003\u0016c\u0010,p\u000b*󵢘Vn\u000cI𑈹xS\u0002V\u001b$\u0019u󴮖xl>\u0007Z\u00144e\u0014aZ", - "phone": "+75547625285", "role": "admin", "team": "00000001-0000-0000-0000-000000000001", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_7.json b/libs/wire-api/test/golden/testObject_Invitation_team_7.json index 844522e7165..6ec71ddd5f3 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_7.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_7.json @@ -1,10 +1,9 @@ { "created_at": "1864-05-07T18:46:22.786Z", "created_by": "00000000-0000-0002-0000-000100000000", - "email": "oj@", + "email": "some@example", "id": "00000000-0000-0000-0000-000000000002", "name": "\u0018.𛅷􈼞\u0010\u000c\u0010\u0018𤰤o;Yay:yY $\u0003<ͯ%@\u001fre>5L'R\u0013𫝳oy#]c4!𘖝U홊暧󾜸􃕢p_>f\u000e𪲈􇇪󳆗_Vm\u001f}\u0002Pz\r\u0005K\u000e+>󲆠\u0000𥝻?pu?r\u001b\u001a!?𩇕;ᦅS䥅\u0007􅠬\u0008󹹝\u0006O", + "team_id": "00000007-0000-0000-0000-000600000005", + "user_id": "00000003-0000-0004-0000-000100000000" +} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirmV0_team_2.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirmV0_team_2.json new file mode 100644 index 00000000000..04e8d21059a --- /dev/null +++ b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirmV0_team_2.json @@ -0,0 +1,6 @@ +{ + "client_id": "15", + "refresh_token": "\\i", + "team_id": "00000007-0000-0004-0000-000600000002", + "user_id": "00000002-0000-0008-0000-000200000007" +} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_1.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_1.json index fdaebfe23a5..7fa3ed5707b 100644 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_1.json +++ b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_1.json @@ -1,6 +1,9 @@ { - "client_id": "1d", - "refresh_token": "i>\u0006O", - "team_id": "00000007-0000-0000-0000-000600000005", - "user_id": "00000003-0000-0004-0000-000100000000" + "client_id": "4", + "qualified_user_id": { + "domain": "example.com", + "id": "00000001-0000-0004-0000-000600000005" + }, + "refresh_token": ")", + "team_id": "00000003-0000-0005-0000-000100000001" } diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_10.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_10.json deleted file mode 100644 index 0db069bd046..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_10.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "20", - "refresh_token": "", - "team_id": "00000001-0000-0005-0000-000700000001", - "user_id": "00000006-0000-0005-0000-000500000006" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_11.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_11.json deleted file mode 100644 index 4b0fa0f0f41..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_11.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "0", - "refresh_token": "𥟟@-c\u0005", - "team_id": "00000002-0000-0005-0000-000400000007", - "user_id": "00000006-0000-0002-0000-000700000007" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_12.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_12.json deleted file mode 100644 index 672cc4cae73..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_12.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "0", - "refresh_token": "", - "team_id": "00000007-0000-0008-0000-000600000006", - "user_id": "00000005-0000-0006-0000-000500000004" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_13.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_13.json deleted file mode 100644 index 9130ffaaa1e..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_13.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "c", - "refresh_token": "DXD[", - "team_id": "00000004-0000-0000-0000-000100000007", - "user_id": "00000002-0000-0005-0000-000600000005" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_14.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_14.json deleted file mode 100644 index 7423e75fbcc..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_14.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "2", - "refresh_token": "T􄳀\u0013𫙻\u0002", - "team_id": "00000004-0000-0001-0000-000400000003", - "user_id": "00000007-0000-0003-0000-000200000003" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_15.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_15.json deleted file mode 100644 index 1085765f45f..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_15.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "1a", - "refresh_token": "\n' \u001c~𡢇)", - "team_id": "00000004-0000-0003-0000-000100000004", - "user_id": "00000005-0000-0005-0000-000300000007" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_16.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_16.json deleted file mode 100644 index 0e1faaefeb5..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_16.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "e", - "refresh_token": "𐅻𧵈\n", - "team_id": "00000002-0000-0001-0000-000300000000", - "user_id": "00000003-0000-0002-0000-000000000000" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_17.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_17.json deleted file mode 100644 index 3dafa3dea0a..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_17.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "e", - "refresh_token": "", - "team_id": "00000002-0000-0000-0000-000400000008", - "user_id": "00000002-0000-0001-0000-000600000004" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_18.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_18.json deleted file mode 100644 index 131733c04f9..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_18.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "11", - "refresh_token": "Y󻒎", - "team_id": "00000006-0000-0003-0000-000100000005", - "user_id": "00000006-0000-0000-0000-000800000004" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_19.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_19.json deleted file mode 100644 index 6f4b9be7a24..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_19.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "1c", - "refresh_token": "[", - "team_id": "00000001-0000-0003-0000-000600000000", - "user_id": "00000003-0000-0006-0000-000700000002" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_2.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_2.json index 04e8d21059a..f83dc811076 100644 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_2.json +++ b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_2.json @@ -1,6 +1,9 @@ { - "client_id": "15", - "refresh_token": "\\i", - "team_id": "00000007-0000-0004-0000-000600000002", - "user_id": "00000002-0000-0008-0000-000200000007" + "client_id": "1b", + "qualified_user_id": { + "domain": "example.com", + "id": "00000008-0000-0002-0000-000300000001" + }, + "refresh_token": "W", + "team_id": "00000004-0000-0008-0000-000300000004" } diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_20.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_20.json deleted file mode 100644 index 154b8783add..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_20.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "1", - "refresh_token": "i\u001c", - "team_id": "00000006-0000-0001-0000-000500000008", - "user_id": "00000001-0000-0004-0000-000600000005" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_3.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_3.json deleted file mode 100644 index d8fe0054b34..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_3.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "4", - "refresh_token": ")", - "team_id": "00000003-0000-0005-0000-000100000001", - "user_id": "00000001-0000-0004-0000-000600000005" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_4.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_4.json deleted file mode 100644 index 96f2d5c2980..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_4.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "1b", - "refresh_token": "W", - "team_id": "00000004-0000-0008-0000-000300000004", - "user_id": "00000008-0000-0002-0000-000300000001" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_5.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_5.json deleted file mode 100644 index 94aec8f7721..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_5.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "12", - "refresh_token": "󹟔hL􍂭崰𫾇", - "team_id": "00000002-0000-0008-0000-000400000007", - "user_id": "00000000-0000-0005-0000-000300000006" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_6.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_6.json deleted file mode 100644 index 4674ca57a65..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_6.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "1", - "refresh_token": "􊅝󰇡b", - "team_id": "00000004-0000-0008-0000-000200000006", - "user_id": "00000005-0000-0002-0000-000300000003" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_7.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_7.json deleted file mode 100644 index 2fd9f36310b..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_7.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "1c", - "refresh_token": "􀃬[\u0017u\r", - "team_id": "00000006-0000-0004-0000-000500000003", - "user_id": "00000005-0000-0001-0000-000600000001" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_8.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_8.json deleted file mode 100644 index c2c75b63951..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_8.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "1f", - "refresh_token": "ZU󱲛;\u001f\u001b", - "team_id": "00000004-0000-0004-0000-000500000004", - "user_id": "00000003-0000-0008-0000-000200000001" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_9.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_9.json deleted file mode 100644 index 659092595c6..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_9.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "3", - "refresh_token": "Y􉲾", - "team_id": "00000008-0000-0006-0000-000000000006", - "user_id": "00000003-0000-0008-0000-000100000003" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemoveV0_team_1.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemoveV0_team_1.json new file mode 100644 index 00000000000..0486a9283bb --- /dev/null +++ b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemoveV0_team_1.json @@ -0,0 +1,4 @@ +{ + "team_id": "0000001e-0000-000f-0000-007100000079", + "user_id": "00000034-0000-0016-0000-003c00000024" +} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemoveV0_team_2.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemoveV0_team_2.json new file mode 100644 index 00000000000..68cf570501e --- /dev/null +++ b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemoveV0_team_2.json @@ -0,0 +1,4 @@ +{ + "team_id": "00000050-0000-0059-0000-004d00000067", + "user_id": "0000004f-0000-0076-0000-001f00000019" +} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_1.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_1.json index 0486a9283bb..6ffcd4de35b 100644 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_1.json +++ b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_1.json @@ -1,4 +1,7 @@ { - "team_id": "0000001e-0000-000f-0000-007100000079", - "user_id": "00000034-0000-0016-0000-003c00000024" + "qualified_user_id": { + "domain": "example.com", + "id": "00000034-0000-0016-0000-003c00000024" + }, + "team_id": "0000001e-0000-000f-0000-007100000079" } diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_10.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_10.json deleted file mode 100644 index 03bdfa92961..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_10.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000000d-0000-0013-0000-007100000063", - "user_id": "00000077-0000-0003-0000-001b00000033" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_11.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_11.json deleted file mode 100644 index d0e2464f10a..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_11.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000009-0000-007b-0000-00050000004b", - "user_id": "00000062-0000-0018-0000-007b0000002e" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_12.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_12.json deleted file mode 100644 index 8c99f2a8e65..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_12.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000023-0000-0000-0000-004100000061", - "user_id": "00000017-0000-0030-0000-002d0000002b" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_13.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_13.json deleted file mode 100644 index 67e2140a274..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_13.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000055-0000-0050-0000-000600000019", - "user_id": "00000055-0000-005d-0000-00140000001a" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_14.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_14.json deleted file mode 100644 index 388c2e9a932..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_14.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000001b-0000-005f-0000-006b00000040", - "user_id": "00000015-0000-0061-0000-003e00000067" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_15.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_15.json deleted file mode 100644 index 79591c3552d..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_15.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000004e-0000-0066-0000-002c00000021", - "user_id": "0000006a-0000-005d-0000-005d00000072" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_16.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_16.json deleted file mode 100644 index 6a71c6ec5c6..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_16.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000000d-0000-0001-0000-000500000049", - "user_id": "0000005c-0000-0064-0000-00120000002a" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_17.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_17.json deleted file mode 100644 index 130cc163be3..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_17.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000019-0000-002e-0000-005c00000010", - "user_id": "00000068-0000-001b-0000-006a0000005a" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_18.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_18.json deleted file mode 100644 index 17ba095a31d..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_18.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000019-0000-003f-0000-007000000071", - "user_id": "0000007d-0000-0044-0000-004d00000004" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_19.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_19.json deleted file mode 100644 index fb62a066aaa..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_19.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000014-0000-0022-0000-005a00000075", - "user_id": "00000040-0000-0053-0000-00060000001b" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_2.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_2.json index 68cf570501e..0ecc091028d 100644 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_2.json +++ b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_2.json @@ -1,4 +1,7 @@ { - "team_id": "00000050-0000-0059-0000-004d00000067", - "user_id": "0000004f-0000-0076-0000-001f00000019" + "qualified_user_id": { + "domain": "example.com", + "id": "0000004f-0000-0076-0000-001f00000019" + }, + "team_id": "00000050-0000-0059-0000-004d00000067" } diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_20.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_20.json deleted file mode 100644 index ba02dfea16f..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_20.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000006d-0000-006f-0000-007c0000006e", - "user_id": "00000012-0000-005d-0000-00790000003e" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_3.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_3.json deleted file mode 100644 index 059084d01ed..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_3.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000006c-0000-005c-0000-002100000019", - "user_id": "0000001a-0000-0072-0000-003e00000008" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_4.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_4.json deleted file mode 100644 index 84c9f3ee38f..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_4.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000007c-0000-0060-0000-007400000077", - "user_id": "0000003c-0000-0013-0000-003b00000001" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_5.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_5.json deleted file mode 100644 index e7fe336fae4..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_5.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000003f-0000-002e-0000-003900000032", - "user_id": "00000000-0000-005e-0000-00680000007c" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_6.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_6.json deleted file mode 100644 index 1a4931a609c..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_6.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000005d-0000-0053-0000-005f00000044", - "user_id": "0000004b-0000-0014-0000-007e00000010" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_7.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_7.json deleted file mode 100644 index 3f58bdb6099..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_7.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000002d-0000-002b-0000-005c0000003c", - "user_id": "0000002c-0000-0020-0000-003900000073" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_8.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_8.json deleted file mode 100644 index 4d8757d78e7..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_8.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000060-0000-007d-0000-002c00000059", - "user_id": "0000003a-0000-0066-0000-001a0000001e" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_9.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_9.json deleted file mode 100644 index af27be7b6d1..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_9.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000006e-0000-0072-0000-00260000000a", - "user_id": "00000037-0000-0024-0000-005e00000067" -} diff --git a/libs/wire-api/test/golden/testObject_ListUsersById_user_1.json b/libs/wire-api/test/golden/testObject_ListUsersById_user_1.json index 98cdc013c84..7bbbb327571 100644 --- a/libs/wire-api/test/golden/testObject_ListUsersById_user_1.json +++ b/libs/wire-api/test/golden/testObject_ListUsersById_user_1.json @@ -1 +1,3 @@ -{ "found" : [] } \ No newline at end of file +{ + "found": [] +} 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 cdc89915f2d..aebbe2c3d03 100644 --- a/libs/wire-api/test/golden/testObject_ListUsersById_user_2.json +++ b/libs/wire-api/test/golden/testObject_ListUsersById_user_2.json @@ -1,27 +1,36 @@ -{ "found" : - [ { "qualified_id" : - { "domain" : "example.com" - , "id" : "4f201a43-935e-4e19-8fe0-0a878d3d6e74" - } - , "id" : "4f201a43-935e-4e19-8fe0-0a878d3d6e74" - , "name" : "user1" - , "picture" : [] - , "assets" : [] - , "accent_id" : 0 - , "legalhold_status" : "disabled" - , "supported_protocols" : ["proteus"] - } - , { "qualified_id" : - { "domain" : "test.net" - , "id" : "eb48b095-d96f-4a94-b4ec-2a1d61447e13" - } - , "id" : "eb48b095-d96f-4a94-b4ec-2a1d61447e13" - , "name" : "user2" - , "picture" : [] - , "assets" : [] - , "accent_id" : 0 - , "legalhold_status" : "disabled" - , "supported_protocols" : ["proteus", "mls"] - } - ] +{ + "found": [ + { + "accent_id": 0, + "assets": [], + "id": "4f201a43-935e-4e19-8fe0-0a878d3d6e74", + "legalhold_status": "disabled", + "name": "user1", + "picture": [], + "qualified_id": { + "domain": "example.com", + "id": "4f201a43-935e-4e19-8fe0-0a878d3d6e74" + }, + "supported_protocols": [ + "proteus" + ] + }, + { + "accent_id": 0, + "assets": [], + "id": "eb48b095-d96f-4a94-b4ec-2a1d61447e13", + "legalhold_status": "disabled", + "name": "user2", + "picture": [], + "qualified_id": { + "domain": "test.net", + "id": "eb48b095-d96f-4a94-b4ec-2a1d61447e13" + }, + "supported_protocols": [ + "proteus", + "mls" + ], + "text_status": "text status" + } + ] } 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 0856a277a59..6b7b73fe33d 100644 --- a/libs/wire-api/test/golden/testObject_ListUsersById_user_3.json +++ b/libs/wire-api/test/golden/testObject_ListUsersById_user_3.json @@ -1,20 +1,25 @@ -{ "found" : - [ { "qualified_id" : - { "domain" : "example.com" - , "id" : "4f201a43-935e-4e19-8fe0-0a878d3d6e74" - } - , "id" : "4f201a43-935e-4e19-8fe0-0a878d3d6e74" - , "name" : "user1" - , "picture" : [] - , "assets" : [] - , "accent_id" : 0 - , "legalhold_status" : "disabled" - , "supported_protocols" : ["proteus"] - } - ] -, "failed" : - [ { "domain" : "test.net" - , "id" : "eb48b095-d96f-4a94-b4ec-2a1d61447e13" - } - ] +{ + "failed": [ + { + "domain": "test.net", + "id": "eb48b095-d96f-4a94-b4ec-2a1d61447e13" + } + ], + "found": [ + { + "accent_id": 0, + "assets": [], + "id": "4f201a43-935e-4e19-8fe0-0a878d3d6e74", + "legalhold_status": "disabled", + "name": "user1", + "picture": [], + "qualified_id": { + "domain": "example.com", + "id": "4f201a43-935e-4e19-8fe0-0a878d3d6e74" + }, + "supported_protocols": [ + "proteus" + ] + } + ] } diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_1.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_1.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_1.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_1.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_10.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_10.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_10.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_10.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_11.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_11.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_11.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_11.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_12.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_12.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_12.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_12.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_13.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_13.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_13.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_13.json diff --git a/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_14.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_14.json new file mode 100644 index 00000000000..da8c2395e05 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_14.json @@ -0,0 +1,7 @@ +{ + "config": { + "useSFTForOneToOneCalls": true + }, + "lockStatus": "unlocked", + "ttl": "unlimited" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_15.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_15.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_15.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_15.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_16.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_16.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_16.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_16.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_17.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_17.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_17.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_17.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_18.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_18.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_18.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_18.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_19.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_19.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_19.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_19.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_2.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_2.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_2.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_2.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_3.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_3.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_3.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_3.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_4.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_4.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_4.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_4.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_5.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_5.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_5.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_5.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_6.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_6.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_6.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_6.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_7.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_7.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_7.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_7.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_8.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_8.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_8.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_8.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_9.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_9.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_9.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_9.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_1.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_1.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_1.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_1.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_10.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_10.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_10.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_10.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_11.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_11.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_11.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_11.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_12.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_12.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_12.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_12.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_13.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_13.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_13.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_13.json diff --git a/libs/wire-api/test/golden/testObject_LockableFeature_team_14.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_14.json new file mode 100644 index 00000000000..447824226ce --- /dev/null +++ b/libs/wire-api/test/golden/testObject_LockableFeature_team_14.json @@ -0,0 +1,8 @@ +{ + "config": { + "useSFTForOneToOneCalls": false + }, + "lockStatus": "unlocked", + "status": "disabled", + "ttl": "unlimited" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_15.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_15.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_15.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_15.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_16.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_16.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_16.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_16.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_17.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_17.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_17.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_17.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_18.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_18.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_18.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_18.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_19.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_19.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_19.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_19.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_2.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_2.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_2.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_2.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_3.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_3.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_3.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_3.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_4.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_4.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_4.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_4.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_5.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_5.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_5.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_5.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_6.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_6.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_6.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_6.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_7.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_7.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_7.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_7.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_8.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_8.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_8.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_8.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_9.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_9.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_9.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_9.json diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_1.json b/libs/wire-api/test/golden/testObject_LoginId_user_1.json index 6da1b3e82f8..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_LoginId_user_1.json +++ b/libs/wire-api/test/golden/testObject_LoginId_user_1.json @@ -1,3 +1,3 @@ { - "email": "~]z^?j\u0015􉮏􏫮X{)􉙴t\u000c@􏯕\n\u000bL$\u0005Y\u0000Uj?H%" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_10.json b/libs/wire-api/test/golden/testObject_LoginId_user_10.json deleted file mode 100644 index 59286829b46..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_10.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "handle": "z58-6fbjhtx11d8t6oplyijpkc2.fp_lf3kpk3_.qle4iecjun2xd0tpcordlg2bwv636v3cthpgwah3undqmuofgzp8ry6gc6g-n-kxnj7sl6771hxou7-t_ps_lu_t3.4ukz6dh6fkjq2i3aggtkbpzbd1162.qv.rbtb6e.90-xpayg65z9t9lk2aur452zcs9a" -} diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_11.json b/libs/wire-api/test/golden/testObject_LoginId_user_11.json deleted file mode 100644 index 8c7cad3cfab..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_11.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "𥦴𢒵A󻵨ovP Ig𖦢t';ᠷ\u0001C爄𦟀{\n%􊑂\u000b2\u001d𬅍􏫣&@m𫼩U{f&.3༆1?Ew短G-" -} diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_12.json b/libs/wire-api/test/golden/testObject_LoginId_user_12.json deleted file mode 100644 index ee43cc24642..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_12.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "@䜸\u0019+h\u0005(D\u000e灕󲤉 \u0007\r1" -} diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_13.json b/libs/wire-api/test/golden/testObject_LoginId_user_13.json deleted file mode 100644 index 8761ea668a0..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_13.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "5-h􋆢󶵘&$og􈰰晲󱣇<%ଧ\u000cGF-yJ\u000c*cK@*g\u0019𝞶7$L\u0018\tV􍇺D\u0007\\yK􊍌T" -} diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_14.json b/libs/wire-api/test/golden/testObject_LoginId_user_14.json deleted file mode 100644 index e2ef145546e..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_14.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "phone": "+8668821360611" -} diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_15.json b/libs/wire-api/test/golden/testObject_LoginId_user_15.json deleted file mode 100644 index 1ce0be5c9f2..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_15.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "\u0006\u0005X\u0006&𗊭8󿃅7E`Y'\u0011TV\u0006\u0010@\u001d\u001bj󳼗,j󲺅󾭍#a1)}\u0013Vk\u0001Q7&;" -} diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_16.json b/libs/wire-api/test/golden/testObject_LoginId_user_16.json deleted file mode 100644 index 010d203dde6..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_16.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "󷔯1@\u000b`\u0019순v􈔿;F䢺0ျSgu%>􆺅y\u000b󸣠\u0015𠢼\u001f󺘓\u0006s\u000f\u0007\u001f" -} diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_17.json b/libs/wire-api/test/golden/testObject_LoginId_user_17.json deleted file mode 100644 index 4c307e4295a..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_17.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "handle": "e3iusdy" -} diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_18.json b/libs/wire-api/test/golden/testObject_LoginId_user_18.json deleted file mode 100644 index 5d067be0f3e..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_18.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "handle": "8vpices3usz1dfs4u2lf_e3jendod_szl1z111_eoj4b7k7ajj-xo.qzbw4espf3smnz_" -} diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_19.json b/libs/wire-api/test/golden/testObject_LoginId_user_19.json deleted file mode 100644 index adb91ee9260..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_19.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "handle": "3jzpp2bo8" -} diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_2.json b/libs/wire-api/test/golden/testObject_LoginId_user_2.json index 104b2810dc6..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_LoginId_user_2.json +++ b/libs/wire-api/test/golden/testObject_LoginId_user_2.json @@ -1,3 +1,3 @@ { - "phone": "+178807168" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_20.json b/libs/wire-api/test/golden/testObject_LoginId_user_20.json deleted file mode 100644 index a361b16adfa..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_20.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "@𦃻" -} diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_3.json b/libs/wire-api/test/golden/testObject_LoginId_user_3.json index 7f8f368af3c..809acaf04c8 100644 --- a/libs/wire-api/test/golden/testObject_LoginId_user_3.json +++ b/libs/wire-api/test/golden/testObject_LoginId_user_3.json @@ -1,3 +1,3 @@ { - "email": "0􉵟^󴊽𣎋\u0000)|𬱓:@q6e/$󼐅Zb􀖑)󱿷05i乭~q􅨬🙈y" + "handle": "7a8gg3v98" } diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_4.json b/libs/wire-api/test/golden/testObject_LoginId_user_4.json index 809acaf04c8..a0990b9f33e 100644 --- a/libs/wire-api/test/golden/testObject_LoginId_user_4.json +++ b/libs/wire-api/test/golden/testObject_LoginId_user_4.json @@ -1,3 +1,3 @@ { - "handle": "7a8gg3v98" + "handle": "lb" } diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_5.json b/libs/wire-api/test/golden/testObject_LoginId_user_5.json index ee5aa511ee7..59286829b46 100644 --- a/libs/wire-api/test/golden/testObject_LoginId_user_5.json +++ b/libs/wire-api/test/golden/testObject_LoginId_user_5.json @@ -1,3 +1,3 @@ { - "phone": "+041157889572" + "handle": "z58-6fbjhtx11d8t6oplyijpkc2.fp_lf3kpk3_.qle4iecjun2xd0tpcordlg2bwv636v3cthpgwah3undqmuofgzp8ry6gc6g-n-kxnj7sl6771hxou7-t_ps_lu_t3.4ukz6dh6fkjq2i3aggtkbpzbd1162.qv.rbtb6e.90-xpayg65z9t9lk2aur452zcs9a" } diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_6.json b/libs/wire-api/test/golden/testObject_LoginId_user_6.json index 0af8c1681cc..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_LoginId_user_6.json +++ b/libs/wire-api/test/golden/testObject_LoginId_user_6.json @@ -1,3 +1,3 @@ { - "phone": "+2351341820189" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_7.json b/libs/wire-api/test/golden/testObject_LoginId_user_7.json deleted file mode 100644 index a0990b9f33e..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_7.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "handle": "lb" -} diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_8.json b/libs/wire-api/test/golden/testObject_LoginId_user_8.json deleted file mode 100644 index 984511ad78b..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_8.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "phone": "+2831673805093" -} diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_9.json b/libs/wire-api/test/golden/testObject_LoginId_user_9.json deleted file mode 100644 index 9d3fe372cee..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_9.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "phone": "+1091378734554" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_1.json b/libs/wire-api/test/golden/testObject_Login_user_1.json index 9c1fccea60b..e7ad6466d34 100644 --- a/libs/wire-api/test/golden/testObject_Login_user_1.json +++ b/libs/wire-api/test/golden/testObject_Login_user_1.json @@ -1,5 +1,5 @@ { - "email": "4􅄛\u000bEaP@\u0005\n\u001c\u001b󳟬i03!", + "email": "some@example", "label": "r", "password": "\u00085Ta𤱷𭥜fa&󿳄o!ov\u000f􌥥i\u0014\u0003Y\u000eR󲁛􉋏Ta^s\u0017\u000f[𮊌󱛣枌\u00186\u0002p􅆖-9󰌏&\u0015􀶤]^㋴;>-Z$Z\u0015\r􌻮a\u001e%\u0000:𮄱먺𦝬?e]\u0003 𢴐 C\u0001\u000fS%8m􊦓V𣺻[󵪶6𩹚󶸓𨌰SX\n%􃆋*>\t+𠕋Y󱥶󲡂\u001dU􄨕6TU!*鲲90􁬜\u001eV𧪳N\t*\u0004{I<􈭶\u0001𬭌!c\\\n􎘭𬭪\u0011,-xX\u0019V?\t𩋈􁘟\u00121\u0001u\u0001콅\u000e+h\u0006::\u001e卬_g,\u000e*\u000b\u0014􋁎HFF𮇶􇻳fF\u001b2\u0001T\u0011)\u000cc豁l􃊫\u000c#~\u0002]󼭎/Or)kY󻳿\u0001NCk􄮲5􈡎x=H\u0000峐􂝖􌕙E/$pbi𡤲\u001cKi㴼󸤖\t7\"OL폀ᵜ5꧊\u0000(󻫄𨩲\u0001𤝟󲸓掩C==\u001dTV3l6󳹞.Z)$䅓|𪊼􊋿J;O\u001dbw\u000bI􌳠I\u0016\u0012^𤡾\u00023%i\u0019W𡵶\u0014􏸓tsL5𣺏W𗦼(_,􊙫*󾎇rckx\u0001\u000fs\u0001Jd𢔞\u0016ev.\u0014\u0010𘌊.􎍡󳚀𣁘\u001f_\u0017f\u0002\u000e\u0013󾴤6O\u0011Q\u0001'\u001d,|]W\u000fa𤸖.\u000b\u0007H&-L\u0012+𣻫􋝤\u0004m)䷕𬎛𬱈!𭎇𢹢m\u0014\u0013󼠪m\u001d𭒥>>\"NDw􆺍hY󼙧sFKz^ 􎣛5Qec\u0015}|􎣢.Q𪐺imb󺲔 p;􉸺\u0016􄌔kF􍐆r8o\u0011", "verification_code": null diff --git a/libs/wire-api/test/golden/testObject_Login_user_10.json b/libs/wire-api/test/golden/testObject_Login_user_10.json deleted file mode 100644 index 4c70905e87c..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_10.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "㑃𡃨!\u0017i􀖿", - "label": "", - "phone": "+4211134144507" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_11.json b/libs/wire-api/test/golden/testObject_Login_user_11.json deleted file mode 100644 index 00eae0e5a33..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_11.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "􅅣+W\u00193", - "label": null, - "phone": "+338932197597737" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_12.json b/libs/wire-api/test/golden/testObject_Login_user_12.json deleted file mode 100644 index e3414dd15b4..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_12.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "label": "\u000f🜓-𞡌:𡍁鮸\u0006\u000e", - "password": "n􋜩Q𩗀\u001b󵅀&Q/\rdꠚ\u001f\u0004w2C\u0006􁹬𫝔\u0004\u0004v󶥜\u0008f,b\u0002󷜰'𪹐C]G듡󸓯𮤾4\u0000Y.𪘲\u000e3sI菌F􈲾5剑rG/:\"󷣦X띟6\u001c:\u0018\u0007eYwWT􈦚𡛑Msbm\u0015@󰗜󷜉\u0004^\u001c𣹘\u0015@\u0005>\u000c\u001eUc\u0004V9&cල\u0007󰱴a'PPG𘡝𫶶>[ൽ2ﷄXc𠃪[0󴲖\u0008𘕄B\u0011[󻑵\u001d䰻\u001f\u0019s-u\u0017s􄡽󵗐𧝿n􅳀?󿒋ck\u00148XC𪣑\u001eI2ମ\u0002\u0010M\u001b\n?<\\\u0013E𑨛\u001d\n$cyS𡐆!,\u000b9\u0017/\u0011?P\u0017ꌞ\u0012󴁱~􂟉W-W4K8.\u00127\u0019L􇌡h\u000f}t+H\u001a\u001bX𝛋s\u0004t𫘧taa\u001d\u000c𥌭(v󺈨M\u001bvg3P1󼊃]gཝ4T\u0015$镄);\\8􎲭\nK\u0015}D'^fJ'𢽥e𪟤骭!\u0019.\u0012{\\CEp󿎈\u0017k_􈨀䟝𨄪􃨬]MG$𭴂[E􏠾\u0008􆅏{b엚\u001b^b@W\u0015$\u001c<󹾗&𦅘R\u0006J\u000f􊷴􌳱ꇞn󵸞8]𤍀\u0005}|k\u0002\u0018Q\u001fI\u0007\u0018DZ􃟝\u0000쐕rb䨃3G%\u001c𧤡\u0004\u00154YB0-i󸣑IM􆋴[􏘂:Cr$𘔴)L𡚅W鿁.x;ꇵ󻨷󳃅\u001fkb\u0018Y9)\u00164\u000f􍙥Av.\r\u000c􃏥9{\u000e\u0017P\u000c茂u\r-9cB4󸄛G\u001e夡󷯔r📷HcsPms𝢛!|J<\u00108\u001c[\u0015WYAK𒔃^󰾪c3󾜀\u0007C\u0003\u0017􁐫Y\u0014f\u0006􃁑!󼀑:RlQ!BX\u000c=􅙦f𤽂𛰿O\u0003\\\"퀛B<\u001eLV4g%3􌅏\u0006`\u0015>\n깒kp󰯶𩷗H冘lyJ\u0012)􁦭(󺰛A\u001ch\u0004j観\u0014M\u001bP-q\u0008n\u0018𢿎~\u001d\u0019\"o合%*e2𨛝L󹼿sy𥕑2m\u001d􀇖{EG]\u00116B+{󰉆IYa󶈙5,<\u001bX\u000c\u000f𭣵𥢐E𠴇󶶐L<\u0019dUO\u0017\u001aZYm\u0006􉍰R\u001a󲋒\u0013^s\u000cu_g\u0019?i~}V2𤓉R\u001c\u00043j댑m؆􌱔\n7S\u000fT5j𩮢\u000f󷵝𢤓h𬣐Q𣲺\u0015ZM􏈮02f🤼l!*\u0001󺯙\u0001􅔰􋑷\t𑱥\u001ba:q UKEN\u001e-\n\u0003H坝a􆘓\u0008鉶\"󼳴𤢿󼎳R4\u0003\u0010\u001c\u0002󵓎%\"@󶛙6=/x\u0000P\u0004𪬗/𮙙\u000c\u000c󵙚?*\u000cIcKWQ\"󴣾P*􋢩6=d\n𦟰\u001e􉧚\u0004\u0012I릍U\u0008=Pc\u0010", - "phone": "+153353668", - "verification_code": "RcplMOQiGa-JY" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_13.json b/libs/wire-api/test/golden/testObject_Login_user_13.json deleted file mode 100644 index 5b2504ef693..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_13.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "&󾂂y", - "label": null, - "phone": "+626804710" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_14.json b/libs/wire-api/test/golden/testObject_Login_user_14.json deleted file mode 100644 index 779e05afdc9..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_14.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "", - "label": "𗘼搊", - "phone": "+5693913858477" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_15.json b/libs/wire-api/test/golden/testObject_Login_user_15.json deleted file mode 100644 index b0b8cce642e..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_15.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "", - "label": "q\u0017(􉓔𭯸>8𢢂\n6", - "phone": "+56208262" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_16.json b/libs/wire-api/test/golden/testObject_Login_user_16.json deleted file mode 100644 index 778e9add165..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_16.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "_􏊊󵇀􎨕-_\u0017", - "label": "\u000eL􇜨󶔫􂰈@\u001c\u0010$", - "phone": "+588058222975" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_17.json b/libs/wire-api/test/golden/testObject_Login_user_17.json deleted file mode 100644 index a211d02d3a9..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_17.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "\u00171󴷦n\u0010dV󻦊d\u0001", - "label": "􏣙{/p𘝶", - "phone": "+3649176551364" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_18.json b/libs/wire-api/test/golden/testObject_Login_user_18.json deleted file mode 100644 index 25726fe2df5..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_18.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": ",𢆡㖮,", - "label": "5", - "phone": "+478931600" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_19.json b/libs/wire-api/test/golden/testObject_Login_user_19.json deleted file mode 100644 index 4fb0e32cbc2..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_19.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "x橷<", - "label": "w;U\u001bx:", - "phone": "+92676996582869" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_2.json b/libs/wire-api/test/golden/testObject_Login_user_2.json index ccab54a54bc..b61e6008106 100644 --- a/libs/wire-api/test/golden/testObject_Login_user_2.json +++ b/libs/wire-api/test/golden/testObject_Login_user_2.json @@ -1,5 +1,6 @@ { - "code": "\nG􆶪8\u0008", - "label": "G", - "phone": "+956057641851" + "handle": "c2wp.7s5.", + "label": "􈏺𐌺>XC", + "password": "&\u001e\u0014􍢴ZⲚn\u000e𦯣󶳚/🄕'􃡾m󶪩\"☬𡷝\u001e(&󳓮\u000ef1Wf'I\u000f𘞾󿫦󼛩\u0011Jq􀠱Y\\Bedu@󷭷c󵇒D쿛􀟶S𣐞\u0003\u0003W>󵜮\u0014\rSO8FXy𨮱a\u0019𩠡\u001aNF𦧁L\u001e$5\u0000k\u001ez*s𤔬𦤜\u000b𪴹\"SY\u0002󲶃􍚚ub5q\u0005󷨛\u000bN.\t𬳰:l􍷴\u001e󺺉\u0007𩁁\u000e\u000bt􌏐W\u0016󾟜􎿛\u0007'v\u0017䀘\u0015\u0002 \u0015\u0002숔,􏙎x󿱴^􄡷櫦I;\u0015b􊧑o𧯋_𮡒MME󹩀\u000f􋨼H;\u000e\u0017s\u000e􄏑{Knlrd;讦\u0014\u000f􆝀TO􊏡󴃗U뺓􌢗t􄺈^y䍴u$\u0011Jp􁙤𨐩𨉞\u0002\"􋛧*\u000e󵌎綦󱻌X􌑜\u0003sK}\u0008𣈮\u00000󱘴12𩱬\tM052𮑯\u00040\u001e󰰚􈴐{ji\u001b󹎀橻&t \u000f\u001by\u0007L𡎯𠇦󲫫\r􁡥ga,\u0014do,tx[I&\u0014h\u0010\u0003\u0010Bpm󴬴-\u0007]/ZI󼎝q]w3n뜿e岌kYo5􊔜'K􊄜}v𣵇;󸮨\\=ꄰ8g\u0010g*has걿󵨦\u0013\u001fYg?I䰆\u0015aW2𤮏m\t}h𥸙RbU\u0002\u0017lz2!\u0013JW5\u001b󺡬U\u000eg,rpOᛡ]0\u001bǟ󵞃F\u000f󿗪\u001e\u000e⺄rl􍦲~\u0006+Mn{5󲧸a\u00192\u000b{jM\u0017T􂔹$\u0011􌣆\u001dj_~Z󵸥P\u0001\u0004o@TJhk\u0004\u0017k:-𗥇[p\u0010\u0011\u001e'\r\u0002Q,,󸐢?H\rh瘑\rj𤈎\u0012\\(u\u001bu𥱑󴳈o\u0014󱕌􍙩􀶂\u0011q\u001d-\u0008齧\u0011qW>\u000cysῂ,'𧃒<", + "verification_code": "RcplMOQiGa-JY" } diff --git a/libs/wire-api/test/golden/testObject_Login_user_20.json b/libs/wire-api/test/golden/testObject_Login_user_20.json deleted file mode 100644 index bfe793f70bb..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_20.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "email": "[%@,", - "label": "􁫀\r9󳰔`\u0015x", - "password": "ryzP\u00139⬓-1A)\u0008,u℉j~0􊐔󼘏\u000cI𩤎er\u0014V|}'kzG%A;3H\u0007mD\u0002U1\u0000^󾴴\u0010O&5u\u0004\u001a𨲆0A󳍿X\u0012\u001c7fEt𗱖rPvytT𡛓!𘥩$Q|BI+EM5\u0015\tRKrE\u0010\u001f\r?.\u0002|@1v^\u000bycpu\n$\u0012𭤳𠊆-Q𤸩\n\r󼛽𐬝O\u0005*𐰴Z\u001fo\u0004n𮂕%&\u0013Me*\u0002;\u0010034\nv\u0015𢑮(􆤦󱮺n@􎥹|봥d\n*\u000f\u0000}\u0015A!󿕺󽃯Hx\u00173\u0002{#T|5|GC􉸮z.\u001fN􇸓圴\u000bu\u0016~LP𤁿CV\u000e q𥆐\u0012e8h\u001fg󸷞;\u000c󳌋􎫐At󹦊)\u001fG\u0013𨪍馩|󾙻\u000f𠮹\u0004c~6\u0010:u𨘑##^\nn{d\u0018\ng㽭\u001b\u001f\u001f~A8};T\u001e\u0015)&\u0008\u0006􎁼\u001d(\u0013u;􋐛;=e􀨚\"黝vCt)o󰽾mꮈ𓄈l1+󼿼[\u0002FLx􇹤:󻼥󲗰71/kE𖹛p\u0014Ij\u0017蓳&\u001a^\u001cl1\u0006󸺜\u0003W,+3🐺𗖷\u001077rG'􇚂JC9M􁑬\u0016\u0012괾>~󸇴Y􃒫=i-\u000cS𪆘𦍨K2-@\u0005\u000c􎭳_1D-&🖂lR𭭰/󲫄$:窷:찫Dg󷷋O󶧽𩢅\u000e𫹟2z\u0015q𢣫c\u001cliJ{􁲵􂳦'BL𩋞;\u0002󿤼䠋B\u0000ẟb􅶹:w􎠰Ad\u001a6\u0015oퟯ\nsPWM{\u0003fW󸨅JT󹖱$󱞍핐𮝮𪓋u4􍖶\t蓥󽱢\"𥚰UM􈫴􋛮蔹􍶭\t\nIn'􅗄剩㻛\u0019\u0011<\u000b\u0008W\u000f}𢧯\u0008􅳓󼰓\u001d`􋍃x\u0000󰼹K\u001cj􇟷\u0011\u000f𩐠d󲆄k4\u001a󶣔쌗^􀾃󸐫i2=$:[f􃺃\u0012n\u0015J<=߬\u000f!z􍷔\u000eN\u0015\u0019𬈌V󺍬CQ_G\nY#ky𠚫k\u0013\u0005}OC𗤶}~M\u0019p\u0003\u001ex\u0008𬺚􅽰\u00088/\u0014?􈄶B󺝎\u0004\u000eU󹏩\u001b=%읶J𩎗\u0017󲕑󱱨󰡢\to􌳬X_@@뀷ꮰ$", - "verification_code": "RcplMOQiGa-JY" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_3.json b/libs/wire-api/test/golden/testObject_Login_user_3.json index b61e6008106..7042d152a24 100644 --- a/libs/wire-api/test/golden/testObject_Login_user_3.json +++ b/libs/wire-api/test/golden/testObject_Login_user_3.json @@ -1,6 +1,6 @@ { - "handle": "c2wp.7s5.", - "label": "􈏺𐌺>XC", - "password": "&\u001e\u0014􍢴ZⲚn\u000e𦯣󶳚/🄕'􃡾m󶪩\"☬𡷝\u001e(&󳓮\u000ef1Wf'I\u000f𘞾󿫦󼛩\u0011Jq􀠱Y\\Bedu@󷭷c󵇒D쿛􀟶S𣐞\u0003\u0003W>󵜮\u0014\rSO8FXy𨮱a\u0019𩠡\u001aNF𦧁L\u001e$5\u0000k\u001ez*s𤔬𦤜\u000b𪴹\"SY\u0002󲶃􍚚ub5q\u0005󷨛\u000bN.\t𬳰:l􍷴\u001e󺺉\u0007𩁁\u000e\u000bt􌏐W\u0016󾟜􎿛\u0007'v\u0017䀘\u0015\u0002 \u0015\u0002숔,􏙎x󿱴^􄡷櫦I;\u0015b􊧑o𧯋_𮡒MME󹩀\u000f􋨼H;\u000e\u0017s\u000e􄏑{Knlrd;讦\u0014\u000f􆝀TO􊏡󴃗U뺓􌢗t􄺈^y䍴u$\u0011Jp􁙤𨐩𨉞\u0002\"􋛧*\u000e󵌎綦󱻌X􌑜\u0003sK}\u0008𣈮\u00000󱘴12𩱬\tM052𮑯\u00040\u001e󰰚􈴐{ji\u001b󹎀橻&t \u000f\u001by\u0007L𡎯𠇦󲫫\r􁡥ga,\u0014do,tx[I&\u0014h\u0010\u0003\u0010Bpm󴬴-\u0007]/ZI󼎝q]w3n뜿e岌kYo5􊔜'K􊄜}v𣵇;󸮨\\=ꄰ8g\u0010g*has걿󵨦\u0013\u001fYg?I䰆\u0015aW2𤮏m\t}h𥸙RbU\u0002\u0017lz2!\u0013JW5\u001b󺡬U\u000eg,rpOᛡ]0\u001bǟ󵞃F\u000f󿗪\u001e\u000e⺄rl􍦲~\u0006+Mn{5󲧸a\u00192\u000b{jM\u0017T􂔹$\u0011􌣆\u001dj_~Z󵸥P\u0001\u0004o@TJhk\u0004\u0017k:-𗥇[p\u0010\u0011\u001e'\r\u0002Q,,󸐢?H\rh瘑\rj𤈎\u0012\\(u\u001bu𥱑󴳈o\u0014󱕌􍙩􀶂\u0011q\u001d-\u0008齧\u0011qW>\u000cysῂ,'𧃒<", + "handle": "c372iaa_v5onjcck67rlzq4dn5_oxhtx7dpx7v82lp1rhx0e97i26--8r3c6k773bxtlzmkjc20-11_047ydua_o9_5u4sll_fl3ng_0sa.", + "label": "LGz%𝒍j\u000c\u001e/\u0001", + "password": "𝘛𭆴DU󼸸hp󵱻t~\u0012\u0001\u0002*􁈚y1􇑮H𪒧{e\\S\u000e?c_7\t\u0014X𡀓6𪊲E𘝈j\u001a\t\u0016􉯿>HO]60󱭓\u0003\"+w,t􄐸\u0007k(b%u𤺝`>b󻂰e\u0006c𤽡􎠜)し7􈑠`𭟉yO+v%󼗀\rc<𐃤2>8u􋉲􇵝􏸗𒔙a𫯹\u0015=\u0004􆼝8R&j󾣆\u001b\t4sj-󲉛鷔n𡘽􃲙N\u001d\\󺡋𑩠5\r𗫬(P!爳棧\u0008􄫼Mr~﹣\u0019jt>Z\u001d~𢖼A󻲾\u000e\\>\u00116\">%댤􈵏I@u5𭷳\u000brY\r;7􅟌#􇒇󸇞\u0018'󾏵\u0019_I_zY󱂤𤟁\u0019d󽷤cd􃑯𡒆Cp3曕\u001dXj\n듡jy값\t-䩹ꥈd󿠓L\u001aYF\u0006POL헮\u0012\u0011\u0011\u0012*\rH\u0010(?\u0013F擜\u0010\r]􅆋j𩣁 @\u0005T􌮍s\u001cF2\u0015]8\u0007\u0013!\u0015W𫅕􏌲K󺐢􏢞_%󴥚􏯭'􌆥𑋘(#\u0001ky\t\u0017!䒢\u0015\u0014\u001b{𝈕U2LS'", "verification_code": "RcplMOQiGa-JY" } diff --git a/libs/wire-api/test/golden/testObject_Login_user_4.json b/libs/wire-api/test/golden/testObject_Login_user_4.json index 472dbd71aa8..a8f2a3074ac 100644 --- a/libs/wire-api/test/golden/testObject_Login_user_4.json +++ b/libs/wire-api/test/golden/testObject_Login_user_4.json @@ -1,5 +1,6 @@ { - "code": "𗈲m", - "label": ":", - "phone": "+04332691687649" + "email": "some@example", + "label": "\u000e\u0015eC/", + "password": "&󲉊󹴌𔖘\u0002J<-~\u0002>\u000b𒇴𥄿5QN틐𨤨ql\u0015𒈲3}{\u0013𪒺S壓;\t7𬺖_F~D*f􀕔)􄥂-9僛7GK= %\u001e@kOF#𫻩􋌁𞡂8_ꕅ\u001dL鍂\u0003󿶊0Wl1A`LYz\u001fy僸\u001ao\u001b[\u0014\u0008t𐑐a\u0003s~\u001fF𪰤G`$\u000bG\u0011󾿅🙣/󷪺C>\u000f", + "verification_code": "RcplMOQiGa-JY" } diff --git a/libs/wire-api/test/golden/testObject_Login_user_5.json b/libs/wire-api/test/golden/testObject_Login_user_5.json index 7042d152a24..b882ff33bf4 100644 --- a/libs/wire-api/test/golden/testObject_Login_user_5.json +++ b/libs/wire-api/test/golden/testObject_Login_user_5.json @@ -1,6 +1,6 @@ { - "handle": "c372iaa_v5onjcck67rlzq4dn5_oxhtx7dpx7v82lp1rhx0e97i26--8r3c6k773bxtlzmkjc20-11_047ydua_o9_5u4sll_fl3ng_0sa.", - "label": "LGz%𝒍j\u000c\u001e/\u0001", - "password": "𝘛𭆴DU󼸸hp󵱻t~\u0012\u0001\u0002*􁈚y1􇑮H𪒧{e\\S\u000e?c_7\t\u0014X𡀓6𪊲E𘝈j\u001a\t\u0016􉯿>HO]60󱭓\u0003\"+w,t􄐸\u0007k(b%u𤺝`>b󻂰e\u0006c𤽡􎠜)し7􈑠`𭟉yO+v%󼗀\rc<𐃤2>8u􋉲􇵝􏸗𒔙a𫯹\u0015=\u0004􆼝8R&j󾣆\u001b\t4sj-󲉛鷔n𡘽􃲙N\u001d\\󺡋𑩠5\r𗫬(P!爳棧\u0008􄫼Mr~﹣\u0019jt>Z\u001d~𢖼A󻲾\u000e\\>\u00116\">%댤􈵏I@u5𭷳\u000brY\r;7􅟌#􇒇󸇞\u0018'󾏵\u0019_I_zY󱂤𤟁\u0019d󽷤cd􃑯𡒆Cp3曕\u001dXj\n듡jy값\t-䩹ꥈd󿠓L\u001aYF\u0006POL헮\u0012\u0011\u0011\u0012*\rH\u0010(?\u0013F擜\u0010\r]􅆋j𩣁 @\u0005T􌮍s\u001cF2\u0015]8\u0007\u0013!\u0015W𫅕􏌲K󺐢􏢞_%󴥚􏯭'􌆥𑋘(#\u0001ky\t\u0017!䒢\u0015\u0014\u001b{𝈕U2LS'", + "email": "some@example", + "label": null, + "password": "z>􉰃󺎇/𡞯􊳌\u0008%$󽖨𣄄:}\t\u0018􂜙󾺽)㊝󵙼s󵪾\u0018}鱢\u0019[ꅾ\u000bX#VG,df4𢢵8m5딝OTK𣑌鋎꺯◆Z\"ZS\u001bms|[Q%􉲡\u0005W\\󴖙C𭌈+􅕺ဒ䖡v𬁡ꎞ){󻆡𣃒f𭬔}:X-\u00082N\u0019\u001fl🎢쇈Y􅤡󷐛r2.1싸\u0004+𡥙\u0013𣡈]'󻂳s󳤴ꜵ.}𭋣o󲍶X𠜥⬅\r\u001aNq6󸻕'\u000cd\u001e㢧􋾜,:%\t𥅬𒃣QD􉖠\u001b(q4KDQ2zcI\u0010>\u00195󲤼1\u000cBkd\u0013\u0006:F:\u0004𘨥ⶂO N\u001c,N􁚶󴌷[h9ᜬ:xZ=\u000c􈾀\u0013u\u001e\u000ce#\u001a^$lkx耤 \rr\u001aJ󷝦󸓡\u001cR][_5\u0015ⷤ诃5惵􁮵󳗴鉅K!􁁠eRR%絬+h~1󲞮谟lTzS$\u0010􂶳\"*􉕷pmRE\u0013(\u001f^ὯJc➑􅫇i\n+G$|󲫉𦉻g\u001c\u000cgU3Y𝄜\u0006f)􊾺\u0016𓈄􌭞/\u0000Piꩦ{󿸟j􈞅\u001c9𠚽󺊬翉w$눟𞴦)Si𨴄牿FX􂋒j{`궤`󳿗𧁁4u%􅔪P*􂉻捎C\u001eR\u0016-잚󶽕g𐰺:S>c㮢𠝌\u0010Y􄝏~a)YW_J􃢤P\u0007+ U􈷓j\u0019k\u0001􋴘\u0011䣷e𪋘𪳠,ᐏg@\u0012\u001dHXl.\u0017𥣁2\u0013mY􁢫\tv?L8L􆍼N𠦽\nb1j󾸸𤋵xfQ=\\\u0005e󳇪󹽶U\u0012p{\u000e􌚌jd^@U󲯝tP.\u0012Y%R`a\r𧍮7}HnUf𠛸m^7:\u0015=챼>l𗑑hwp27𤦾jE\u000cx=!.\u0013]Ar\tw\u0014&\u001ak㒞s󾦄ᆒI𣪗􂼥dsY\u0010𬚢dX.𣭷i]𤹉󻃀\rWS\u001fU􌏬\u001a시􈨂\u0010\u0002N~-\u000e6𮙏􏄲\\O𭍍Jc􀻇􅢮\u0000HSo\u0010-W\u00136𩥑I􄺨)𘗘={𘗔h洹M󹩪FwJQ􏞨ck\u001a\u0018|UV-\u0015\u0001|\u0014;\u000c𦓫𣦃\u0005S\u0015.B\"D𧲿#o*𞹱胜m\u001e􀓪B3Gg;\u0011\\𬆳􌒮\u0005 B^\u000f𥐶$e餴𩠵>fMgC𭮌,o🗨\\?󼛣~/s\u0001?MMc;D18Ne\u0004\u0018)*\u0002\u001d㾌􀡸l􉔂m'BtB5󴼐,t\"􄕤9(#\u00054\u000fIy>󻯶􌫾\u001dbf\"i\u0017㠟a􉊡C@􇘼􊨩纟\u0015󳻹嬰*N\u0016\u001b:iXibA𡚓𩘤q􀁗]:9r𒁉\u0000􀢋\u001fCN\u001f𤃾􀁹󸐝eR\u001eZbD5!8N\u001bVᲰ\u0006𪐈\u001auz􁓾𭾔~\u001b\u000f%{3I/F抐/DMS\u001f>o𭬿Z􎬞\u001d[K𭇡𗇅􉭱󳀒\u001bO-4\u0018\u001f\u001cZp", - "phone": "+930266260693371", - "verification_code": "RcplMOQiGa-JY" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_7.json b/libs/wire-api/test/golden/testObject_Login_user_7.json deleted file mode 100644 index 8ba7a5ba2aa..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_7.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "email": "BG@⽩c\u000b}\u000fL$_", - "label": "\u000e\u0015eC/", - "password": "&󲉊󹴌𔖘\u0002J<-~\u0002>\u000b𒇴𥄿5QN틐𨤨ql\u0015𒈲3}{\u0013𪒺S壓;\t7𬺖_F~D*f􀕔)􄥂-9僛7GK= %\u001e@kOF#𫻩􋌁𞡂8_ꕅ\u001dL鍂\u0003󿶊0Wl1A`LYz\u001fy僸\u001ao\u001b[\u0014\u0008t𐑐a\u0003s~\u001fF𪰤G`$\u000bG\u0011󾿅🙣/󷪺C>\u000f", - "verification_code": "RcplMOQiGa-JY" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_8.json b/libs/wire-api/test/golden/testObject_Login_user_8.json deleted file mode 100644 index 20658e70ab3..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_8.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "email": "@~^G􆪐\\", - "label": null, - "password": "z>􉰃󺎇/𡞯􊳌\u0008%$󽖨𣄄:}\t\u0018􂜙󾺽)㊝󵙼s󵪾\u0018}鱢\u0019[ꅾ\u000bX#VG,df4𢢵8m5딝OTK𣑌鋎꺯◆Z\"ZS\u001bms|[Q%􉲡\u0005W\\󴖙C𭌈+􅕺ဒ䖡v𬁡ꎞ){󻆡𣃒f𭬔}:X-\u00082N\u0019\u001fl🎢쇈Y􅤡󷐛r2.1싸\u0004+𡥙\u0013𣡈]'󻂳s󳤴ꜵ.}𭋣o󲍶X𠜥⬅\r\u001aNq6󸻕'\u000cd\u001e㢧􋾜,:%\t𥅬𒃣QD􉖠\u001b(q4KDQ2zcI\u0010>\u00195󲤼1\u000cBkd\u0013\u0006:F:\u0004𘨥ⶂO N\u001c,N􁚶󴌷[h9ᜬ:xZ=\u000c􈾀\u0013u\u001e\u000ce#\u001a^$lkx耤 \rr\u001aJ󷝦󸓡\u001cR][_5\u0015ⷤ诃5惵􁮵󳗴鉅K!􁁠eRR%絬+h~1󲞮谟lTzS$\u0010􂶳\"*􉕷pmRE\u0013(\u001f^ὯJc➑􅫇i\n+G$|󲫉𦉻g\u001c\u000cgU3Y𝄜\u0006f)􊾺\u0016𓈄􌭞/\u0000Piꩦ{󿸟j􈞅\u001c9𠚽󺊬翉w$눟𞴦)Si𨴄牿FX􂋒j{`궤`󳿗𧁁4u%􅔪P*􂉻捎C\u001eR\u0016-잚󶽕g𐰺:S>c㮢𠝌\u0010Y􄝏~a)YW_J􃢤P\u0007+ U􈷓j\u0019k\u0001􋴘\u0011䣷e𪋘𪳠,ᐏg@\u0012\u001dHXl.\u0017𥣁2\u0013mY􁢫\tv?L8L􆍼N𠦽\nb1j󾸸𤋵xfQ=\\\u0005e󳇪󹽶U\u0012p{\u000e􌚌jd^@U󲯝tP.\u0012Y%R`a\r𧍮7}HnUf𠛸m^7:\u0015=챼>l𗑑hwp27𤦾jE\u000cx=!.\u0013]Ar\tw\u0014&\u001ak㒞s󾦄ᆒI𣪗􂼥dsY\u0010𬚢dX.𣭷i]𤹉󻃀\rWS\u001fU􌏬\u001a시􈨂\u0010\u0002N~-\u000e6𮙏􏄲\\O𭍍Jc􀻇􅢮\u0000HSo\u0010-W\u00136𩥑I􄺨)𘗘={𘗔h洹M󹩪FwJQ􏞨ck\u001a\u0018|UV-\u0015\u0001|\u0014;\u000c𦓫𣦃\u0005S\u0015.B\"D𧲿#o*𞹱胜m\u001e􀓪B3Gg;\u0011\\𬆳􌒮\u0005 B^\u000f𥐶$e餴𩠵>fMgC𭮌,o🗨\\?󼛣~/s\u0001?MMc;D18Ne\u0004\u0018)*\u0002\u001d㾌1/\t\u0015 󶫒󷘿z苐Bv􎲋(=<\u000eq􍪬?L᪽􄗻ஜc󳤌<&!􍚌󴆏j~O3USw\u0012\u0003\u0007\u0017+󺀡Ny粰(/Sco\u0002{3\u000fEh\u0016󼆏󹫐气-\u001c.'\u0005X𘉸𤮓Ti3􀩲\"%\u0016\u0008𮀜+\u0004\u0002^􎧯)2bR\u0006\u000fJB[󿊻&O9{w{aV\u0005gZ?3z􄈭8፳𦔖󱴵`􃥔\"PE)uKq|w\u00160\u001b. \u0003𑻠sxW𧉥󴚗m\u00057e)𓁘󶑼:s\u0018Yj⚎㿤\u0006\u001flTu􏄥I.􉙜O#kQ\u001e!g􃔗\u0018Q\u001f𪍃\u0016\u0006|\"M\"P\u001f\u0003@ZPq󸌖gY𤒍=\u0007􂍭l8󾌀3󲻄󹪢CN<𤆤gJ󽡢]𗋔mX~\u0006w3\u0010𫸴8\u00076\u0004}\u0010i\u0013L5󼂐PY^|!Vz\u001b4走!iLa⼻\u0014􂭺𩀜\u001d:󾟿𤢈h\\dLx􉣕\u0019𥚚\u001a𠷫R%ps7𗏀s􆓓fg\nIf􄢿\u0011l\u001a󹮗-n_ឱUY?4d]|c\\[T\u0007jS䦖휆鄐aK󺖖􏩠\u0003\u001cx+", - "verification_code": "RcplMOQiGa-JY" -} diff --git a/libs/wire-api/test/golden/testObject_NewPasswordReset_user_1.json b/libs/wire-api/test/golden/testObject_NewPasswordReset_user_1.json index fda243192fe..e04f0b2bba8 100644 --- a/libs/wire-api/test/golden/testObject_NewPasswordReset_user_1.json +++ b/libs/wire-api/test/golden/testObject_NewPasswordReset_user_1.json @@ -1,3 +1,3 @@ { - "email": "󵷑b􌓆9#衿\u0010X2oᨅ𩿅t@􇰸\u0011ꎳs\u000c𘂕}\u0018\u000e\n8\u001az𩛗\n𥫨Zdb#\u001a4IM8𐚙+" + "email": "some@example.com" } diff --git a/libs/wire-api/test/golden/testObject_NewPasswordReset_user_20.json b/libs/wire-api/test/golden/testObject_NewPasswordReset_user_20.json deleted file mode 100644 index 4fa88a2f0ed..00000000000 --- a/libs/wire-api/test/golden/testObject_NewPasswordReset_user_20.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "phone": "+77098859488192" -} diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_1.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_1.json index fdd2f022571..6b023c4d423 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_1.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_1.json @@ -1,6 +1,6 @@ { "description": "eX\u001f\u0008󲾗Ok?🢘\u0006Y\"𣔒\u0010W궙\u000f\u0018!u\\𗬿\u0019.J󲚗Z#A2dEH\n/)󲲹P8\u0001E􃥜􅰦3q虌_🚗e\u001c𦧬\\|7H􁴷됈􂢘󾬷eY􎍻&b豽5\u0017𐰵~Z\u0010􉻘Rkli🞶b\u0001ᯫE\u000cNh𡷦3fFi\t \u001dwdꛄHuDSl-󴊹e4:S,p-\u00034L.$1\u0013\u0017󸔊r􃇢hEr삄yJ#\u0010W\u0018ZY\u0000\u0002$b𡄤\u001c㳯Y?𪄗􍆳\u001d󹃊𠯦𗧒\u0016l􋯻i&k켭CB𫌔ꯗ\n-l姯𩬖Q/\u0001tt𨋝􌽺A􌆋\u001e󽷲j\u0015e􄯫𔗼􎇝\u0015EN楓\u001eǿ﹆\u0017_f𥭦􃒸𐍓{.w󲂢K𫪪<\u0008V\u000fpR/􄚉월\u0004ౌ􈒆A7I􎶔$\\\u0017k\u001c𔐥U󷑖W@8Fċn\u0014R\u0007\u001c𨁱\u001c>\u001c\n\u0002􇹨nI 􁞖V󺪋\u001e𐘏O'`@H띱 m3𤔌+^s\u001fm󵸥葨M𥇪脪\u0002\u001ccP󰸮\u0010𡞰>􀯷F-@V󰠆􊄪\u001b1\u0005w-\u001a,􌣺\u0006􉰡𓅀󵏲Jrl\u0002VX3-􄂗𬜃X4a􏷘V\u001a?=􁋀|𪙜loc\u0001V\u0002n?􂾊)U󴵸􈓴>탔v󴒝8v?⬜l)\u000f\u00073󷓀O𪣭𦪾𣴀U􄹍Dm\u0002.V𭑶𡥷LJ0裒\u001b󾯛\u0007F_􊗰􊾠󾴇0􁛍𦊃𠍞\u0018:OK !jPv\u0008_$s\u001fSC;\u0006\u0000i퀥\u001c􄖬Ze\u0003󰡗LI^1#𑀳\u0012s\u0016.|a\"􊳀{BD\u0015|󼽗l_􍞄󺄙󸾬\u0018\".󹏪D0\\\u0016.ZR\u0001竉\u0004􌥟+𠤥'B􄚜&7LM🢒𣳮\u001b󶎑\u000e6`>\u001c}C~UE𤄡~A8\u0004B􈜏\u0014b\u001c7=\n'𦿣m^z𐢃8[󽛌20\u0019 󱄺󸙁'\u001eu[\\DI\tz󲒆z\u0017Fz󹑩>)領\u0000O\u0001{\u000e󵬔hh􁬀爌b\u0011밲􄂋𨬶68􆚘&u瘫󽩝\u000c&W𣝕P𦳷Z^%{;\\$jh􃇈\r\rll\u0014jI)\u001c\u001a\\󳘻t꼔\u001d𒐲0\u001f0f\u00130Nmzs\u0018\u000c\u001b`\u0010𧯺𧔮^t\u0012r𬺵.f':􎾮l䑤w𩵒󹦯9㛚5􏁴\u001b󾍕@𗎈aU.􃗹\u000b]郻s𨖏Q\u0014􆬛ᑩ𠴟㫿\u001c6\u0018􃷤Q\n𡧂󷷛􎤪if\u0004\u001cT󷔬鵀k\u0006衦\u001a\u001ab5H4~󼴡V𩺒]-󿣢\u0014\u0012`zh􊠎\u0012~􌩰L\u0012h\u0015\u0018𧎽fJ~󴅹^\u001e𢜹{]ms𗟼de{\u0002祫N􇹮H𫶐OP]󹶁𤚷\u0017z󰫈\u0007𨂘4𠏍󰻹\u0017\u001d\"P\u0005𦒍뷕L~Oj\"R50G춂􆾡lhcU􊥔\u001d9𪪵\t{as𣉾\"𐛪v.\u0017\u0001SCD/\u001c\u0013𩐨l􉖹𓈔^𬶮􋅲\u001eo𥏝𡇗$銚T^\u0019\u001d&\u0005Tsl_un󷌗󸧱\u001dMy:pXᜄn\u000f_\u001a0", + "email": "some@example", "name": "󸟵8􌈸:(\u0010󴏏f󵛰\u0007㋗􇷰4KꟄL", "password": "u\u000b6\u0006\u0002\u000c\u001e􃲳\u0016\u0001\u0012|2q⤶󺟂f􃤂J\u0003Mo𬞡 D􉌕|-󲣺e|yfd\u0004\u0013\u0006󺮬t\u000e謱\u001a.SftU󰧸\u0000\u0011<􆍤\u0002'\u0008󳬭\u000eU卒\u001aᠨ|blDM0K8𧖸w\u001e=z\u0015[<\u0003\u0011\u0019𪊆􆋘􊼞\"1x*\u0019𑗔𝟃qo󲟢\u0000o􆍒\u0012On69@󰱛\u0015Zꜵ^y\u001en󻯚󼌤`U󳎎o\u000eE3f\u000eM􋉒C*Q=O𭇁\u0014𠫁/o|\r=z(􀱚\u0010\u0011^6셮?C778D􀷸<~+9𘟵hm𧝪f\\\u0001>B@`*J(\r\ry𡼬\u0011⟴i\u0018-S}􍍓\\𒒟Bh􍭍lY\u0005k>`)\u0019𪐖gZ4􃳌\u001eIAJ􁛰WgoA>텶󱅡󾢬Sw􏲏Ṗ8if\\X=\u001f?d(\u000ft꿁摷\u0014KR􉿽󰙊G\u0015C5\u0000􇆦c[7-kp'𦺝\u000e󽎜ꈗF󼍥e'=\u0004\u0007\u0013Z:q\u0008w7󾸡𠋋{]󻳴Ah|!$\u0007uzh\u0010o􅼹\u000e󹝤𥘼G󹵌\u0010剒1p6\u000eUX\u000eၶkY5(\u001cV𭺮\u0013\u0015􅠾󵷯.JJQg,%c'aq\t5f󷍿驹𥰄\u000c.\u001b{\r󿣰>'O롢\u0011\u0003$R\u0003)󳣀$𪕚Hj7}F\u001c%􏀁􈞩󳲘4𗎶\u000e\u0007𫼛Av󱗥\\\u0002Wq9\u0016W\u0003󲋝C%\u0008\u000b\u0016鍮AzG'4Dj𧙎|+S\u000b\u00029|0$tLe>ol\u0014\u0019􉨃\rmBK橺􆽓bX𮀨Pw+\u001a|e碢B󾉾6v\u001e􆏺\\", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_10.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_10.json index a0a13e470c4..cf3ee530ceb 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_10.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_10.json @@ -1,6 +1,6 @@ { "description": "-uD|3n󸴉+4+@􊦹Jw~𫤕𢾜Q󳯢𗮃r𦆴맹2,𤭁=i\u0008N#y𩡌/8QhH[𪌎􄢋yl\u000es\u0008\u000fW𛈵󻜹𘧄s?`rr\u0010􏱚\u0002j󳁬äq~d}*\u00117\u0011\u0008+^XL🖦0\u0008G􄉠/*\u0016𡦣-!󷊡w@3\u0018\u0002𥞲z8N \u00186`\nr$B\rgmp餫𗕴G\u000f􀃱䳎w􅶖\"𩄧&􆺓􁫠􋌷ﳐkZMV􋛶\u001e𡖽\u000f􊜶\u0011\u0001􍱟𥑇Q\u0012\u0000}󶒤љ𬟴𩇝􏕃\u0008z;􈢍[\u001a-<\u0006'󱮓󳨭􃈗𧳄\u001f{3\u000f~:X\u0008鍑!.V6rNo#^M\u0006a`W^@/􅲵\u0002KP\u0008AAT_\u0019\u001d\u0007md{Pjw𮤣k􃐣\u0003\u0019랑䏈eꡍz5tp𩅷\"#𥫘 \u0018🈠\u0018󼊘z[ 𠆾\u000eAz\u001c:;᮸\tc􂐨\u0018\u001ew⩗􁢱󵩦)K󷇢\u0013&󾱰r:eE~\u001c\u0019ᖽ;\u0018T􃒙𝄿TH\u0002󷷑b8t󽧞^?24Hb=;G󹽉\u0006\u001cж\u0018XGI:|hBb揔:kj\u0013𗔀:0\t`E3t㲚\u0004\u0016P􅸐-iw1z\u0005A\u0010u䁍󱇼𬉱\r3\u0001􍅝O􅛗F\u000c􃬯輎􈽶\u0011𣚗\u0008ybᏇ]t𭑣i󴌂-JD\u0002𦁝I𖤺\u0010\u000b\u001e3\u000e 􆲡􉿋\nh\u0005)n$1F&1\u0007\t\u0019`󱺆􍊛𗏊a#䦟T󻕮𩕘v*i6\u001e-q*VAcXM𥾉q鏛N󽀼<\u001br\u0005&+\u0012$\u001c\u0010hT쟛􏸞i3笧󶵂􈫵\u0017pl􁠀\u000f\u001aR\u001d", - "email": "_e_v\u001aL@\u000b4", + "email": "some@example", "name": "?󱶤웛Ykp*V~z8xa\u0003`6^\u000eT􍥅\u001f􍣫𪷋z󳃤P\u0007n/󹘚{/O麿f󲜇-OG{\u0013伱󺗐\u0010z\u0010\u001f󾭿c\u001e𤀶j𝘨A􎷃􂰭2Wc𝜟󸩰P왮m{\u001c'9Hi􃫊hMB􆦾􀣫Nl&됦8}𢠟⚎'\u0003g\u0005󼧊󵭁%Ps)C𫨺\u001f\"G.󼩀I ", "password": "po!l𩞋\"\u0004𪤚💮z\"mpt\u000ff/,\u001e^z𖢠􄇌Mb>󽡐s\u000f\u0006𢰆󽆪􏃏\u0002𬩷𣷳\u0012m𫪐\u0008>V?􊞯x\u0002􀬰F󳍧-ZT_5󾂿󱾢'uO\u001f뮬􅖁\u0005J􇢜\u000cM\u0018H􋧬YyF/d-)𗎣jS\u0007\u0018F\u0016*}󹁧f􎑳\u00137{X\u001daD훻\u0000n\u0012H\u0010\u0017uu/e", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_11.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_11.json index 5ab91c4ad00..590f6686255 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_11.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_11.json @@ -1,6 +1,6 @@ { "description": "VsX|n𬁵Ue;󶿲󻧙,\u0018]\u001aV%*\u001b6x􃯐\u0007D 󲚝􅌘)俻\u00053\u001d;I𐌌\u001d􅣂i Y𢌔c􅋸𦁦䝠r􎻤V𫼄*\u0010/|\u0011:􁕾𛋕B\tk\u000b\u0004󹓭𤌪.\u0001GA D^9􆷦󼣑󳆶𗤴젛}}6L󻌞FHWcu>({4I]\u0012\r\u0003&|5(KWw넦􍧤󸗠R~󿓘9+F𧮒qG@𧏷X𡜲gn^?$󰡗E嘎𩃄䵜\u0019\u0012 \u0011D󴎣􎰠\u00114O羕#􍴟jC|\u0003^n\r\u001br>Mw󵔂s\u001d祀:󺶿q\u0002e@INe\u0008󼺇􎻄W\u0001𤻑ZU󿐻+틱\u0017\u0016\\\u001a󸷣\u001c𡟜1\u0019䊴􇳚}/7\u000f)`꺵:)%k.u`BJ\u0012\u0001$𨞑\u0018󴼊'9󰆦naҷo六\u001fsC󵺒|󻛮A󹃨𢽣o􃨴#\u0007a􎾗2􈋱󴧁􌨺夹(􁲄|?[\u0001䎲WzQ杺I'𭍉'?𠀟*\"{𞋆\u0014^􆺗Gy㪁qs2z󼖔a<-r\u0013n.<𥐲\u0015F𨊤\u000c\u0014\u000b삎\u0011TR𤟵\r讘", - "email": "\u0007*\u001e_@6󻞻\u0007\u000c#9", + "email": "some@example", "name": "󰁝󴀗-\u0007%~䵖\u0014v4@yim⼁\u001f37䐗\u0002𠧤#@􍘠a}mE\u0013𪌻Es3CꀀTZaTUy𬝀󰀪u\u000e𦷠n\u0004[\u0003𨃁졉_\u0001d\u001a 𐦌\\#𧐀{@\u0011%8s䭟󲗇,!D<𖨤KM\u00115T\n\u0018@sl\u0000*\u0007\u0016xZ\u0006ᤖ ei:V+y\u0015\u000eA䖹dX󱿹󱼬\r􂏥\u0004􍑡", "url": "https://example.com" } diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_12.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_12.json index 11c467b239b..6c55c90dc11 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_12.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_12.json @@ -1,6 +1,6 @@ { "description": "Gt'`-h\u0004\\\u001ci󵣑kꫤEl󼄕?\u0019#󴈫\u000b\\𤜽\u0008/O奅w\u001a0𠽓􇦟h\u000cOs\nP􀴊U0d\u0002􇈶q9\nFᮞ􈑃+@\t\u000f\t(󷧟\u001f󿎢\u0005-3I\u0002V\u0004৲&~\nk kv󿝁9𪥾𦈁x􃪲sC(xNZT\u000b9+u\\⦥\u001eL\u0015L􉹉=\u000f𝣘q^\u0010\u00167%/\u0001jꢤKe𢅉󻣠􁓭5}\u0008\u0019󺴯tDB꤮\u0011􂉟#q1j(Cg硎RG\r\u0007~8\u000b8󸄳𨁸􋫔vOg\u0012;\u0016p굸Ip􍕈+j|􋷖TO&W🛩\u0019=\u0013T眯❳𡸗藺@Dg뗇鏈]m C!𠊡vF\u0001􎠈N&(𬗨苽x(j{\u000eES􉶣\u0003-\u0007P\t묘+ \u001b-\r㤓r􂘰􉲿\u001699\u0007ZG\u001f󺙪L$𡫓{駤𞲩j𨶓bY~i𝞼Ո󰾣􆼣Ev|䂬󾚺\u0016󸃿􏞂\u0001`󱑓Nv󵻱B'?􁓼\u000e\u000f󽁘󺶞\u0008'\u0006s\u0007𮮿ᇇ7'\u0019􄲈\u0003g𣩈󺐈5\u001cgDuX\u0019g}􋗰󻣼\u0008RN󽢹/D\u0011O0?\u0012RoG;=\"Q1m\u0005􍁗\u000f\u0007%<\u000bf艧􊪫\u000f\u0014쳋𧬨N%P􋸈g)=QA]t.􁓴F\u0016;\u00013.wfEL\u0012Ru!\u0005qN\u001f󺽭􆪫\u0012\u0015P@I(#\u0001<}*2𭴢😫r\u000b%E󽁑WlL2x)+󽅗=2;\u0012[\u0012H2)􇯅𔓣th@𧺋\u000b爫\nk\u0010eWF-", - "email": "@d*􀑚jA}", + "email": "some@example", "name": "E\u0013MNO膵\u0004Z󾢒\u0000I⎕5e3", "url": "https://example.com" } diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_13.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_13.json index af210cece22..e0c58e5ba57 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_13.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_13.json @@ -1,6 +1,6 @@ { "description": "W\u0015x;LV𠁿t+#࠷.~󼀷Y+ꊰ\\^(CK@\\\u0002|p\u0015{\\&j^󽫇$P^󷀳􋐗\u0006\u0010󹇸t󹠁=􁛇𧙿dx𠲌ᲢİO\u001bR츴\u0014\u00041GM\u0007S󵫹x$d\"\\jm;\u000fD\u0015h󺣚qd\u0016􂪙G\t𮆤w󺟃\u0002#`6󲹈􃱦$`B𑨎h?5?5!~9\u0008󽶻A􋛟6𪞦\\9(>\u000fq啓\u0018􍸩Kd\u0016𭊭𗱸𪜳)\u0000t0F\u001dF/\u0014*􌯧v\n𡄎%􌉸q󰩥Z㔠V'롤\u0017\u0004Fi\u001e􌀊茳󽏂~𠸵KR\u0010\u000b8\u0006A󿾍b\u001d􅍯E􊼼􆄍\u000e;󾓟I􂨟%~mH\u0001)\u0012,\u0018/9MZ𭤩h\u001e\u0003,{\u0005뇙4뻋Mi7T]B󷨔􎠌]\u000bU&<􃞵=k^𢎩\u0008a?)𗮸8\u001d|\u0007'㗆}@?hL겫QV/?p\u001cd􅻁iv􃕛z鳆77Qj \u000fnhW\tu\u0016%K𩳱]𪑥q\u0016ft􁰸\u0012jS\u00005q𪑚𩸛\u0005h􁹽⺃\u0006鼏1n{A#XQffⱌ\r󻹎3\u001b\u0016@z\u0000\u0003P𧔺ࡉd>|[U􇳄Iံ􎦠𝂍󰖇KyRA㣉+\u001ck妰𨍰\\nQ!􀅕𦊐r4󻹼?\u0008kO\u0010i?JR#V𦝗Zp5\u0015\"\t\t\u0004􈇶\u000bC@󴇞\u000cf􇺆\u0003\u000c㍹诺:\u0004f6\u0013▫푎󻩢󵾞7WnH*怿z𭮥S𗣯\u0019E\u0017\t\u0013TEf\u0013CP\u001f*𗐽`󼻮/\u001ca󻪈\"\u0012󽍐􍯧`𪗻\u001c\u000f\\􄦕$\r\u0010B𔒀󹋉𩏝\u000c\u0004𗇨6~\r􃈌䝅@%􀍬\u0003,I\u0012r@􋍜1뇅_뙧􁾨󴽲𔐉\u001b\u000c%54pb\u0010𐡪\tQ섿)􅂶~󾮰.􋢱`ꁥ􊧑G0󲿦􍂛uj\u0010w\u000e뽱^\u0010,;\\\u0014q󲇟m\u0015!j3t\t􄀽\r\u0017n\u001d)󵽺mT\u0004\u0006T\u0004\u001dh,帨\u0004ᕌ\"\u0013[椁lI𞢂󳤙5\u0002y7z", - "email": "B\u001evq􃫹7@굹", + "email": "some@example", "name": "𣵸敝7\u0012􆯧Y<􄿘+Babu\r𮐴𠘼\u0004Go𨁚:􀾑z􃄑𨻻᧟􊙟tIid𦔶CR+􆯗󼬰\u0013h+Wju\u000f'\u001bc|󾮆?y4L𤃫Z\u001c\nQ*\r慉\u0006an􋯁Sw톘󳘪\u000f\u0001Z䕱I晠Nm\u000f>\u000e\u0005\u0008\u0016\u0002􍪂𪝨\u001e,@+Fm,;(cl", "password": "󷆴󳚁󰁗L􊣬󰱺T𘟲綣F_􆐒\u0000V낓\tHSx)\u0004x󸮍i[㔲*󽆣\u0016t]g󻏭ꚳ;\u0012$U\u001dY)𡦱\u0012a\u0008ey*_垆\u0004󰷚\u0004_\u001c䭭(\u0013􌧽󸧇)`\u0018\tc{*𝜝\u0002󼸆\u0017(\u0016\u000c\u0010\"\u0006𫖽\u0013󱉵{A\u00123\u0008\"k^\u001702𩖚󷦬<\u001e\u001ad\u0016KhꍇL𢗧\u0001􃠽󸞛cG\u001d觚䕦2l􊖱𩠸>ᩗ7g􎙛󾭭HP8t\u0015A[r\u001d$mat-󾽡\u0012\u0003?𩾚M𮧝!󶈁󽹿7\u001b\u00189HB𑒔xL􎅜+\u0004N☟H=\u0016^䟄O𠄃\u0002`n,\u001a~+\u0016\u0018󻜳<\u0019\u0012\u000f>󸔐9\u000e()J[=\u000c?MBFQ^}􎋏,􈝜􊢝\u0007ᒽ\u0011𠰷\u001b 􅄐fL+;𫉭\u001fp\u0005s#፟i􃬕[,\u001b*\n\u0003d󱋁#v6HW *T4h\r\u0010\u001a\u00192􌳥󼿹㚶\u0005lJ~j\u001dd\u0004\n+n𢎍\u00064\u0011(\u0005󴠒󶩬7y+\u0012q\r0\u0018eF\u0016󽗧𡕬Y\u001d]v𨻲\u000bᶍ󼹶cY\u0016\u0002냑R𢅵b#-􄿇[P\u001a1\u001b逕<󴂆\u0006\r\u0018<\u0002􆇨\u000b1\u001akw}\n(􍎾x8N\u0016S\u001c!zw\u0019􊪦/\u0008㉲͐o𫍥󷯂󽌪􋍄\u000bwN\u001c?􈒒7Ml󲊇DSr􈛋w\u0005\u000cM󸰹󼦐$I𢼔𫸳:N\u0018\u001c􄦬{ \u0015\u0013QspHP4F\u0008\u0015ᴣTI\u0012\u000e\u000fp𧈋󰑼2&w𥏖6\u001d8㾀-\t\rA䎐c\re5\u001f\u0001D𥥰k𤶶&O􆤫\u0008L4J)NSkhAY,v[󿟁wPL􎨹a\u0004\u001a\u0003\u001e}\u0007O6\u001c\u0006𧝋TkjQ[2\u001eY#X4j\u0016+󿷞袑O𝅜S%\"g\u0017-􋑪𗻯􋕿䃓\u0001!&􇰣3$\u0001􂼮s0g\u0003\u00132\u0013\u0018kv5𧷄fI𮎜'\u001e5\u00008󸏮]埪\u0014􇓑~Qs}IGki8r\u0004𣓺奦\u001d6C𤪒ꧠ󾁵𦽑ὌDP\u001a+~𤂞|m+Wp𠈄\u001b;N􉅴@\u0019𑚋뛪애11󵪅q󲮿'l\u0013󴾸Q󲮝􉰽sK.\nx\u0003\u0008V􌢉\u0013𬗦q{\u0004W*l\u0006Ox@<𢫿._~\u001ayJ\u0010#Fw_󽢞(i0\u0004o秚 \nHy+\u0017pF&}\u0012O4\u000e@ZDt\r", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_14.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_14.json index 3cbad62f257..48298ed480f 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_14.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_14.json @@ -1,6 +1,6 @@ { "description": "<*n\u0001@磮-\u0002\r\u0014Wr\u000f󶃔\u0013\u001e􄩔|\u001f蠓\u0001E&𥚯󽃸𖣹!L\u0005J\u0006㞤󻮽/>\u000fF}5u\u001f\u001f9\u0014𪦾1\n󻢂􄓒I1V󸇄m\u0005𗬨g\u0005x󳫌\"\u001d􎭈c󻔔\r-/\u000e县Tf9𘗓\u0003a󽦒󷐱q\u001d󾧩\u000f1𭋛wL\u0000󵤪` o^𡾴D\r[!Z\u0019]𪷎\u00011\u000f`_xlwX~󰁍\u00109󻮑l\u0000^V6`𑵕􆣤\u0004D oo5\u0004u\u0010MA󹥡\u0005\u0019i㙌\u001buc󱃄z7t􇌓W𠺚e\u000b)9󿨭f𗥜Fn𨈒\u0011\u0004e𐲔\u0006\u0019(\t𐢀𥠠𑊏\u0017💋Eb􅶎1\u0018D\u0015>mqH\u000f󳶢𗆸:\u00101_l,\r@O?󻫸\u001d\u0017󵝽\u000c𒇌A􉙜)6Y筎􉓽N\n\u001a(^Z/~o\u0000qNdf5\u00158`D[t_s𪾭\nb=*\rQlb\u0019h󻑝O\u0019RKs\u001d\u0012\u000e梖q䚖=J@+\nz􄑙몎U,\u001aq􉘁7>M\u000bY􎺭hꋷT]􏈌[-+蕚Z'qW7%􌖓j6\\u\u0013\u0004`k𬅩!o􊜣\u001c\u000c\u001e𤡉1𬥥5MwmP;%Oԩ]Wp雰󳦸𗮑)\u0014앻󹳢MR󾗰P\u0005'\u0001\u001a-\u0013d􊂮{𭗺'<\u0001\u0018RGw𥵓䝘s,PwtQ~\u000b\u001cN\u0019\u0011@᠇R𦙊@\u001b(J\r<៕\u0007\u0004f󺤮겎~t\u000f􆰭|D()󱍉5j󳴚𬔖\u0018𑘳􆫦*􆴸#[0󶠞\u001c𠏴BCWP󳮺g=t𝇨\u0015󰍀󲒴𩪏V{q}#((𗹡6󸠝?:'7F\u0017㏥b􌊁D+j\u0005\u001eB'=󷅸\u0014􃚑􂖸k􌑞􀎄SB􀈸\u0013:b󳗤둜\u0000\u001b􆿁n?󶸶i𓏸8I趛[%[\u001ev /咖S𡢒𪿛?􏆦\u0018󴵽=w󴤬V󴑂톘\u0008]\u0018\u0015R%􌣀􋢘}􃤣$𭼥\u0015􃾁4\\\u0004|𢙬Ti󿃆\"󵡝󿣽\u0017EX g🎼 ❢X#q2􁚨<\u0010蘀􃬺H𪦠\u001a3W􂯌A𭋀Rv􅚳#𥰉B2\tF*𢤴wibh}+y4c\u0016𦀩`4󿱬o0}􂰵鲼EC$-\u0014b\u0012?CF\nK􆭦􁈶𘖤Y\u0002n7H𐮫p􋫁\u00117󻲿N%\u0013O\u0019{7􌷎h𗗭Pn𧯘\u0007頬_u𧯫洤\u000e𑚄;<", - "email": "%a󽕷@\u000f\"{", + "email": "some@example", "name": "\u00073󻤋􎮭𣚜W\u0010\u0003f\u001aW󾏐\u00126󻾞𪨱^5]b\u001aS󰵑\u000c􌲠|=􇣟\u000cYu\"\u000c꼘V⬼\u001a4S\u0017󰁒􅍞9DD\u0010􆴶􁧱.M0p)󼼈>𢚮.\u0006$z,🍁s󾹻P􁓐gO_p󺌧\u0001󻃦猎𦨕7I\u001ah𮉇qCk7󲷟𬣺𦧩N󺾿󳝭c롖t]|􃃏as􄯙ね􍩡5!w%𬫥*\u0018X\r󶾣R\u0011𡥒\u0007@#󾣛𬮳~\u000f\u001aP\u001e􇙭\\远9s>\u000e##\u001e\u0015󻼌􈲺U⦍g\u000c{\u000ei󰩴\u000bꌾ䒨󳀃\u0003\u001deo@E\u001en\u0010\u001a墠\u0013PZc(𗛧􇨮󲛥V2󶳧z􏡂\u000e愐.􅰤\u0006Y_#`&\u0007\u0016􏴥5^󲮸\u0019]Na\u000f􈃾\u0000%3$o=𠂠\u0006󻞷􂫎쿥%👡\u001a\u0018\u001a􌸿L𗁇Q/S\u001c󳪙<@a\u001c&!6𫚻~􆲇䑜\u000b𣊄𫜄\u0008\u0014\u0019b􋭢1𭹝#d\u001e&\r}𘘥🨆%t𥮙U|D\u0014m𬬾\u0015#yC\u0013amKV ?&pR8&4=󾑅\u000e3󽋒ꯏ*t2g\\0􋖁\t\u0013b\u0011𐬟{z\u0012%Hk􍰘sqUA=&A5Q+\\&𗣥􇵺A􅊛{^\u001f)^5\u001c\u0004W\u001a\u0010󵑴󱣬$T>*\u0010\u000c㌃c𔘙\u0001G\u000bV潄\"LB9\u0008🐳+󳨁7O+柀\n`q-8 iu𦻻/\u0008󹫪\u0014㨺E\u0000?c斝\rK🏫~\u000b𪻥w{𬐇\"M)GU󸕅Y_'\u0012A󸰙􏯣􆊶z}UukH:8J{\\M9}\u001a_O󶯓󴳋󴯊A\"\tKA𠁏hHA\u0003t𬷖\u0013􎷋\u0007󳰤􏙭\u0014󰛜\u000f,0\u0017Ps𧤾P􌯚曜<2Mf𬑂2𘥔MQB\n\u001c\u000f@\u0018\u0001\u0013/\u000e\u001a7z􂓴􋭉9(1Dh󰓭ꌉg\u0016\u001eb0-*U󶡈\u000b󱰨i􉈚𩥘D쑎Qӿ<)𡲵T1F\u0011􋙰$\u0004#Ꮣ8\u001a(Y􋯵\u0010>􈣟쑘𐬕𗣄AC󸇂2𬅱0\r\u0010󿪷{𬝗+\u0000y45󸏙~𑒕Poa倞T\njꇅ\tf󼺍O􌏅\u000c]􅂹\u0003뛓&\r𓇢yF_\u001f\u0010ASe~[=/\u001f \u0004\u0008$魔􏫽g\r6x\u0010P\u001e\u0013疹OO󽽓r-ARf", - "email": "𫘋-\\@", + "email": "some@example", "name": "\u0000A]𭔢Yw\"TOP[f\u0004w󲓊W뀥C=m瑜@s󲾇Z􌿧찯1 𧢓(\u0010树[\u0003V\u0008*𭎳\u0005󳤄", "password": "UL󻼽ཽ󾻖[𬫍ki􍵋ꦼ􃀃dMzz𢁁\u0011\"!Ut!󻇨𠀏󹯋u𤤔!𦎥󺀥K+'!x*e$^4\u0008􅭁􅊝&\"􆊬䠽P\u000fK􆬋𣼗\u0007$\n\u0014'%𥁋\u000f㜯󾸆\u001fq?𧸸N 󲵪NAsu_f䀻\u0011S\u0017J\u0007g(𢳳cee~􁔂Wᯘ\u0016`-B􎦦𗫘Zc\u0016\u0004𪧘.\u001bt䁧g\u0018𗊁\nt\u000bKD\u0001󼠟𘅣w\t(\u001a\u000cd\u001a󺑤J2\u00056;e󾺤c\u0016󴟪&+gU`hp\u0011W󲀝<\u000bq󵢔{􎱍vM󸱛⽃'c𤳐)\u0001뻹'\u0008Xw\u0012y-eil~󸊞s[𐑤􂱯󶁮ꕗ\u001d\u0011\u0011\u0015CWK킷tJu7􈚘\u000c[ꁂ\u001c󲀕&\u0014𫙀𩢐𭵼C\u0004db\u0011T\u0002t\u0008vs\u001dc1=`\u001aKmu:3\u0002􁸂餠RMW)􁡽c$Qk􀊩2\r+𗛝棿EC📭R\u001cm\u000fPG\u0005X\u001dN\u0015[Z3aVL?􋐜|ﭴۛ\u001a{\u000e𫕹󵬽桰p\u001d'sp\u0013𠷬󾗯Y􋒾𨏂KD+X䤪\u001c𣽐vT\u000bO𦒥\u00056KENY\u001aT󳑛5󻞦\u0016绔K`g 𥂆𨀢\u0011\u001dh]*(w0_𫶚=\u00178㊮b󻯕6쒻𢸼P󼨷􊚮ꆝcI\u0010㓅\u0013𬙯忂\u001f𦭮\"P\u00033󲨤c󷰔 \u0007\u0003उz\u0016.^e(Wiy\u0004\u000e$\n泹>􉍀᳕r󾷆)^AE;\u00149𭈂\u0001𝜴\r𢔰$\u0017􅩗\u0006蒪0𤗃6𔗱\u000e\u001f藴\n_\u0005󾊂\u0003\u0014􌫹u\u001d桏Q𥰈H\u0001\u0006\u0007􎈙jiyh=<\u0017)<\u001f$󶤙i𪒐? I\u001ap\u0019f4V𧎤xPﲨM7G2\u0010􃨨􆰊󴌷󱁞JF^󽰗􂦧\u00143\u0019𨍌\u0006\u0007/VMhe䤸Uw\u00026r󼰅𬏛P\u0016>]Z1\u001fe嶣Os*3􏈑􈘲𔘵𤓜􏨊𐂗=󾪯\u001dtDk?𥥇9t\u00108\u000b\u001c\u0010(⟏𣴾NCw\u0014<􍡯C+(|􊯽􃘣&).\u0001\u0007+b\u0008n'`Mk􁃚\u0010熟\t𘀉h?\u0005{/\u000c󸡜9\u0015\u0012@씪󰘪g𘜭4vKVT\u0003\u000f[O\u0004\u000fay\u0008󱂲\\s\u000c􋏿\u0017\u0008􇝎<|P𬑐\u0017=a󴿘\u0000D$󹠾b~$󸴲𥴠r乄\"R󼄐\u0003\u0011秩M*sC3𒈃k\u001eo\u0019,\u0015\n𪫋5\u0017g1!\rd5-󲮗𮓃C󶖙&%zz3🛌􀅳菊:`M5B󵅙ઙ\u0008g\u0017Qg􋓴Y𭁡kZ􁐗\u001b𛊆\"\u000c\n\u001aFJ\u0002\u0008\u0011b=\rE6Nെ𒍎Ns󷅥!J􄩫\u0001\u000bVT\u0002󴙧􎥶`2/啀\u001a󴝘za]\u001d)􅎣l󷿥𑴓q􂂸􂵢dtj󻑪:g镵₠𡵦*U\u0012\u0001u\u001eI􀁻i鞎'.=\u0005W", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_16.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_16.json index 061b6f49e32..1c51dd3be25 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_16.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_16.json @@ -1,6 +1,6 @@ { "description": "X\u001b&%\u000fr󷝶tGpJDG𗦖D\u0003𤠣p<'a\u0004kQus[=􃿘\u0019\u00144b\u0001fꊁxx\u0016􍷁c&Y`῾&𧠻\u001a@<0C\u0008&op\u0012).q\u000cR?L󽣈&􆽉\u0018Q\"k\t𧦰󷃓𪂧bg\u001b󵳠\u001dAK\u0012Y\u0008欯j倅.*WG󴥕K􀲡\u001c\u001fZ#\u0018\u001a𣀧𤠉_=𩰏I􏖍\u0008䌠r^\u001b\u001fA~\u0011\r뵖\u001a\"\u000e}𤉳\u000b\u000b𮤱\u000b󺔇@\n0\u000b\"{e\u0005f𦽈D𘀦􈃼G党󶪩󼦼猦RB䉂\u0014%upl\\0\npL V@󸕃 #ZI𑴤pv\u0012z.+@l;딴󱡷~QQa状\u001a𪍿\nk鶑\u0002󶽹Bw鰻\u0013J𗵗w$\u0018\u0018hJ\u0011􅝦\u0011\u0001⣺*\u0010j\u000e\u0006T󻼡o𖥦󸐍1$rQ?oKn󳑨t&󸻱~\u001d3 R8𪌯𗤋[$(󱐋Z4󽢚a Bp1𢩍t􋚕^/M𣟭H\u0011\u0011lW󳇈\u0004(󿡗)@4􋮕dM󼐁\u0014 0m\u0002IkmV\"\\𥮇M3_\u0001𘖆􆝐􀪈\u0004k^TU\u001b1D\u0011E$\u0013􄒠\u00042''Y􃓲g#$eꃄ<*YU\u001b]\u0012 ],󿨶\u0005~+@\rZ@Uw&IMz󳁬\u001f-\u001e󵹟\u0004ArWv-[?􈑞\u001f𤕵w\u001b\u001c\u0011𫀅󽿪􍛕𭈫\u00175􃲚􈐮lr\u0000ka&Y􇙦;r)U\u0002x\u001b?Uw𨕲\u0011\u001c􆸾􋙝'𑀜𦤣􂓫󴞀\u0016\n-꼇]𧄄\u0015s𭆛<\"[>g\"\u0001\u00029󳍟dY􈞋\u0018녷\u0018􊪨$L\u001aIO}\n}0(5?k𠔑󲢈󻫖\u00034ꠈn\n샼𣵺;􇡔\r\u000bP^u􏸙\u0011#㩦\u001d8󷒸\u001aU󿲾O`cT$o\u0013󺖣𤨻㊤psO.|[#\u0016X[\n\u001bM, 􌵶&y,\u0015,`d,\u0018gMNr\u0000\u001a/౯E{ⱉ9R-\u000e\u0012􇵌E.\n$", - "email": "\u001b\u000eFk2@;", + "email": "some@example", "name": "\u0016\t\u0017S,gN\u0006+mK󲯾>Yz\u0016^jl󱍧󼉪/@Up`\u0004䯎\u0005!󾿈Q\u001dH~?@=b\u0002_xీ\"𪵛cfF􀐩]㐍XP\u0008\u0010\"\u000e􁲕P'Z\rC]󼕬𐬐􄐢#󵌐Cq󷓭s\u0004􉴯=\u0016\u000e󹯹(%𦛜rU_~󴈣\u0016o|v<\r𭖡𫮕", "password": "ScV9\u0008湃\u0011/\u000e􈎅~\u0008@\u000fN\u0004p,\u000cﺵ󸥄O3鞠cu\u000bK𗐂𬁬󴅋Sfi􊿆&􍂑`\u001cG\nx𮊜0\u0018\r⓰>\u0002xC爩\u000f􊌉9󵲄\u0008H\"\u0018IA\u0012R𐆗&𠨻%\u001e󳇹Y㆚]*\u0019f!g\u0005;4􃱔M%􁒋󶗧𨛤h𠁳w󻔼􂵾絖\u001d\u0005~t&y𐇴i\u0008󽚨`ퟺl6􆮝􀽻+☼\tj𝡙;X\u0019)@m􅗾𒄑󻆣2`𥺇󺚑mw=wbEA\u000f&\u001b4wE􃦙SN䒓ZQ􀒨)V\u0015QX\u001ce\t-I\u001eeY{F`h\u0000x􆁑牛\u000b%\u0007p)tXiOV;+'6JKh\"\u0005\\h0\u00170󼺁􅧉K;󼡛3]H[󶱔O2f𫉳􇕍9iOa\u00030]!􆁧\u0017\u001cs>&󸤠[𫢾O𤁦/k\"h􁟿~#'G𐼾魑vmN𣁑\u0003[T&i$Vj㫫\u0015\u0016$􄛶𠿸^\u001chM\u0019\nr{4ੱd􏃲󽵨IG\u0002𗀲", - "email": "lW6/@fD􊻥", + "email": "some@example", "name": "\u0014*43􏰸Xg\u0015\u000e;\u0015J}󾲰wZJ󻍓.=c_\u000c\u0010I𔒨\u0008mU\u000fnT", "password": "\u0008\n6Pq=􀪁(y󹅻y\u0001\u0016􅜝PO4🦮LJ~𫸗\u001dXn Kc'Fn;;S\u0019]𪯎t󹝃Ib􏆢\u001d\u0015R􏳚FhbLz]\u0000\u0001🠼\u0015\u0005󴁧𦠐\u000fg^,\u000b\u001e󹏐𫕰#F\u0006p𐭣g\u0005i\u000e\u001a\u0013W\u0012\n\u000c\u0018\u0015赥wm;iKl\u0018$󺄓\\](*䎸M𦰚𘡲\u0000\u001aw𠃟t𪬤􆬳􂧈/넟$USI-\u0014\u000b%󺒟[궗󳙱*dh&'I{b󺯯)Y󲙂\u001b󿌅A\u0018Lั\u001e.𩅺i􎰇󴩥Tie=} \\(S'T\u0014m\t\u000e\u0010T\u000f\u0004􁱟m𛇖[1𩂛H𐴝痱}&\u001d晊^󺣵3H󶬱}G󱝳\u0016衃󱊻𫊪U󾯫6􊓸\u0017󴙑*b⋂(N􂽔x\u001eS󴌑􎫛]󳳅xL鷤}𨘩\u0008𭲗}𤉰𓌅\u000c\u0017\u00155K죿:𦯁䨰\u000f[􏫺\u001c>v\u001d&\u0016\u0017\"jK󷴃#\u0000㷋?󺤳emU\t3a~󸱑𭱸𥿻*u󳹑f_8􆐏ي󱜄e􇻽𥢷2ᦸ󹧀+c0𬢔R\u0004\u0002闢CW_3𭸚]󸔑&𪏥qU󶯙?7\u0001앯\u0012gc55u儉阛쨬Xt\u0012H𐹼;\u001b󾲠󵏐pP󽪅󰈁𠮤􇨽\u0005N-\u0016)06󿞳󱢾`賦YnY#(技t\u0013&\u0006iu^o5y1若#𑄹=𘏒g)ꚾ𦨔􈉂?􄎺𣬁\u0015浶墠 󶱩!K5􃚵fk\t;󾔲g\u0010l1\u0019\u0014Vgr$", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_18.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_18.json index cf6fe2fe510..ddaeab983a6 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_18.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_18.json @@ -1,6 +1,6 @@ { "description": "L\u0002\u000f\r{𬤮kn횤􏤙VXQ\u001b\u0012𩐆󻡨\u0003熀'􀪻햂𦸓\u001a\u0004󰚾\u0005\u000c\u0005C{@𮋅<[a􃗣gIW𦰆!R􉂊𑴺􋖝]󼀳z䜽𒌑\u000f𝃐N赂\u0014\u001a%1,T&􆉨*; +Lu\u000e󰙔nM󴐀\u0018\u001b\u0016X\u0002a$\u0010acT@P󽺹&𠑄t]k6욞;7Hළ𠑷𠔻MY.\u0007\u0000\u0007󿠾B\u0001Gdb=\u0000\u0010\u0007\nw\u000f\u0015𩸞𡑝Bci*\u0006A}y\u0014\u0008󲚁􁮓Z=󶪰S􍮢Rs(\u0019\u0010\u0014󵰔\"zA;jEcJ𗬦\\􉍮_0v\u000f#P\u0008􁚌x㰒sx󼦴𭷁𣪁𭝣$W􀖴S󵴼􎯳|6-d\u0017\u0007.\u0003􎱌􉫼)\u001cu􌼍.\u0006󶝙^l丫0􃄴q𗕿\u001eY󿌖ag陬Hᷮ󺎒􂷑4e懎aX[#*𤰐=:z냑󶇴􌜼7\u001b\u001bH󰣩dl병􉤍\u0019O𦌺'􈪏󷱆7:]夆x9JLR𫐂\u0001􏒕eGhco𩏾󳩜󳶠𮩭􃤗/]\u0001R\u000b=B𫋴K\u0001@Q%i\u000e\u0010\u0005\u001fzfh󹖑c<(x:_󺞝$\u001e|?_\u000c/𗐈bRvD\u0015󹒓66𫎍jr{ \u001c󾚀h?󰐐spq%&Rxm!i\u0011@󾤌𣥊2d􌒞xn󵖞oU 󱁳🄳f𣇇\u000bZg\u001c2􉟽㡭%T󰮚\u0011y豚\u0000B[\u0014`\u00110\u0002𠨗Q􉶡\u0011􎄿_!G*𑇄𑢶\u0006􍥠R\u0001\u000e1RV󾕌9<2&34b\u0013D2􌎯Ow.l\u001e\u000e\rAr󵹧𡖢\ra)🥊]!a\u0018\\9Vp6󼷽󼗋M\u0010\"}`8V􊽣펀ne\u0017&􃉃q𐌌\u000b咥\u0004Dcꑰ5a4U8󳐝fcm~উC􅯫󰚠4(􂗳Pk\t''M[茫𤗻1𧥶CBSt)k\u0000𠧡􄬻;s湚~8ml\u0014Ʉ󰓸󲑾ik&1}L;D]K𭁚\u0016􌻔m􉈾\r$󵴎𦺭$|𠻰M􉼑􆂫}+=\u0018&􃾖􎀭1𥼗𢚞w\u0017𫙶횳\u000b𨈤􏕓􍑂𭓡\u000bt􇋔7d𦳥e>38R􋟽􀿼\t%i]\u0018\u0006\u000c𑆻^\u0016\u0003t6´𣞸􏸋\u0015fgC7(-tv{mb􋦜=\u001a~}L]\u0000PpH\u001cqW]\nW𠕄\"󲒠\rgr)n'\u0016\u0019\u0004TNk鱟N󶍤Z󿉥􉐾\u0019󺕆𧣝㌉=F@!`\u0003;\u0014F\u00160k|8\u001envY ␏S\n=켻!l\u001bnUC:[HV븱\u000b𠒝\u0012N𧦒\u001czV󼟄W\u001d󶴷𫞍􄅞/G\u0015X𦀰\u0018 bA𬦮n\u0019tN𡂆%%a􈻝8`󹟛{_􁊟𘄾\u0015\u0001Bm\u000ck'Uz\u000bIi\u0019y􍋿x\u0018薱\u0012RmF}Ej\u0018S;s\u001f􍰕-\u0005Gi.p󹫰Y2𝕽\u0003^VUx䠮uA:,T\u000f[y⣳𭲤/\u0007󳔵\u000cz󻺅tq=,S㏕軗-0󷝴i3ld`*󲮭\"\u001c\u001c!\u0019Lt'W=-MW􇏏𦉗暈􌾿&x!Q𢦅\u000b􋥦0IY󹀬\u0001q􊷈0G𩥖h\u001bH \u0012𑋑􄽩62􇱽e󺬟󺷂/_@𥸾\u001a\u0010\u0013~(_\u001e4H\u0003K𑶘}\u000e􇧌Qb􏾉Js\u000c`\u001c􋵙𧣦(\u000cf\u001c", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_19.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_19.json index 3ddd6ec957c..05d93a39a24 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_19.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_19.json @@ -1,6 +1,6 @@ { "description": "0", - "email": "􍣯\u0018Oe\\\u0008@􈊍\u0004􍬇*\u0017(", + "email": "some@example", "name": "\u0013-\u0018\u0003ﳑ\t/㑋T􂙫\r\u0019􇲪XNiu\\LB.+􈬒𠔺\u0012󴬥", "password": "\rAc󾜌􊍗n𫴚N\u000c^&\u001eHl󵻶\u001d(V\u0016柛\u0014!\u0014P󠅶𠈐j𦎢L}D\u0016#u󾪬[\u00066y􎏝d󺍽Qr<󵢺l匙89􉐤.󰯑JDWC4\u0008𗍈\u001eW~t\u0010x\u001a􂓫H\u001aY𠫗ej󷰁\u000c=\\\"z\u0013NFooo󸡂󶬠\u001e;n:<=fq=𧞈\u0002󲕘ZV,<𢂛\\󼏟aL󵔫`\u0017.8􇀌𠔟0󲏯w\u0000*􆬬![𡌐", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_2.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_2.json index 773701d7452..49216e07811 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_2.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_2.json @@ -1,6 +1,6 @@ { "description": "􌠋Ygo%ΰI󵘼`F\u0017YT\u001f-3󲔬沖\u0011G'㧛<艔􁗩&ꤟ\u0015𗅁𒁆\u001b]\u0012\u0015)!\n2K3ႚ[+{\u000bCν󶠜\u0014L𛇬\t􀡜E\u001b]AF\u0018褯􈞃􃥇\u000c\u0000\u0019}w[F䡨\u0000𮈤\u000f2𪣰^z𝜭>\t\u000eY𠁿$4f\u0005e\r􄩇E{􎩉\n[>􎝂󲊋􏩵xa\u000eEG󼠍󷅍usz\u0018􇺊Im\u001c1[$󲍚9/\r즅S1U𥶧qK\u0005,\u0012攠󼆯2󲲦ElX&92𭕖T\t\u0017\tkM5󺹑,󳯚#􄢝~'vmCROC󵙙`q(\u0016q􈧉]<𡒠h\u0018;\u00179산𥢕猁X𠋪eH\u0005󵷟e􌒌\u0010XKDs\u001a\u001a \u001a.ྦྷGZ𞠎@󵱜QDOJ𬰪􄃨e𛃂􀉟\u001b7󲖒瓛L󻚂\u0013h=[F􃚺\u0007fH𡟕f𛉼*AQb]ᅙH𫞨𦸁o\u0013&𪨓4\t)yBn25pNa𬛓Ex>\u0006%x쭄B\u001f&MFo.S􅁅=m&us𠯹j\u0003{\u0012:𠍇\u001e􃣩𦹪󸛑𫂬!u_\u001b?毀1n1\u0017zg􎒦N\u000c𫆇?㹽p𠯰󻺩\u0019\u0014X7vS𦍁󿼙P\u000f'󼦴󿉗Nu=.4M𮯘Wu \u0014}N]a,🡹􈿇\u0015)|ぅ9k􀋳\u000b􄚨𭤐\u0000\u001d𩓜\u001e9\u0018Tl􀋌󳚭\u000c읭O%􂲝+D\u0015\u0014\u001bY󴎑𡺖M\u001fc\u0000Y\u001ex\u0004c):󶄷\u000eJ󿀗p\u000ebS?[KU𮝱Ap_𨂥\u0003󰵳\u0013C\u0007/铆1mEmb\\ʝ\u0018\u0003B󾤢J髴&\u0016Z焭\u001abniY\u0012B6\u0013\u0018vm𘧮lM*𘏂𣽸M\u0011趮펁󺻊\u0003𣇬Ee𦦒󲒂w핽𓅢\u00017#[ck𘟪u0P\nPpN\u0011`*\u0004:=-hn#\u00011u:|쬂/\u0015􂱬>\u0012\"珌i^1ER\u001e󳋮8ѥ𤴖🀶)N𑚂wm궊7\u001a\u000c𠓓\u0018W􏷪1jf\u0002\n󶾯𘤗輐D\u0017x\u001ec#􁽅B~󰎞T*\u000f𞤢󹡭0.r\u00049F{u\u0012", - "email": "S󴈑k\u001e뿋G@T,", + "email": "some@example", "name": "Tx뫫q􄅀@\u001a󺕹\u0007W\u001cG\u0014\u0010$\u0014$`\u000bO?\"Z󿢗􈟛5K", "password": "yx?@/􃻮I\u0010􋤗]L󻐣\u00126C\u0001醫\u001eF0A󶰭\u0011\u0007쩖v𦊍=ul<\u0001\u000e􏖾㝍=}\u000bZ\u00125\u000bZ酘yy]싻\u0017󱫒󹱓\u0017\u0007g!K\u0004~kg\u001c3<\u001d𥸡\u0015󾳍eT󳗖j􌾉j+*`𫚌␋wprI&󴇻H\u0001󱩖Sb\u0014.LaL󿧘􀒝\u0003!x#-􂙯\u001a 􋷢\t\u0001o􂶶xc􂁒y\u0003\u001b𘪤PH𘁨.K\u001f󺤔b\u0014%6\u0000汳\u001a}Q\u0002\r\u0008\u0001󺋶ໞ\u001f􆟛\u0002!xvB4-g)Ꞩ\u0006!GN<󱙶z􌝇􎀚\"𨚔n𣆠wr2lY𪙶9k🨀hL粤/\u001a\u001e􏩚}c󽞆10R\u001f\u0018isO\t㦝jP#ziᑉnaR\u0010\u0018a\u0016d῍􈙑OrW􁯆𔐴𞤟\"𡪠잲\u0011+'\u0008qS\u0006󳨆\u0002󰗈*:󷡃!𩊳J\u0018s{\u0010𓎛#􎸒󲫀u𨆘\rk\u0004ceB\u000f󽥡\"󰾉㗠q𪴆ed􅏤􄚜𗐵~䕝~]⣌'\"vi\u00069{[zG􋞠C^􌱾杞6\u001a𭽅-\u001em\u000b/\u001e𭗹Y:􎅺􌊹r\u0001x􏕬󹤢OR+􅴓㞁𗲬>𬎏:q0'\u0005a󴹠np{\"(􇠌畷뱓#a󵊔𥷃QN$T󱰦?Dcj韖:𦛞#nW}𗶖z\u001a𢼌n$􋮙UP`\u001e?1Z𘓚mx\\峱!*􏫗􌕂\u000f􎿫Qo󲳁󿨸\u0002\u0007|/\\6\u0011𧳩\u00147e􃩱y\u0006T\u0018f&_蹫^sy􍆮𑈝;^\u0014\u0006#l:i Y\u0007|f\u0017.$챶\t\u0011\u0006􉳓p\u0007􌟫\\l\u0010?di\u0015𬜬yr(􊩣O􃉁\u0003Ͳ\u001f𨡾h􁡤\"j\\\u001a}\u0017𬈊\t\u0014敏󾎣|']u󰽀󿵘=8\u0000\n𝇑=\u0013Bkuj]<迧춼\u000f-𣘸t㌘'󶖨D튙&\u001f\u0010T??􎑽\u0017\u0001𩔭\u001f㬨t𬸂\u0004|zUk\u0014󲡼\u0006J_4L\u000b\r2Egy{󵏖D󴐛\u001f졽B𐬂%󾭅\t\u000e􋏦𗕻X\u0015#\"ꨔ\"󻓰W;ar𣌆𧱌!B-[𑢮􊣵󼉸䖢#!|k-a?O􇽑𐑍Y\u0019*l𠥟c(\u0016]䏴\u0003c#󽍄m;􌅂𭤜2X9吻V\u0004b\u000c5&\u001e𧥒h􂳣O,gG鸱睪\u000cJ\u0019󽸟yex􄝣\r\u0003|\u0000>\u001d􃏘g\u000e\u0004Er\u0006i=O&肧𗎴𐔶\"\u0010V\u000f\u001b𗛾]\u000bA2\u0016l𝤻~pn􂽗H*eK󶁶%K4K~\u0017_O􆿿fV%󲫤\u0010󷜁b\u0003~7@'u􀻴𣞢㹡FfsljK~\u0014Igb𭐖𤤡󸣹8Nz􄟨\u0002Dy𝠤󻑝􉟸3pd3]^NBዥG\nG}7\u001b㙝􆫡b]􉘚WM!Hf𑲡e-𬳆\u0011I􇢰􁣥)P\n\u0008GK-Qh5\u000cS\"zA鍳\u0005\u0003\u00178m|qL`\u0003𡲱𨹷:\"*􆥂;hq\u00103b1𑰡\u0003{镓_􌣚9􈣓𑓅\u001ace𥪍\u0016\u001f櫫F詵\n\u0002dJ󲫳c\\4\u0005𝩎𑒪v\u0016\u001c\u0006\u0002𫬢iY􆧒緬4n\t􈞢/𔔭\u0011t(S[𡠸􁷬00`𡛙~󷣫^󰱤\u0002\u0000e􂺙\u001d[𫺴TsJꘑGܱ\n s \u001e\u0018 ^!b?t\rFe푥 82􏵡􋀛jf󶣥\rF

,驕UF􍩪𬤚4\u0018􁴆:R𨢪Jm􉺃𡬊G`7_\u001e\u001dX𦴂9𩳮T󾎺E>@\u000f\u0007@藹\u0018f2'Y􁐧V􆃵U󶿸R\u0000\u0013\u0006u\u0003\r𘩷g󰝮1d󲳴73)\u0002u\u000c\u001a69=\u0007\\!D3𦅍􌇶[U\u0011l#A𠐮@\u0014_\u0017b🖋𡐟ed\\󶁠h%\u001c=U(I#\u0015𗬠\u0015\u001a\u0005%J\u0007\u001b39}j_뾊\u0000눋|h&i{\u0014(\u001e\t󿩂/h옆𖼚}\r=eRH􎚔6\u0007\u0013󿣣T\u0008𢝂jD􎱋&\u0010k𧚛󿤼)𨜝􀼭E𮂷𭟾&K%]X𮝍^R|+\u000fUc\u0015@\u0002\u000cLI􀾋eH", - "email": "@\u0014:", + "email": "some@example", "name": "s𢣟\u001c4,v2\u0010E\u0002F\u0016f\u0016\u000b$🁧󽘝C􂜈􌏫𠶐*2K[􈩦2?\u001d^1` 𧥃6?!􁿝!\\\u0002ァW/䰄􄓓\u001e=\u001fM\u0003~.W", "password": "Wx\u000e\"\u0012\"5v\u000b𤷙\u0010}\u001e󽮔w􌤛:^ꨦ\u0015\\Z􀛝J&\u000c<\"𘏃􊾔&daGH􎀿\u001e󴶛􋿸~\u0010\u0006o󰻅속L􉔴㑈bέ/겠𫱟-#@_Ṏ\n🩬jdIs@r~k𦖺󹞎X\u0012>𭉲􄊚􅬢\"\nN5𬰸-d#67\u0006F^G;5𭖢f󼟦\u001e-g𢑎eg􋞧J(0<&\\𧧏z\u0008󲥄\u001e?Y(\u000f𬁸ORXUf=\u001fxW5=𬑖3󽀚N\u0002!YK󴣄,C󼔟#p󷙪욫r𫵨,<𥰏0󽄛󳖽E𠩝\rh󾤨\u000f(\u001214?pJMN\u0014\u000ed\u0011ᶫ\u0019󵥔📫\u0008𗇑/S𠯱꾲Pd \u0012fj\u001aLm'\u0013i7𢁝)2B󰤐鯿r7tLJyhj􁗄𭈏\u0011𫧎𣁤Z(\u0012􋑺\u000fL|t-󹝙=%\u001f}\u001c苙\u0019\u001e\u0005\r𦠯3𦉜\u0019B耢a;!𣜠'M󴱿k)%0+󹔛oM\u00043K\u0001$\u0002-tR>$`\u001bd\u001f\u0018Z\u0002𢊎RHG󿲣􈡄T-;<{忙K\u000b=7.mP#󰖇}G74\u0017^󳤹N\u000fF*$b\u001d\u000b'J󳾚5j귭𝛔􋈁s[\u0015𩥷\u0005᎙l󳸠\u0001xN𡭻6\u0006\u0005v𮗓>9aR􅺏󲄴T\u001b\u0008-QZ\u0003\u000e󷶶.\u00149]􎲇E𢜷\u001cl롟\u000cCww9Cys\u000f%揈􀍞WY补R3\u0012*Y\u000ba\u0016\u001b\u001d\u0015\r\u000e@ U\u001bG&,>]󾴩\u001e𑒞\u000c*j{𛊶\u0001G􀗐\u0007[m脦𖹅-󷬪󱌨)3qH]璝2 C5Ih$D􊥵[󵀩^@FfꟄ},{󴧁OemlSN􌽠\u000e|wF􂨥\u000eY\u0012𒃼󿑜Kt]\\\u0003BQ󰕬\u001c寁󽳝Db횕󻏾\u000c􅡮钼x󴞗󹰶!uj", - "email": "y\u000e𝟲@ e", + "email": "some@example", "name": "!\u0002OF󽠋􃥰􌞻󺝢𧯜𗲲+|\u001e\u0012NYwPo\\%𬓐vWF뷁\u0014\u001ck􂥴↝\u001eCQv􁣓󵹓b𧪂𥖧(𡽻_뮊\r􇫏􋵃r\u0005􀪔󳦌󺓃A,y%|⤕\u001fR\"󶉙w*􄥡B8󴛄k!{䶩M|e", "password": "~8㠃􃏴ڌ> \u0016q-gd\u001frD\u0003b\u0016Ux󷌬𣦉⊹msB\u001cqe\u000b*,\u0008􍣺\u000bs\u000c\u0004\u0010dh`j\u0011$V^\u0001\u000f𬈮\u001c\u000b\u000e󷨬􂫻\\\"\u0002󱄉hf\u0018s\u0005{\u001c e\u00188K􊳈L𤤵f\u0016sP󵊅oin􏣣\rG\"tXU7S𝦙\u00007C`x􇬼𥝇z\n\u0008\u00017󷌰\\\\\n𥫕\u001aG\u001a\u0015)d%󲥉\u0019 ]7坏H5t^+Sg \\\u0010𗽧\tbI3𩩬E5􈓁夆\\@vp#Y\u0005BA􅤑y9z\u00007🁯\u001ais^&\u001bh\u0017\u0018\u001etlM\n\u0008rT@W;qs󻚝n\u0004f\u0016#ᷠ󴃄))M\u0008`𪰦𥩭K􂢍(ftW\u0013􀕹r\u001b_󾶦􀿮*$2,\u0016(U,\u0019\u0018BFp\u000bXR\u0016\u0012\u0000=3)\u0013j9􂰗s􉵢-k𥔋콮UG󴝠N}\u0004􀜔", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_4.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_4.json index feb6897562c..0f97f5b591b 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_4.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_4.json @@ -1,6 +1,6 @@ { "description": "􁴳󸠝󰛍e\u0018\u0018'Y8}sJO(qR𑋪[\u001d\u0013W󼮜󾏗𢋴\u000eX$},u곀Kv𨣦<\u0012󵚷\u000c􀨌H&UJ𗯜$UJ}\u001b\u0007b!j🗲T󻙜\u0001eWj\u001fii\u000bLc 🦖􄣉7\\𪶹(%\u0001🐉T\u001a󰼳KC𫴝I𤒂#𥱴~ZS~+\t㎐H5\u0006u+𭘻Zꈸ󷰷Yd\"y=G󺪇!\u00048P$[\u00173𫠙𪽌͟|\u0006I􏔗O􌭅)\u0011\u0005뭋󱹱\u001f鱗G\u001eEG<%Q6􃊌T\u001ed\u000fFE)`\u0017􅞿O2\u0008󸄪𮁌I\u001bc?nw􈭹!󸷎T𫹨t𡫖拵\u0006𤙵p𓋞~lu\u0007\u001c礂}\u000eᔷaa􀇛T+\u00162𬭗XRZ\u000fb#\u0018ដXzW􍸢>󻰙􈂐\u000b䘁\u0000]󾼄\"%\u001e🪁ꬶ󸒧q\u0005􊴷𫓷𭨚]S%0 0T<6X\u001a\"i󴺃yBz𩎻\u001a\u001d󵶞󰢸$侂⸌*\\}\u0016\u0007SV𭚊\u0008\\4>GN_K􂽓\u001f󷷬Z~9N𐛋𢒩H#$𠥶&/di!𡑦,ZJ\n=O\ry/嚤\u0011𨦅7nN䆴l?𭫌xp𡿭q󸥻􏹡Ao\u0000N|G󿝕f󷊪󱶻𢌪z\r(;P銨鸢\u001eh?Yo#\u0017Udꯞ󹒴W􉉶a󾈼e^􌰏T𢒛􄛍x𠱘)M\u0008(꼇xJ𐌿􆋷}k􉭙𬧲x􄻘E󴆑3Q 捼\u0000^gd蜡\u0004}D𝒚j󶴦𨭢𩳭\u000c헐o|웋:RW.󸅀\u0000t𪚊󹩃I\u001by2mm災좝]\\R\u001e$\nV\t0&󸜿\u0014􌄎[X^𠳼$2􊨵s\u0015\u0004𝌘qQ遒􀞊𥷾\u0008.龣N􋎒.pSw\u0008WI\\\u0010\u0007%r\u0011\u000c󵳰?J\u000cJD\rLS@{吽6,6𑈗󸐹\u001f\u001d􍇪g󵧺輺b;𗚇U\u0002d\u0003􅢓\u000b<&E)ᄅ`ﲄk𞡯𪃋\u001bu#\u001e\u001b뷥8􊹻*@VJGm󿥮\u0019󳫁\u0014k\u0015I \u001f#_!􂅕􎃜\rc4@𝙽5 W\u001cGFS謂\r\tnGb\u0011󶙵繤\u000f>gCr?*#k8P\t\t\u0002􂵭\u0015w4u𐼺&`F\u001beNk< 0󾙳󲛄P.𡟑jᢨ|{􌑆k􍭯󼸱Ήn\u0008X􇍞\rB􆌫y\r𠒮U􁇕ꮛ@𞢕ki\u0017@𑁄\u001co꽇ZmB𡙲\u00160𒇇𩥌8󲍪8扫󠀮ᘲ\u0019媬\u0015\u0018\u000b\u0007|.nCX󹘍\u000e|4?\u0012t5h𖹨S\u001eu,&m3WL*푧\u001f3抪\u001d\u0007m/$+\u0005VM8$}󱐼\r\u0000􌑀𫖙\\}g󾸡􈷃G􅂼U󲟂P󲦸𭴩\r7\"\u0003讕7\u0002􇑻\u0016L\u000f\u001clf4;󲭋#\t𥷆\u00002:g@'K􊬷𝖢ſtꯣv\u00039𫢏D*Cjlr誵\u001d)𤪲{An3\u00178􀋱eu󻅍B0eO\u0012𣳀\"\u0014𮍚Q\u000cf󱜦󶆶b𫯫\u0019\u0010\u0003Xglf\u0004󶀟D%/dd\u0014𡲚l\\\u0015􋄞Q𧩤:𥌵x!3탵󻹴9㸩0?\u0008􋌕<@卾RQ\u0008\u001b∱W펻x!h𭟚󴊾6\u001a=~𮣝𩫽x𭳠\u0001\u001b𬟚\u0001𛱶碠墱\u001fA𭫃\n\to𢵁\u0012\u0007eX󳴟\t󵂮F~E⢊q8m햜\u0004󴤛(^𗏸H􀹦\rMlD>􉄣󹡈􂴼rC􋒤pfoc𝁫J\u001d𢗻woa-󼛩A/\u0003󻯊𬡯🨕R󵒦oGyj􁐥\u000b\u001f𡛼p\u001b?vw5]󲼕r𬧧􈊁#􌄽)DQ\u0014\u0015Uh`:){x𤓓󽓛/MHDZ􏧂O빆1;tR𮞃􊌧􃆚M􀪉\u0006QC󲕴􈞈", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_5.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_5.json index 7e72d476b51..f791e3d4e02 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_5.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_5.json @@ -1,6 +1,6 @@ { "description": "􏖙^\u0016A%BD󲩓ev}Ia󼏴p쵶+PI~%l􌧁6ZO󶩒EZ𣙰u𪘢X\t\u0014\u0013;걵􍊤c󿆯Nh`ᣚ{13\u000cjQ\u0019M']󿙡e𪎡^􃣿41𘋣𪓨1;\u0006\u001b;鲷\u0016I0*`t\u0008\u0006󶘖Art\u000c𥬶젽𤢖|g􍞅dd\u0001P􏦪\t𧸂x*\u001f\u0012=;𥢓N𧔙􀎢\u001bzH\u0013\\\u0006𓁝𧠚=M𥿭@|鑗\u001fi=\u001c\u000f􉝹􌾯", - "email": "􉐱@", + "email": "some@example", "name": "𩓢\u00124)􄒃\u0005v7\r\u0003p\u0010eI𬾐4SD^𐴃;9*", "password": "𡢗\u0001\u0003'\u0019Qz\u0001%􋴵𦳐p;뭙e\u0006iF􂁴\u001b\u0001gL#󽅕\u0017􎎁\u0006\u0006+\u000f󽔀T⠲r𬑥\u0014_0\u001d0󿷠])'􇢘\u0005'𥡆^n\u0016c;\u001e􋁖KwzB\u001e\u0008𘪃qdQ礈󺮏\u0007-郚h𬈮>~Hi,Ԃ􋍜𡌺R4'a𥣦\u001e}p􁓳󲣟𪻔P?\u0005naw\u001e\u001f󹏤@zr\u0010)𩳎&㈲\u0004\u0017!|T\u0000 '\u001f)cM\u0018\u00054`𨟢F殯=)U6.\u001b\u001c7\n__~􂖫Ruo􃽆󲞺", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_6.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_6.json index b27ff1aca4c..d0c196454fd 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_6.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_6.json @@ -1,6 +1,6 @@ { "description": "5ZK~v{􅋗EG\u0000>\\䧙\u001bb\u000cR󴥂E\u0014󸚡\u0017B6](WU󵈍t\u00003u%%\u001fy<<𬛎9\u001a\u0005RcFvשּׂ;nB?w􁜰\u000e󵹼􋕖|􂐢|l]\u001c󼔎\u0002<󽳽hBlj\u0019K\u000c\u0000*z𩼴葼󻸟󰉝*U󷧶~\u0011-󻒀ࣻ@𧪪􈙪q>𭡋hw\u001f󴃞 P􅥅G󺎠HS󺿮\u001fp󸙲􎄚w碧0􇨨󽜾ya-J3뢰𩮳\u0004J)\u001dki𘤱\u0000D|󲁥s⇴Ue󷥒u𗾇w\u000e\u0018|\u000b.𫀕􈧤\u0013𦱔雾𬰡r\u0013gH𡹡B)\u0000ᶙ,\"㝔n\u0013󼔾}䘡𫰴-V\u001el\u0000r\t\u0016􆓕^O*X\\6t󰅿.󶵥F􌀸b\u0015𨧭z\u0004r􄰼bG.l\u0016𣳂J󼗣𥴩 /4qOZP𡻔Q\u0019\u001d𧩸\u0001\\\u0004j#ZpBZ#󹗷\n\u0001#_XO\u00176?gg󰙲\tX􎭄g\u0010􀕤b󺃨G\u00178\u001b󺴛V u\u0012+h\u0003R􈺈d鳺󰔸Q\u001f\u0000v\t\u0004tj\u001fU\u000c+z𞲎\u001d󾱾F\\@􍘬=𐅪n\u0005\u0019Vr󻓲TW觸\u0012i󽀔\u0008{0XUg🚎\u0003t𓌚yz🁨󱄃\u0007OkJ珼􏠳1\u000bx\u0002~𦑇fQ\u001d􂘼\u0006H;o󷚉[{􁸕S\u0012KㆄA\t~ho9", - "email": "􏏤𤘹􂤒𡠆\u0015@\u0003􂟣\u0005-H", + "email": "some@example", "name": "\nLY-󾏄􁐨`\r\u0002贯`\rBa\u0012jv*m\n\u0012\u001app\u0010\u0004+pq=P󵪀𭌣c~ꜶQp5靅5􀭒X 󲣫*%􂕚&QNA\u000e6䏥e\"K供B\u0003=􋗧𫵘2a􂿜󽁃󿶰\u0006𦾩.\u0001\u000cP慌f\u0017𠭴2h􏝃t𑿡􎴵i󰚯x]cx\nC퉸\u00179;£${󴫱v􂄚#SNKZ@𗗠-\u0013E\u0006{\u001d", "url": "https://example.com" } diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_7.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_7.json index 584b3d68ff4..fa1fd1b7659 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_7.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_7.json @@ -1,6 +1,6 @@ { "description": "ia𩋕\u001a$\u0016忡Km\u0008Fc󱱜\u0010]m\\'\u0017d\n􆏥mm-𪊋L𨧭슗𘓊}\u0015󳨚\ne\n\u0000Wy뛤𩏯\u0016\u001a=P=寣\u0006sbD\u0019fY󵿟p\"R𘔨g󷣉H􋢟𔐥L\n􂡺>\u001bl>Q\u0014BlW𨥴\u0007\u000b\u0007T+'?rI)\u0015󸆜H\u001c\u001ad􂦿\u0001󻐓.A=>\u0007{@GBf𖣧$G2\u0017􉌴:󿳻󱋽v\r\u001eL\u000b^ujRN+b#ꚧ$CulYu7\u000f\u000f꣮R\u0014?󵪍XiDD\u0018\u0019᭼X\u0013\u001bi𥨗r3󹇌\\mZ􊢡󹔒􎶀|eJ;9쁛\u001cb\u0010􅣄\u0018p􄠰7𤏍[j\u0008\u0015)󺞃?X]c𧹺􆚿\u001d膌a\u0006ⶶ .yu󹱈󽦮𣩶􉀛Ê𩳟\u0014M𧱅?Q*pzVXTk/󻠈*\u0000D놂\u0001🎕⣖aD!6]\u000b5x蜬􇜆>n|#\u001e󳅃􍓗\u000eA\u0005㏞\t%4󺏡Ƶ\u001b+-_\u0016\u0016󶒇􎄺\u0005dH𢐴􊟲-:\u000fQx\\󵀈𤟐a\u001c󴀡𠡚eJ􌖀,󶵐\u0019|ட4檊󽹵k;\u0008󽩇6\u0017s~\u000e𐱆\u000ezJ𝀝Dm/ra\u0011S0 \u0011剐\u0019jw\u0013{𩓫\u0008􌥘C\u0002`J7#\u0001-\\]$\u0019𥯏`Z=|􁅴\u001f𨭂\u0007\u0012󵮍[\u0019kX?𐰺%0HOW@Ew𪜚'4𩊡[B𗭘󸌁^m􌸌󻧚tE􅄯Q\u0006𦐘\u0002Z\u0016󱾗M\u0015_R\u0017N\u0016\u00076\u0010:䬚S\\\u000b􈃻-Y7\u000c\n6􅃴hc󷊜&N􂔜􌭔E􏚠\u000bCqHrWV𧢏􃀳𠃋srO􀰺 󺥵/\u0002~𡜞蜞C愫𣝑A6WvF|󳢢MK\u000beS.+)4|\u000c󱉚m<_{\u0005\u000bW]􂠐m\u0003(&𖥺e㊱󱖠\"朇kC裚i󰱿\n/&,7󴭻v\u0019-s\u000e𫯷䁷󽴦몭󲟘_\u0017,_Rk,=Fx_𦯚\u000b􄚈#yℤ\u001e邤\u0000V\r󹑘\u0010\u0019WD󠀻BV󼛝6􆝋ࣘq\u0006圩𧪎欜󵽼\u0008􂕋>'\u0010󴤲", - "email": "e\u0011₋c[i@2CTG\u001f\r", + "email": "some@example", "name": "^􃊤\u0005MSO􃋏0\u0002", "password": "8\\%\u0002넖s\u0019F\u0019v5BC𩓷NL\u0004埚\nTASqz)\u0004kQ􄉽UD\u00060\u000e󴡀􀀫筸󻓽\u0016󾏸🔁\u0001􆖞qM_,𩭇+pa8\u0001J\u00002r\u000f#􎸯-\u0019\u0001^\t剽XuS𦯄\u0007\u001b\u001a_󱊎XPC𐝥\u0017&C73󼪬\u0016|/M2F\u000fS\u0008煱I3{/\u001c>\u0012H5rv5\"?!0𣢃^SC\u0008S5𓈊XP}0VT𩩍\u001cDꑺ`\u0006󸰟f􎣉𠂸Qt`P9~𪢶\u0006\u0017𑿪󼶵K=\u0015RC뇉􏙨􀉄\u001d\rY􏁑e&\u0013d\t率󴃶󻌿W\u0010\t!\u000bex@]xh\u0019o%𬜁􈍇􇑾k夁7/y:􋐦󷑊R𘘯A3㳣􁚔*tnwXQk𡋻􅭓􅢠𨰧𛆋\u001a𤝡\u000e\u001eㄾ\u000cꍭ'!%p\u0007􏠢x\u0001􂎬鵈SP󶈸T𦰓j\u0019$5vA;2Y3\u0010􇙿𐄔`{\u0019S6\u0017\u0016𡾾?󻾘m\u001c螮I𬱃E0䯖<.Gu\u0006\u0006\u0012\" -C_W䛵j\u0013\u0015\u0000C\u0015s欙\n\u001c&G -e㏣\u000bM+pB|𬤠d\r\\,2M󵰱𪧕qA泾\u0008.􀝔gP{5y0󶍁\u0001􃶭d|\\EUG비s𣏃\u0002F\u0016`󽭲OD\n';􆂬$$D􃓶: r\u00114𘣾\u001b\u000b\u0012j+\u0002P\u0016cZF,u􀰳𪽄\u0019\u001b|yAE'蠱\u001301I\\J\u0010\t\u0000\u0006^mn𪑷7/\u000fs撗\u000e4c!u󻹎P(\r-\t\u000f\"賵\u000b2Y;\"h􄿮`?;\u0004\u0003&tyLb(P􊻗2EC\u0006󰂁O+Gt𡝨\u001d􀽟;wwl8\u001czW󶩉\u001e󹶱c𭱤kSB%\u0004nC-\u0019%\u001c󷱍.\u001dy􆎑􄞈􂨨\u0004󿊴𢼠𝅜􋂢i󸖨&7𩊛\u0011󶹾\u0016󹌁.W𮕋9셖a(d􇭏,𢓰xOX􍓃􅵉󸖹𧮤)H󲈸7K𘋙@\u001fM䰮c휭K6xk\u0016\u0015𡋠CH,$⅜y\u001cK􇐰iZ􍚞\u0007\u0002\u0001k\u001ax𐌃􆒚h\u000f\u0007YH􋹘\"舙􁤁$u沞wk\u000f\u00113\u0013\u0004󻔅􋜵>\u0017􏞿D􅭨kHAt%\u0001-AN减\u00070|\nC\u001bLdigb\u0016O\u00154𦛉>\u0015\u0011哀𬑶쿉KE􊝤%𭄽huLv;𮘈𥐘栲ll(}]8#􌽗䂹0(󽳉#𨓕cd`-\u0005𓊑P󼲎[ 􊆺[$脼^􇍑,uKr𦬛!M웥#b𣯢]i𢹼\u0003u\u001d\u0013\u0018bU.pgb􂵂[N\u000f,q\u000bed\te\u001c`<7X'ot󰈺\u000b𦲑KK𥛾g𭄄w󺳵𣝻['!\u001fx/.\u0012ഠJV󿒇uC=C𤎛rا\"𥠛\u001djR*", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_8.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_8.json index a4194a2c1f8..032da5deefe 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_8.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_8.json @@ -1,6 +1,6 @@ { "description": "-\u0008k8Y|'줦F~?󵚡c􃘫@⽎\u0004=ੁ⽼j𠼎>s\u000fE\u001eZ􇐂VI󰖍xZ gz󠇧\u0019{둪Z;𗬰𤢥󿅍nX%h_,G𢱆逪+𢝯pgd󺶝𦹨V:/\u000cq~j(+Oa􌩬\u0011f\u000c\u001c𫒿/\r\u001e=v;􁨽9=7𢀃󳎲\u000cY-𠕓􇶩Z%􄞺𨖳PO꓆􀎭􌋉<𤪶\u000c`\u0017z䵪~𣇊𨌷\u0000\u001b\u000c󰵳K􏻢𦏸h\u0002􌭸\u000ft\u000e\u0019\u0013\u0012]TM\u001a#j俏_fzNi;󻺕e3🦔_b\n󽫏\\hn6Y󿐐+g\u0004\u0019FJ\u0014?󼦜𤧄ꤋ4l𑀬\u001fZ󴴜'Y􏪙\u00148𨾶󳎦=PZQ@􎒊\n󳭿옐󱩅𭁦y󺢏d\u0010e􀻢P𪕻L󴦃:􉮤I뷬<\u001a󿉽\u000c􇛟\u0013|E󽋗\u0002I􄞐3\n콴\u0000􉏵7\tn\u0014\nF󰷶@\u0012\u001d\u000eiB\u0010\u0011W6)𨳔8;\u000cIl\u0004)\u0013𫬃k􈟬_\u00047<=Wx향\"YG#󱫱#|=P􎯬\u000c\u0004\u0017\u0008ok𪜏V􄸡\u00042B]𥣍\u00010e\u0015𧨦鞅;@𫫾R\u001e,NUR􂈁7󾷬\u0010\u00013s,'󼼖𦑔􀧜\u0007\u000e󷜤􌄾58^󰥊\u0014da𨭘\u000e7\u001b(R𐳰x𫃬{㓍𛃬?l#\u0005q\u001dxB􊐗(%j󸳩𗥐\u000c^>q􄎄7鱸􅶓XL\u0004.􎛭\u00074𫮜􌁦\u0010cpr?釞4􃋷􌩤𣮮\u000bS𧁭gZ\u0000\u0001\u0000h(u􇕜`eRZT=Z\u000e𤤘\u0016Ha\u0003􈏘𬃁\u0014x𩑍abpmUb\u0001\u001e𪿍󵛧􇧪RA􍺋󽧷l),𠑩󿍟~0\u0005Na鬟󸪯𦠒護r?}􃡨\u0001\"㚎_9Ge󱌿\u0019BEԥͼ􀁵贻xmP𡱑pg\u001e\u001f\u001a\u000b𪌕虉J𩿱H\u0002ꊖ𐬤\"V傱\u000b`( JAC󶴧\u001f剞m[슅\u0014\u001be\u0004󺰡𑀩/\u0018SX:s>k⏓q-\u0006C $[M픵T^46\u0008󳍄{q􈈢x&G𫥅𘪹𞢎\u0011􎤶aM􁣊N􋿟\u0013\u0018󳆉G%󸴺^7\u0004󷠌(c\\\u0017󺯤𧞏gYb\u0015z+\u000f`\u001a\u0002hT?@旲􎥿\u0000!zX\u0017P􅑉my漯\u0000봹6𧝙Y𠵫\u001cE閤+:󿻏远%\u0010𭾀p\u001dO􌇼\u0010\u001d\u0018\u001d9\u0015E𣵋\u0010i𨑊T~i󻬠V\u0017𣆒H\n\u0010\u0015􃸦\u0015;􎁜\u0005𤋢𤠁\u000c\u001bv\u001eMq59{\u0017🏟2J\u0013.\u0013P^", - "email": "\u001a#\u0019dlb@\u0017󰦍\u0015^", + "email": "some@example", "name": "㩆\u001eh􅩏;Io2:\u0015^e3\u0015𥓍Vq=\u0019􊓵\u0003􆬸􏝇\u0011h5Zqc􈙩vz\u0008V\u0010\u0016Zh-\u000bZ4󶑯5/KI󸕫㦷\u0015\u00139徠:q5Oe􈮣9\u000b:xx'U􎎲\u001at|\u0011O-}\u0001\u0014Zz󹝟z[,F{􆗄\u000bਁ\u001a4\u00100-\r\u0008\u001fw\u000f\u0013a􆔜mv\u0011;鯝W&e|󶜇􇽰;o🔼𢑞\u0001ds眮\\󾽊\u000f.픴J.1", "password": "𬖩2𭽏qfb\u0002訅􋏕𫪬\u0002aC\u001b𒃙𬘐𐅥{\u0018𮘻w􏑆D􁠐q\u0011!N.]bQ\u0003*\u0017\u001f\u0007󿥧q𦥲w𥏽)p\u0006󳊩􆿴𬑒){N5\u000f'Eb㭴\u0016x\u0000􆆶󱘉YB7􋋤co\u0004,[󶶮kDB􇴒qU\tr좺s󽂘Jv*羇)TM1󵨘=䍩\u001f=g\u0002𨂂\u0014\u0004-5\u000cr@f뼘󸬋80\u001f𨳜뻍ns8WB\u000c􊹱#C\t겴_𗝪Fl𥇔\u0008K~\u0006󴍹\u001c𧑒9\u0016.\u001a4R\u001c#𨖵䄈2\u0000\u0006伸\u0004\r\u000f􈶘\\\u000c),퐣kJ𬆸􂗀2B27\to?Gލ+r􉓽o\u0013\u001d(\r\u0006J:`\tZ\u0000Kp\u001exK~,E􂖇\u001338󴖿\u0018󷉶𪳂ynza\u0010g􍳰fB\u000eX\n􁂊􏼫H􆯇󲆀b+󻷈IC$\u0008󶃰𘒍}jc󸥉B\u0005+𭡢\u001f箒\u001a\u001a\u001a\u0003|\u0015󾚃Q􈂢\u001a1󳭩C\u001dA󿾞X_𣜐\u0003T;󷯫\u0008(𣉜\u0001🏝𝛩9no 󷖾v媜b(\u001b48\u0007󺋦wEL𡾏􉸞\u001c6ePOm{\u0011𨆫,&zj\u00069f\u001cd%z􍿟󽖱6\u0010󽮑i,\u0010\u001dy,Tꆜ>z\u0006【\u0004{yUcu剕\u0016𠲦gB녯\u0007t\u0000Ka>𨒕󱵌 𬡽􌸿~\t󸭕􇶎yzRZW󶉐\u0001T󾑫,\u0004Y~F𮚉\u0011\u0002Z󰯾\\8ei$FViM[e2F\u000f􆶕RcZ󾺗\u0015N甔Kr\u0008o\u0008\u000e;\u0002s=𤵑v𮆬L&r꿑3\\𩚠6\u000f뾕惾\n􀏍𢷟𢱉4􄱺YX࠻\u001b󼩼廓𥃥cFE\u000c3􍚕QiPb\t𦅎\u001a\u00079&\u000c󹼳\u0005\u0000\u0002\u000f,M\u0014\u0013~tx0u𐹠c6\n􊮫􂪃WA-⣇S\u0011?󱞀𦲯^𠘬u\u0011|#0I𬔵􏩵\u001da𪤡𑈱r\t%􈛃RkwJ:􈓷^𧵔􅐞iI􏫩f\rx蒎eN𫆕uE𧅶𫉥鎁];毜\u0018\u001c𦃕蔌\rT\u0008𧮱𡚻\u001aS|V􃜉&􂦦\u0010:\rYxL[ረF,c쫇\u0014n>􅘏\u0004;/+􆡞\u001a𧞆\u0015t𤊇또#<\u000eV瑺\u0014$󾉁5󴍹􅦌𨴱eN\u0011\u001e\u0002􍔇\u0015u3~\\𨰛𖣘𪁿(󼐶[𥒈\u000fC<𨁹|Um􋩩e]J\\󼫊]\u0000Ac􎚍#銭󾲗󳒷o\u0004S󼜯餻9\u0007=T\n[WkRO\u0016/􁩥u$`(󶢦\u001bw\u0012\u0006;􉼱B􊶩I[gi󲙙", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_9.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_9.json index 1616f392f48..72ef2a3ddd3 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_9.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_9.json @@ -1,6 +1,6 @@ { "description": "i󷕳gG\u001a[ᰩኬ𐓀WṶ\u0000/$󶖑PU+ā\t;/0􍬟夞U0\u0014ྐྵM2Q}$W􎛘-𨌕+𬅈\u0007M󷎌5\u0015鼉_𥒺󼢀%\u000f\u0011t\u0007𣩪R󺁃4o?V-:H瑢!P\u001d\u0017N,􀺧3\\㾪𣝘\u0001GqKb􋛽􃙕i=t|K󿧏G 9􌥢X󶁯o쵐􉍞)mR\u0013f\u0018\u0002\u001aNU\u0019Af󱲘Q9t𡣖𠹄\u000b􈊞@󶊶􌄲M\u00198S`[e𗘓m6F\u0016G}󹣮K>ᐉ󸯳^ec9\u000fYG\"6mcB4𭖨𢥕ᢷh$\u00055+|&L󸼼\u001aƆAv󼗧\u000bxS9􉂣c`D\"\"b𬫱\u0016ᢠ󽋤\u0013z𤇇Q`Dr􈉴R𗫛h^\u0015>\u0004\u001cM>J;󻒷=n𠥊z|E\u0015􏸦Vl􂓝P>󹳲V𑢸jz\u0003𦭨𪯶qP-h[冴R9󺝞*\u0012\u001a𦢶+\u0016$@𩋊<𫖛K_􉇩T𦀦?_〖Iz𣭛󴧪ꢞp8^>\u0006C욉ၷ?\r$𧽵f􄧧#\u0014I毪'\u0013\u001a𦍈l\u001f5󾉵\u00116o\u001flk󸿖𩅖𧈝y􎖰\u001b\n\u00025\u0011󱎓Ohq/\u001b􅳱S󵉭ewtk􆭍􊿡H眀tnSE5\u0019\u001f@a𫼽d𩿱\u0003@\u0005𖡙\u0010G`󳴃8𘟚𠳨􏪱𥃚?jN𠆝w)4,J\u0013𐧊(\u001c|*\u0017ZNdJ笱fcx\u0012rjH$㳩􎱈R󻿢󶒔\u0008A#=􏡶𦸵T󾰗ty!\u0000l樶H唆OK\u0017[R)𝐫1`𫱘\u001d\n\u001b婍rAnl𡗀3n󷇺\r+𪝠5􆅪%R5tIG/,E􀷔_C\u0012\u0003󱛹r룮\u0008𗺰\u0014𘕁Xy-󾲄h|𥳪\u0004􎖂s\u0016v4\u0003.Q\u000cb,􋛼\u0012C\u0014'讣JFS!I*p檴&\\E\u000eS8ꍇ9𒐤4t𨢫㢵󷭷9v\u001fV\u000f􅩠\u0019\u0018\t앒p\u0017V\u0008\u0006bY𨀔o\u0008𞄈j)Y\n􀈰LE[{\u0016'E: 𮡑󹯃D𬤌~𩒨\u001b\u0011p󺉡J\u0016𮀖[士􇐠\u0006Mg󱞉\u0001xK󻄊C\rK\u001f\u0016-鼅<􃌗P\u0003E<[4􄧔KIXm\u000eV\u0012]Q*r\u0016*\u0001􇉊\u0015\u0000~gA𗢺XB{\u0005N2􊝛뛊p\u0017\u001fn.}OB1\tU\u0019JHB􂽻\u000f𩂩DP\u0012󺭖$󳇴\u0016}윧\u001d𡬨e72𦸦)&ol\u00189I\u0007DOW\u0005i선4\u001ezbB\u0014统L\u0005X􌅶󾊋\u0013d󽭄2󷨞w8􀏙􍫮I\u00002𮥝\u001e𨨸\u0013x뇝(yLL󳢿\u001diFSE8\u0014#\u0001T󸸶\u0013\u001c𪯻\u0015􁬰kf𦖣ߊ\u0017\u0006mh效'Lqo,\u001d(󱾧\u00031@\u0015b\u0007\u001a:\u0018/\u000bA\t𪮋鼻gv􄽅𦳈i󵹩\u000e暑첱V4BEy󵲊\u0016\u0016&󵡱混ak󺄡𧬥$􇐻BV\u001d\u001bnV?$/F䰖閽7.?/K\u0006X\u0006IZ/\u001e𡔶𧪡𥡞x^𪼁;\u000f\u001e𬐳\u0018rwFL\u001b5T\u0010v𪴞\u001fG𨸇x\t ;\u0002h@󵸈~􎭇\u0000;􊗃*Mu\u0017Iy`FI`X汓6#,Hb\u0015~@𭐌\u0017m>눈`H)E𩏯p@~f󲭡\\\u0007\u0016H⣦𦴈\u000f󽥰oJ7󵃅)$\t8pxM􇷒\u0003\u000eﭐ]\u000cn􈴊-?^\u0018Mda/􄏸Ei(􏷀=󹜾𦑧&62.\u0000", - "email": "8-\u0016\\@\u0004옐~#", + "email": "some@example", "name": "h{kᮤ𧥕𘒭.t󶿥\u0011K\u0016>O\u000f\u0001뿐pJ\u0001\nh(􂜊E燙9䜃\u0015\u001cܛ\u000bD󳄵\u000eg6F􁙉qUD톢󻋲hn]f툏$kZg󰗯\t~;󸙈󵶞k󱟔5𧚍r\u001e\u0006[\u001e󸧪𬶝靟\u000733𤀰6􅉩$#{\u001d\u0010\u0004X\u001c=N\u0019𫾳6<0%\u0012cdm𪦑b㟼\u0005=9F2:𬡑~\u0019\u0017묬k!Q𥙾􏊀[\u0000eld", "url": "https://example.com" } diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_1.json b/libs/wire-api/test/golden/testObject_NewTeam_team_1.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_1.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_1.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_10.json b/libs/wire-api/test/golden/testObject_NewTeam_team_10.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_10.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_10.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_11.json b/libs/wire-api/test/golden/testObject_NewTeam_team_11.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_11.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_11.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_12.json b/libs/wire-api/test/golden/testObject_NewTeam_team_12.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_12.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_12.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_13.json b/libs/wire-api/test/golden/testObject_NewTeam_team_13.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_13.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_13.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_14.json b/libs/wire-api/test/golden/testObject_NewTeam_team_14.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_14.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_14.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_15.json b/libs/wire-api/test/golden/testObject_NewTeam_team_15.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_15.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_15.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_16.json b/libs/wire-api/test/golden/testObject_NewTeam_team_16.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_16.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_16.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_17.json b/libs/wire-api/test/golden/testObject_NewTeam_team_17.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_17.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_17.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_18.json b/libs/wire-api/test/golden/testObject_NewTeam_team_18.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_18.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_18.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_19.json b/libs/wire-api/test/golden/testObject_NewTeam_team_19.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_19.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_19.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_2.json b/libs/wire-api/test/golden/testObject_NewTeam_team_2.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_2.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_2.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_20.json b/libs/wire-api/test/golden/testObject_NewTeam_team_20.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_20.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_20.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_3.json b/libs/wire-api/test/golden/testObject_NewTeam_team_3.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_3.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_3.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_4.json b/libs/wire-api/test/golden/testObject_NewTeam_team_4.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_4.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_4.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_5.json b/libs/wire-api/test/golden/testObject_NewTeam_team_5.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_5.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_5.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_6.json b/libs/wire-api/test/golden/testObject_NewTeam_team_6.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_6.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_6.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_7.json b/libs/wire-api/test/golden/testObject_NewTeam_team_7.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_7.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_7.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_8.json b/libs/wire-api/test/golden/testObject_NewTeam_team_8.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_8.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_8.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_9.json b/libs/wire-api/test/golden/testObject_NewTeam_team_9.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_9.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_9.json diff --git a/libs/wire-api/test/golden/testObject_NewUserPublic_user_1.json b/libs/wire-api/test/golden/testObject_NewUserPublic_user_1.json index a22cdbd6852..8d5f51fcbba 100644 --- a/libs/wire-api/test/golden/testObject_NewUserPublic_user_1.json +++ b/libs/wire-api/test/golden/testObject_NewUserPublic_user_1.json @@ -16,7 +16,7 @@ "type": "image" } ], - "email": "test@example.com", + "email": "some@example", "email_code": "cfTQLlhl6H6sYloQXsghILggxWoGhM2WGbxjzm0=", "label": ">>Mp१𤘇9:󺰽􋼒\u0010D1j󾮢􂊠;􄆇󳸪f#]", "locale": "so", diff --git a/libs/wire-api/test/golden/testObject_NewUser_user_1.json b/libs/wire-api/test/golden/testObject_NewUser_user_1.json index 211528ba5f3..54619703ce6 100644 --- a/libs/wire-api/test/golden/testObject_NewUser_user_1.json +++ b/libs/wire-api/test/golden/testObject_NewUser_user_1.json @@ -16,7 +16,7 @@ "type": "image" } ], - "email": "S\u0005X􆷳$\u0002\"􏇫e󷾤惿󻼜L\u0017@P.b", + "email": "some@example", "email_code": "1YgaHo0=", "invitation_code": "DhBvokHtVbWSKbWi0_IATMGH3P8DLEOw5YIcYg==", "label": "𭤐15XwT󲆬: \u0011Z+\ty𗌉\u0001", diff --git a/libs/wire-api/test/golden/testObject_NewUser_user_7.json b/libs/wire-api/test/golden/testObject_NewUser_user_7.json index 4e1c9e40a64..6eca6b1916a 100644 --- a/libs/wire-api/test/golden/testObject_NewUser_user_7.json +++ b/libs/wire-api/test/golden/testObject_NewUser_user_7.json @@ -1,6 +1,6 @@ { "assets": [], - "email": "12345678@example.com", + "email": "some@example", "name": "test name", "password": "12345678", "team": { diff --git a/libs/wire-api/test/golden/testObject_NewUser_user_8.json b/libs/wire-api/test/golden/testObject_NewUser_user_8.json index ca19edb493a..017389cbaf6 100644 --- a/libs/wire-api/test/golden/testObject_NewUser_user_8.json +++ b/libs/wire-api/test/golden/testObject_NewUser_user_8.json @@ -1,6 +1,6 @@ { "assets": [], - "email": "S\u0005X􆷳$\u0002\"􏇫e󷾤惿󻼜L\u0017@P.b", + "email": "some@example", "name": "test name", "password": "12345678", "team_code": "RUne0vse27qsm5jxGmL0xQaeuEOqcqr65rU=" diff --git a/libs/wire-api/test/golden/testObject_NewUser_user_9.json b/libs/wire-api/test/golden/testObject_NewUser_user_9.json deleted file mode 100644 index 975b72224b9..00000000000 --- a/libs/wire-api/test/golden/testObject_NewUser_user_9.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "accent_id": -7404, - "assets": [ - { - "key": "3-5-5cd81cc4-c643-4e9c-849c-c596a88c27fd", - "size": "preview", - "type": "image" - }, - { - "key": "3-5-5cd81cc4-c643-4e9c-849c-c596a88c27fd", - "size": "complete", - "type": "image" - }, - { - "key": "3-5-5cd81cc4-c643-4e9c-849c-c596a88c27fd", - "type": "image" - } - ], - "email": "S\u0005X􆷳$\u0002\"􏇫e󷾤惿󻼜L\u0017@P.b", - "email_code": "1YgaHo0=", - "invitation_code": "DhBvokHtVbWSKbWi0_IATMGH3P8DLEOw5YIcYg==", - "label": "𭤐15XwT󲆬: \u0011Z+\ty𗌉\u0001", - "locale": "sn-LY", - "managed_by": "wire", - "name": "V~㛘钟\u0000w􍻃􇅡1𑵼󹄧%㥫]y*𝧑jqM\u0016𒈔/􎨑-*\u001f \u001eA\u000e}ﭛcv [󹦺t󷇵R𬌻Y󽃈6tg\u0016󿅷+\u0010𘚈;\u0006Oj\u0013뷑&aD:\nf󴯋!*", - "password": "𣉙%5T5'䎆᳦\u0005-􃭧𘨛7W@)!$%v{\u000c\n_I6􉱮츜]r􍶔\u0002Gi_L\u0005@tr<讃2Dr䂇\\\u000b8쁽\u0014􅈿e\u0008𮞲𑚜srN蜨旗Qk+赥󳼩O\\c6󼉭X󺩽􆓖VV\\󴀯^􍺔\u0014(P~y\u000f(\nrO󽖎U=$󽩻k󷀘7.\u0015[dn􃊾粷_\u0000󳞑\u000bNVd햲z󻓕pV6\u001e𨭗#/m􄊮w\u0015沐u𣎯\u000fs\u0011𡔱^A𗔌>\u001a#\u0019sC!3#`𧂅q𐅄\\VrnT\u0010\u0016􂹙\u0014\u0002𦍺󵅅\u0012d 󻆃#\u0018𫺦/k㤣X\"I\u000fO,`GU+\u0011\"\n럲n)\u001b􂰕x󸨾􋽯%\u0012\u000fVr\u000c󾾡H`🚇W\u001c\u0015􀛞vii\u001c\u0007\u0005󵙼&d\u001d𣶇󲅊.􊈄j󶈟$=a_s\u0010Q󹇪\u000e\u000c\u0003󸽌B\u0005\u0018L\u0002_ZX\u0015 h_sGj)󿬂|\u0000\u000f\rlUN)\u0006\u0011`8\u000c󸫲󳼍\u0008,A\u0011\tt/0lT􅪡\u0007}\u0016j\u000f\u0007z|\u0005𥕰J,26󹰅\u00039⮫0\u0019w'\u0000O&g\u001fF0󴞭kg\u0002\u0011|Q􀁨\u001aM𠌸󽣾vuPgVp𬆇)/Xi􇔬\u0001\u0011:󽌤𬀶𨥔\u001a[\u0018.+uOgWp" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_12.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_12.json index 6842effbc48..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_12.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_12.json @@ -1,3 +1,3 @@ { - "email": "􄵱𩆢🙖>@􄆤{]%\t\u0013n󱅶􆎎􃯵CD􊤽>󼓞a롿⿂𬩔n\u001b\"Xw$\u0007G" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_13.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_13.json index b28663d545d..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_13.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_13.json @@ -1,3 +1,3 @@ { - "email": "󲶌5\u0006𠓫!􉄃\nVb󺴝nU&󽋡u𩟰@+I𫅗q􃾘\u0016􅊹#A𧿃\u0010}.\u0001u󷴓" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_14.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_14.json index 47cf254a683..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_14.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_14.json @@ -1,3 +1,3 @@ { - "email": "v@􊌉" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_15.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_15.json index 1cea568c611..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_15.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_15.json @@ -1,3 +1,3 @@ { - "email": "+𤳡~􆅘VFc\u001e􍐴R\u0007\u001b4J_􉚂I\u000c󾵯Dj\u0011\u0004q@󹃹𡰨n􃙋Gh?\u000bPXOO\u000b􊱳\u0012" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_16.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_16.json index da903c0e028..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_16.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_16.json @@ -1,3 +1,3 @@ { - "email": "]􏖌Dn\u0008\u0015\n䔟𨲌\u0005󺃬2\r􅃁󴯹󽦀@%L(\u0019􎼖\u0002k\u0004o𩯑B䣟O*/+@볡" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_18.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_18.json index 54914a375df..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_18.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_18.json @@ -1,3 +1,3 @@ { - "email": "\u001c󹮂󷆕^3𐭏*(󽗶𘕇@󽓵Y\u001b|=𡧿E.A.\u0000󴭝K>􄠭cZZ~\u0018􂟺i\u0010.rꡇ󴪩 𫍒" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_19.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_19.json index d21c352e333..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_19.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_19.json @@ -1,3 +1,3 @@ { - "email": "x|􌸆J8󵹛|%𢷎'9􉺫𩿺􉀀F􌌯xyP􁟃 4,@!]w6:\u0001d4t(􍠌􁂡$\u0001rl9⛉𝝥t8" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_2.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_2.json index 4a7cac622ea..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_2.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_2.json @@ -1,3 +1,3 @@ { - "email": "mh\u0011􏧜\u0007􏝯#e󲴔m𞀌𮓹𧛢D]@\u001dJ-0𞅀DU~Ẕ􉺓\u0015F$" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_20.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_20.json index 7c18619be65..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_20.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_20.json @@ -1,3 +1,3 @@ { - "email": "魳2\u0016)=Xd𥸩}o@4\u001a𮂬􁙭g\u0000􊫓󰗸Q`\\\u000eU󸝠" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_3.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_3.json index 0904742ebb1..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_3.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_3.json @@ -1,3 +1,3 @@ { - "email": "-BP\u0018󵷒F䰯􌭱]W󽲘d𡹕􇈞\u0006\u001d𡳖Dy\u001bx\u0018𦯓uOU󱄌\u0018􆩶\u0006@󸷩e\u0011V-j" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_4.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_4.json index 44b7a3501a7..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_4.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_4.json @@ -1,3 +1,3 @@ { - "email": "\u0003!\u0014]$Zp@R\u0002\u0010Q" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_5.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_5.json index 7cca99e7318..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_5.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_5.json @@ -1,3 +1,3 @@ { - "email": "󶆁Mm𣢰\u001c9` 𣫱󲫷\u0010𤲜\u0003꾡]󿶏歲2\u000f\u0017뀙@B󱩠{\n􀎙O\u0004,P" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_6.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_6.json index f26d931ed4b..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_6.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_6.json @@ -1,3 +1,3 @@ { - "email": "ᒱ􂬛𥒗\u0003Lv 󽎁9@3J\"K'-Q𠂊P𗗖Q\rf􇇓6_kN􆉆\u0003$󳅍󹻌4𬐬k𠯣󹰶k" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_7.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_7.json index 00efb9b2268..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_7.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_7.json @@ -1,3 +1,3 @@ { - "email": "\u0001\u0003rra\u00014|]c&4%#Al\u0012*U\u0002𔐧m9\u0001󰧏UQꏘ󿤬@1G*𦂸f\u0018V󳒭㰒𗿫lR쥩" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_8.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_8.json index 67b2e07f780..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_8.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_8.json @@ -1,3 +1,3 @@ { - "email": "6􃨣C酵(|\u0000\u001e𠡓@襄\u0019饲" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_9.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_9.json index cc41c9ba8d6..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_9.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_9.json @@ -1,3 +1,3 @@ { - "email": "ui0^p󸘴\u0003󲶬u<8\"YgWb\u0008x[\u001e},W\u000b󾮟耠\u0016@\u0008*󻥹0>*N`𠲧\u0013 t" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_Presence_1.json b/libs/wire-api/test/golden/testObject_Presence_1.json new file mode 100644 index 00000000000..184b9f0809b --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Presence_1.json @@ -0,0 +1,7 @@ +{ + "client_id": null, + "created_at": 0, + "device_id": "wef", + "resource": "http://example.com/", + "user_id": "174ccaea-7f26-11ef-86cc-27bb6bf3b319" +} diff --git a/libs/wire-api/test/golden/testObject_Presence_2.json b/libs/wire-api/test/golden/testObject_Presence_2.json new file mode 100644 index 00000000000..be0b1e54ebb --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Presence_2.json @@ -0,0 +1,7 @@ +{ + "client_id": "1", + "created_at": 12323, + "device_id": "wef3", + "resource": "http://example.com/3", + "user_id": "174ccaea-7f26-11ef-86cc-37bb6bf3b319" +} diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_1.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_1.json index 46df516a4a8..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_1.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_1.json @@ -1,3 +1,3 @@ { - "email": "䀕𣚼,kyz-\rᬙ󿰠[􌖨#mh>6@c\u0004;QAjc\u00042O\u000e%\u0005-󵄅\u001an$󶢴󰭵b" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_10.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_10.json index f37647d922e..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_10.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_10.json @@ -1,3 +1,3 @@ { - "email": "䂨㢃z-\n\"T𫉦󴡈𑊒f\u0016u`\u000f󰞐35𭛯𭫨/[3ᚫm@{~3J\u0005\u0005\u0010(\u0004Y\u000b:l" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_11.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_11.json index b0c9a2bb9e0..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_11.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_11.json @@ -1,3 +1,3 @@ { - "email": "h\u001a\u0006󼯑倀C\u000e\u0003!𗣦\"󵌵pWN𮬒E\u0011EGZ$T\u001a@󲚨^[\u0019" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_12.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_12.json index 58adaabfac7..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_12.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_12.json @@ -1,3 +1,3 @@ { - "email": "{TC\u001a\u0005?\u0007u􄶍E󻂧@ꉥyMb\u0019\u0006|-eH(\u001d\u0004|B~㳊\u001b󰰢j\u001b=\u0008cs" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_13.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_13.json index 8ebdc92778f..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_13.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_13.json @@ -1,3 +1,3 @@ { - "email": "iA􀍭󽂴Y\ni󴭬WCU𪪞I\u0001+:f@./" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_14.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_14.json index 4899a48978c..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_14.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_14.json @@ -1,3 +1,3 @@ { - "email": "z𨁰xGh\u000c\u0017󸻵D\u00034\u0015S@5-)鯡C\u001aO􄒙-\u001e\u0011}%\u0002󿠐𤍷𫍶𠟛\u001c𪖹Tﮍ㾏" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_15.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_15.json index ae1bddc4161..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_15.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_15.json @@ -1,3 +1,3 @@ { - "email": "𑂵켱ᐘ_(\u0015|􃓾@􍰗\u0007nt𪒫\u0000\u001cS𬪊󹅥-\u0003􌍴K}q􄸀O8" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_16.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_16.json index 727f4266a6b..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_16.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_16.json @@ -1,3 +1,3 @@ { - "email": "@" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_17.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_17.json index 21ed146bcaa..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_17.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_17.json @@ -1,3 +1,3 @@ { - "email": "0\u0015􄵿3@󲛼1\u000e𦂋Z穰𓍓|\u0012fA%:\u0011D􂵹" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_18.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_18.json index d6e1f452e1e..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_18.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_18.json @@ -1,3 +1,3 @@ { - "email": "x:鉩𢏘m\u0015󹔵gJ_\n{_.b\t\u0004<𘝙lB0\u001e촹(@7/k\u0001𬁟쌐\u0006u:󷝍b~\u0010^\u0001􈛪" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_19.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_19.json index 9363d0872cb..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_19.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_19.json @@ -1,3 +1,3 @@ { - "email": "󺁩\u0000𞸤}􃼣󸞢K8𭹾$驒@L" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_2.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_2.json index 8908a1ad5be..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_2.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_2.json @@ -1,3 +1,3 @@ { - "email": "􉽅\"K1\u0003;}\"n~X𠹸𧘖Fd\u001c1^fo}M􁌬q\u000b=𬲈xU@C\\" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_20.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_20.json index b079e812221..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_20.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_20.json @@ -1,3 +1,3 @@ { - "email": "2h/\u000f,\u001cl\u0012\u000f\u0000)@cu􋈜-\u0002iW\r" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_3.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_3.json index f18b3365a87..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_3.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_3.json @@ -1,3 +1,3 @@ { - "email": "J톩uv\u0004\u0016\u0000:nO𬄓YF\u001ao>H𩉘0&Q\u0000󷏯\u001cU􆳳犂\u000fᚄ􌍣Kf" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_4.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_4.json index 90eb35f710b..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_4.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_4.json @@ -1,3 +1,3 @@ { - "email": "b󺂈w\u0007f\u0017􀠥\u0015a􃔣\u00158@a" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_5.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_5.json index a444b3baa15..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_5.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_5.json @@ -1,3 +1,3 @@ { - "email": "C俵󰮩E\u0000'U󶖩@m􏑯\u001aGⱽ\r=P\u0015~􇜄%t㬸H󴥛􋤚Rn\u001a㓙亿󰛷[&" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_6.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_6.json index b3c4728a8b0..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_6.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_6.json @@ -1,3 +1,3 @@ { - "email": "{\u001b'໑DC\r󿣀|m4Z=|\u0001𥘡0\u0010\u001e􋍞󷼟OP\"𗼔𘀕@􈧎\u000b\\5" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_7.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_7.json index 2e474032e11..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_7.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_7.json @@ -1,3 +1,3 @@ { - "email": "bl𧧖u󼼂g,}𐢍63@7󷈃!I" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_8.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_8.json index 8039cca5390..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_8.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_8.json @@ -1,3 +1,3 @@ { - "email": "_𒀇:󱸧F$'Q3\\󳛐@MGx$\u0003w)8C+𫷨\u000e\u0000" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_9.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_9.json index 0f6bb5d78b7..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_9.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_9.json @@ -1,3 +1,3 @@ { - "email": "~랹5QI3[$\u001c@Z\u0015\u001eওkv\u001c󹑣q𗸚\\𑐉􌃠" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_1.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_1.json index ac6d7124f22..ba470460278 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_1.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_1.json @@ -1,4 +1,4 @@ { - "email": "亴𬇛􃏗䷡Z\u0004𦙕􁤃𢟥@􍫠E􅰤Q|𤏶", + "email": "some@example", "password": "\u0011瓲􍀏𘡸\u001e>XJ#\rA[􃰿\u00156r𝈑A\u0013\u0013𐃝󵂧p/􈉠\twP􅫏\u001eCq[\u0014(󴳔P]OL𮫮\u0000\u000f?\u0018\u001c􄭍O?*𠤑\"~𗔚\u00155\u00123󲜈􃝋GC \u0007:T􉒾|$r󺃢Q*US𝍲ἥ󱸃􊲨󷎧2\u000fXp\u0011l,{퉇$􊣮\u0016w\u001e󷤲p𧀦$6󴸕䍤\u0016𫤽7􋔴]𢤵\u000fi􆂾%\n\u000e9i\u0011\u0017I#\u0003\u0006z\"LJ+\u000c\u0004U\u000c!nSGq󾓪􇠪\u0008\n\u0016􌩲􎓑vkoE\\>L􅪃󲚽2㥆V\u001ffCJJ󾙊\u000c𬮓\u0011𢟊鑐S=􎒊v󲴼/jdg󴶕U󺖈s\tO\u001bD\u0012\"􂭈;𧯦`C#\u0012􆊊^j^q𠪂;`󾺒󽗻\u0010󰴖0Q󽮕𮄡𡍖\u0018􌂩99\u000b𠜾􇰽\"󸁗􌷔𗲩JW\u0013n󿸖\u001a'\u0017!\u0007;\u0018F󶌘:\u000f\u0018DNhu\u0017vWC]i󹹹]>mM𗥨󱫋ISK+𗺅\u0001m\"h8硳\u0002\u00143F/A𢮍󴬮𘆬M/궮󾔠`\u0005􁑑囶k\u001c+\t󶱅\u0011,6\u0007𐳔D󿔘.:$'P儍󸯥􈚱𨰲w6V\"I~\u000c`☒N2\u0013\u000e\u0011S􏝽/흝ZfjtU\u001d󹩲u%\u0017[k\u0016󽤶s+G\u0019h6\u0017k󾚒\u0002\u0015\u0001i󺆮P󲲘\u0011󳺉O\u0007\r;;𑲢󱎴\u0011y\u0012\t`\u0003{n\u0018#fb𦇂z􉿹@𤑮-𠸩\u0010_[)' r\u000eh\u001b󵡾4\u000fi\t𪑿􃠀\u000fEe𛱁y\u0001\u001c旺U\u0010Y\u0017[Wu𤊬󾲐\u0007􎪗d潑\u0008􄞕\\&F2F=L\rlk\u0015􂸍x꽞\n􀝠󸓆s\t󲛓]T\\󸏠􍞎𫕭z=^\u00062)Y]E\u0000@\u000f\u000e~ 뮀k7􍐞{m@󹸼}D*i\t|󵸞\u000bF􅗉+\u001a(qIOhI#\u0018󶡀󶔄7E𣗆/\u000fM_z󻌬ॠ⸢ڛ\u0015\n\u001f\u0016^)\u001aLVo\u001e쑃𫡯w⽞UeQi\u0006〴𗸎&캐3S$$𫮶\u001e6􇂜􄩚*𠈵Kkk&Ie\u0000{\u0018@}𐌛L𖬮𥈣L-\r6U$3wC 󲨝󶣠yf3􉈰u𣗯}}􍑟9\u000eX􍙍t󺷬{\u0012m􅲡o\u001cEzaDAZ忖T\t1\u0010鏦𣢗\u0000𒔺\u0014D|t0'W\u000f,l\u0007YI\u000e󶽙𬤱l,\u0001󸤙k\u0007B􀃶\u0016n󲣋\u0007EjoDc🍝EhKM|𦶥R𐳽\t\u0005\u0011q\u000f\u0016C3󶸟\u0019Uy\u000bB𠞅a蔜!8\u001b\u001d\u0000w黈xEV\u0005y\u00136􎎄䠋_𬾙\u0017𗟜7f􎡲v0顐\u00052*󺑠bW 󼁠\u0014𢕳,䝺zh􌨐rO󺽑\u0014N(\t󾓵\u000e뺘,D|􄔎󳰪)\u001d;q8{\u001f3\u001c)􉾑Z\u0002\u0001\u001a,:htBg6Qz􂯋󴪀􄺍G\u001eꅾ枕iS𪀜8cdr\u001b;\u0012nf38瀊gl$9\ngdX\u0015􃚨󼱚􉎧𣑬q6~\u0011d\u001b\n𥡤-5B󽠤/>M0\t\u0008VK 𐄡&\u0010<𧘜𠢹2Id󲥁0\u000cKg\u0018ZF𨑖rE𦆶󺟨l$𝒹\u0001f􃚀T(bH_e\"ᷛw뇋\u00029o^\u0016𣼍eM\u0003𮈗-𬐵􏜍E󴁿\t𝤥@(뗅m𤐲𤮅󰽀Q\u000c~󼋽8Dv1眰m/r\u001bYr‒yq\u000f 􈶩x𗞰䖍𤟱\u0014\u001e𦙠zR\u000f􁇴0\u0004󼨃𤁳kFꢘ@􋌌xqo/R𨘒\u0013G\u0010𫮭⥺𗵛\u000e@NL􌻾\u00130)uc𢆾9󴎳󴲉1\u0011\u000c\u0014󸢕􌴒|\u000cN.仆z\u0005r\u0001,;\u0019𧯏𡏭\u0017a𣞸\u0019d\u001dKvH󵆷3G(F\u0014)`􊶃O􃇾\u0018\u000f\u000cqL􋌳\u001c\tI@\u0001/k}`e~-\u0015o𝟊S􍽩ྗh𖥒^u>󲑖1𣁪%E\u0011o𬣣us󼕮\u001e녩􏀹\u000f􀱵ꆻ\u000cl\u0001t/󺶻U𮙗󿎿\u0000\u0007𧇏\u001e𣘱\u001bI\u0001\\\u000c𬃳Ni\u0012=>\u0014#𝩶*\u0013I\u0017O\u000eh{􉃘\u001f彙\u001cw!MY𬕺X뎧.\tf?a䏷􀯎=\u0016󽂁􈌽3O\u0006􏭦􉅳r󽹸\u0000;}", + "email": "some@example", "password": "𭱝By$󳢞#JH*k𤘴\u0007S鴊`Yi\u00165𐰌󷔴s𤂜F彄\u000fQ􆝬+n\u001dj:`\u0002𠻢9V)t󽹺\u0005n􁐼8󻚣􍾴n||\u0010𔕱\u001c焞󾠵(󲮯徐j(􈩷3\u001cwS;\u0003]􊄾*􁎣j-󻲻-\u0007;`\u0004N\u000f􊁁" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_12.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_12.json index 7659f0df5a0..0117b0f581b 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_12.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_12.json @@ -1,4 +1,4 @@ { - "email": "ip𘑩􇦢\u0005h:󵎱G\u0015󸾲@󱮟6􎟖k", + "email": "some@example", "password": "h𩔟x^iBXW\u0007x}􈑶\u001b4J9􇝈󶀄􌙒.61x{c󵕔\nz韲8|󰠜\u000b\u0014k\u0014l'\u001b 𣚳},*Di鄡𫿏p𤵆%\u000fzoud\u000bU_y\r\"0󽛯<\u001c\u0001_;`w󰒯/x\u001d1r$Xnf\u0001l􇹌ana\u0005\u0005\u001ac;$\u00194􎺗l$L哅F􇖿󱟵Tq𤦓\u0015c1#SD󸕇v\t4kpS􍥮篏㤮G􂦴\u000f\u000ePT86H\u001b\u0007\u000f󴖭𠜮-\u0005OnJ䈶E\u001f.q\u0017𤑆`\u001a\u0013L蛌煒\u0015󹣾xY0\u001c\u001f\u0008sᆋg7\u00076𧲓 <=<􀵯\u0005􉑞󽁂aOrL󽲓0q􈗰g𭵟6\"#𡇋󼥅󱢓q\u0001V\u0006󾿱xO\u0005J\u0014\u001a\u0001𗏑\u0005:z#ﱇM\u0015􄽜4<2U!HWMMQ#𪍺h\u0012瑅}𝆝#\r\u000fL􈖨y#𗵲\u0001譫jS]!O􄝡脲󿍰o\u001b󼐕䍥$H0!DQ􄟨􌞧\"&J\u0011\u0018\u001e\u001d2󸏆S\u00020􁴸\u0001\u0017Q𡖚𣢉\u0002c9L􉘐s㑻4KFz󽩼\u0001䠋􃩨\u001d6ink>><󴽪Iw\"\u000f\u00182'@29\u0012옋\\􆽀\rპ\\4w󰄺%/6􍥁g~.g𒆤웟􉋇 Au\u0017P𢟎𩡆𪥯x􋏚􇹽袢􅂐?𘍳\u0006I :\u0002Zy$\u0019#􊯤𣳱jv\u0008𫬹<\u000f\u0004𦻼\u0013!󶿉a\u0007|悔])\u0011\u0000]K󽝳2qo6,𘢚z鉿􎯋Av󳄀v󱸕\u000b[e\u0018\u00124!)\u0007),@e?絠DAՌ%%F+󶅳Oc𪷁\u0007\r󹤟\u0000\u0004g4g*⊒AkKC\u001e\u0016g>=𩼅3󽲥\u0000\u0014􉋥p:2HH𡟯玚\u0013𤊦𧁈q\u000fk%𭾖嬐𢜸\\a𝓓E皌\u000f𬵟Nr\u0016$&󴱫􉭞i賉,𥄹\u001c7f𗻫e\u0018L鬜\u0012󳹥>\u0019󼀲N뵽\u0019􁹶7p􎽺vP1?,c􄃄Xv\u001aN'\u0001󴕬lE\u0007𛅳J\u0010𫈢U󸠵kP󷫈`O瀪\u0015jjC󿚼 Me`06.\u0001\u0019s􎩥󰞥Z)ǹBw\u0000zKZ𩮘H\u0003F6yhu𛉙触d+𝞱y􁤯󿇫􏷿)𤺇|\u000e\u0004 􉪰󰴆,\u001a$.-7𣦄a\u0004􄙜R5g􂜧rM𣄼󸂀H\u0000M\u0016\rT𪭁Z\u0015󲥑𮥝\u001f󺝎-덖\u0005󷣜~h\u001epV\u0015MTRyE9\u0017t\u0004繣𭊖𭥻]$v𬎬𬤦ap󽬜; 􊮞\t\"Ww𘇫$􄲆y:`M\u0000/~Z\\eF6󿇸\u00008󳭻𔒜1\u0000⾶\u0000daS\u0011􂍾f𒋚E\u000bt🖪C\u0019%Q􄘎𠙾Xp*78蟊`𢆃<􇘭dJ􃷨􉉊\u000fb8u𤂵\u001b3𨩘F􋥉\u0008㚕𔑵E\n>컕q\u000bj\u001a\u0018\u000e􏯍󰑫Y\u0004􇌁X%𣣰in󷤎w\u000c9𐃃􇃋󹁵Ff􂫌c>\n$}\u0019捋Y햋)􋤯肓󸡀\u001aW+l\u0007b慎\u0014𣟏FmR]|u\u0017kR\u00077D쫏G텫𨋊@\u001a貲\u0017/kt.\u001f{􊲺#^(zcVm\u000c􋇩'6j$[\u0006\u001b/VOQ󳕏VLw􄡆#f\u001eC𣷢𗳅@s딃me2\u0006e\u000cs5g\u001f𣗯𗴆I$t핽\u0017\u000f􆉨&tPz󶬙\u001e\u0015𤯼&`x󵺑?:1p\u001f󲢃\u001eC𦡐㬑X􀱥ﻈ_ZZ\u000eaTW\u0014\u0004􏎙$\u0018p귡|(𖦭67㴳7瓮􂝜𗼇zLc-Y^󵇠\u000c薻\n𖡂\u0008|󿃼\\\u0011\u0000􄾬\u0019,W7`𬶶\u0010𢪾Jn󳾘Efa\u0019\u0000JLwN" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_14.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_14.json index 0e55bc0b7b3..dbeb6287ed9 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_14.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_14.json @@ -1,4 +1,4 @@ { - "email": "q𘝔\u0011 3%Y+RO󹦸1@4`󼞏", + "email": "some@example", "password": "\u001a1󶾝􁒬\u001e\u001dG􍼑.􅪫T\u0002\u000f🞸:𨕩8\u001b1aV\"\u0019G|g|7\u0003P1􏳅A{_귗⪶bufZ?𗡳퉷𭟻\u0013󸑇~g0󶴽B𭠅\u001bR|\u000e'4\t\u0017 􊸚𐡹\u0016炽鱨􂟯􋕾I󹗑9𛁺\u0013\u0004P+\u0016z@m䁞!-u}(\u001bG\\𭷩\u0008|3󼋇鏰e\"赫-\t􋠙\u0012" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_15.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_15.json index 6f5ac6975c5..6aee4de4c76 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_15.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_15.json @@ -1,4 +1,4 @@ { - "email": "\u000c9a\u000clw\u0000,@i\u0012/d𦨥I\u0003\\𤰉t", + "email": "some@example", "password": "':󵶌%칅|󰸬󻪘𡌹䘒󵍋\u0018o+!4T\u0008\u0006𘎗W~󻌢𥄮\"u(4l4nH2􌽪A㻘-󸢦幍𛈛K3@q#𧙅A\u0003z}𮙾 \u0017rⓒ􇑫C냋\u000f|􄣡\u0016x\u0005<󸮙\u001drj􅏯\u001a𮓌\u0019\u0006[\u001e𡬈!VP\u0007u\u001c92f\\\u001b𪌥 ^g󷷺㐙t\u000cvS󾭫\t\u0005F\r}\u001bP𣎨<\u0005)kD𘔼\u0015􄣇@P𦲜-Jⰶ\u0000\u0012R춟x󴏹䗔\u001b{Y\u000ck\n酪\u0007b󻽢u6熞?5\u001b`\u000f\u0005`_𦫖2d\u001e~61Sk𫦔􄾈K쐏\u0010󸶔􇱄􎓻\u0003₮𠷽v\u0011+\u001c􎥎c8ꚣछV􍭎\u0003\u0012h9䎸L뱀7𗷚𥪶)u#\u0015,)vl󷰮7Ogx\u001eS\u0007𪜖L~'g]𐦍xG󳅺j^􍐱🆊ZD('\n\u0010\u00135􏣌𭋷L􋭔􆦦-z\u000c]la􊨳=]n#a󽣎睳􈩐\u000bCPCRM󾚺𣮁=uh2:Z\u0003螾E􍻔Y[N\u0013\u001b􏈨􄕅_7\u001c􂪹7󰻴4x\nE𐣠璚S瑣\u0008e6\u00081 F\u0008l\u000b\u001a󿍋𥑯\u001b🝟C\u0006vX'/4􊤁寖𥆅+󳌜$;+\u0013𪣎/r'󺶜\u000b󰈪H􎉄_M\u0019UU^􆾎X=\u00190\u0003\u001b􈒻DQ\u0012㥼𠠛=rm5Bv*𑁧󹙯𩸿" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_16.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_16.json index 872848275ba..337516617aa 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_16.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_16.json @@ -1,4 +1,4 @@ { - "email": "􄣅bp\u0002.=d@y㯼\u0013𧔬ꋑ厲󹂹Zq😶𣉂", + "email": "some@example", "password": "󽊭\u0010v<𗋹:C󵹱3\u000e1_!\u001bP\t𬊉JW?𗰎󸟶󿗙l􊡅=ᅥct}sUS􅭼\u0019\u0004\u0016ࠃ|/HR~.􀅄/N\u000c\\𢩄4羇t=CY𫳩y\u0013􁖼p}_\r\rhEa􆩏c\u000c3\u0002^@\u001d🥒@\u0002[=\u0005w\u0011󻜮\u0011𘫇~􊰉𗝀.􊁥\u001bn贳.􉜣]6`A]􀢦pA\t𝣾\u0000+\u0004Mu𦘱\u0005\u000f󾶉\u0003?&\u001c\u001dX􌲐dh6Fpዽ\u0014i𡒹􄔿3Z7@8\u001b{\t\u0004Q4g𣣼ᅴ)I𐁉[t\u00049󺥓w3\u000eF<\u00000􅬩~#俤m6I\u0018􍲉󱒮-\u001b:76\u0014\u001f/?suL􂩵)x~\u0003n\nHI@b􌷳iT󱘩%{a츽!%𗍬Key􉛙>󶎤h*𒈔Rs\u0005[!R\u0019\u000f🦨a4쒯b=r<\u0001AQy_u󰯸\u0015𑿝\u0010j9,\u001e󶈰􂲡[𑙄)\u0019汼O\u0004󻙓\u0015􄭠\u001am\u0017m;~!Nqx\r<%?\u0018\u0002K[KxY5M?\u0013nS;󽫘􂃁@𢺾1lb鴛-\u000c\u001fJZ󳥶Ki\td􊛨i𠓓[𘣣~$#\u0016\u0001gށ􃵽g\u00169];|\u0007𮗨bR\u0018햜𢾨󼹶誽AlfL08𝥐\u0010\u001f=.0\u0008%yk6n𩕭\u0005[\\\u0015󼈛놢R𮘔𫶞[f󽚽5\u00004f㋟𫇇t􌰋𦄅*\r/V$󽝞껢8<𐓻𠔷-\u00082􄖥6%f_-􎰒3a\u001b_􏍫\\u3XZ9 𭶙Y5􂮇(󵴩R\u0008󶍹f\u0008󻱍z}\u0008\u0008Y6UOz=ᚇ󾯇W,􏔟\u0014󾽦𐇴\u0016>􂬬𖢸\u0018􏾘Z􆹇󸵥g\u001a􈯣Z\t!Q46\u0012𐳒jM\u0003c􁙬EZ\u0007𗒸rcDK\u000f\u00029ⱬ􇣣\u0008s\u0018}:􍂌쯸!gp\u0015z\u0015𝑷\u000eL󻕜\no𗹦;*E\u0007㵀gST\u0008j~􅟲d􌩣V󸂙Qf􄸿L\u000ei\u000cY𮢄6\u001d<6N\"\u0007],s9􋮖􅛼M\u000f󱴾k\u001f缤E􉯌\u001eTJkEc\u001c󷒩4􄷲QE󷒯慲8𖦝󰤾'𤇼\u0016􉚥\u0007>?\u000b\u0007\r{rW,󽏠d\u0005$2FA􂂲\u001fwL疯!󲩅\u0010\u001ecf𢙘\u0015)uSz􂟫O#B3;󼷸rC\u0002DA5\"\u0006 \u0008j\u0011^\u0006𣱺i\u0004􎬗BM]\u0018!􏘩𮗕\rA&\r;GTE\u0018z\u00196tQu𡕖Rn\tT[\u0001\u001cQn>愮L𖦝􍢚\u001c#𫽌&n󼚬󳷐\u0005K\u0000H#\u0010npu􂐌Bt{X\u001c2je@5󿧳󼯽􅢒;\"􌦍Q\n𘘙􉀀흝\u0011U~@:\u001f~锯\nRLiD󶁑q󲮹{9d🗤.퐏\u0015\u0008]HlZ𥛇\t柑쟷\u0018{3󻏟E;F8G0g󹯼&'󹹮C􁘋\u0014i@v\u0018%[_g󴖽5􉥛+;.DtH૨z|􀫰󿊥\u0014􀼴Z\u000cS󻐸\r💣r~r^[𩦪䘷o\u0001掯_H+;\u001d🌱\u0007&m󰏯U8f%\u001c䗮銜Z󻛩8},P􇳰H\u001a\u0008􁙸H^𠱎`gRf𠬶􊰁+V\u0013\u001c\u0001🖭#" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_18.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_18.json index 799eebd86dd..af8b333a696 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_18.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_18.json @@ -1,4 +1,4 @@ { - "email": "1+dHm*𑘍b@", + "email": "some@example", "password": "N𦄸\u0004C9\u0014\u0011\n𩤬裚堽/p􊠏8\u000bg\\\nYGz|/\u0007Nd󻕺\u0012%툶o\u0016\u000f闬r􈶵󱂾d􎲀o􊻨\u0010l5&}i\u001bg􍤵ᦫ'`8N􌂽@𡖨󶎄Q[o\\\u000c􍦥Ꮜ\u0007d~ꝼ\u0006Kꞃsnr]pLA\u0006􅀊\u0006𒁾Xl,;wz\u0019󳳼􅀌\u0016c\u0015A7q\u0013\u001d툃ft𬰣f𬠀\\󸈦􎒉\u0001፼9F󽄮𦍥D􄔂\u0000z󻡔𥄚4\r􅯼􅣿󽅊\u000fO\u0018\u0004Y9븲C0|8𩉛]n𢍦\u000f󺉪ra\u0005􆯖\n]J\u001dBﱲ\u0003gT𗘠P\u0004>tﳨ\u0007\u0018r\t󶥍;Yy\u0000\u0019􅥯Qz\u001a-'\u0017*\u0001;J4]9𬥼\u001b𡫀[z鵺=cpG_;\u0004u􇏎ic\u0004\u000bkz\u001a|}󷂕\u000b]儼G3\u000c\u000c0\u0010d\u000b,􇱜䍜[#~8OG`n)􍼫5𢻤\u0016𐠧wK\u0001;\u0011+[􊖢'󷘅మ\u000e母-\tB􃃓h\u000bVl\n$\u001e\u0017\u001bw^!\u0010􍤩\u0013󰯽3j\u001c𤛹8oj𗜂\u0008󶧡t4>:裞_\u0004\u0018𡇔F󺶤lup\u0003+n\u0001P'yV;:𩧱V\u0011~ 1󵻽􍒽A-\"&8}\t=\u0019\u0001\u0015\u0013F󰮹􌁈\u0015픓/BIᎿ,#\u0019,󽉌gp\u001ew][\u0012>\u000b*b퇹,\u000e\u0019\u0002𑅥ŅCuTd𫬦27B󸝚oRm$𦢵Y3\n'?\u000f\u000b\nf𪵤K\u00016\u001bl]`U)m䣪" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_19.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_19.json index 8ac508be50f..141c7262b8f 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_19.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_19.json @@ -1,4 +1,4 @@ { - "email": "W󽅄`)SR\u0002,Zw\n@", + "email": "some@example", "password": "yMlﳹ0~􀠺󹑎a􋆛\u000cQ𗆨~\\y􆎛\u0019\u0003rqFsvG%So\u0006aTH,\u000c\u0002P𘞅\"\u0014o;UE\u0007\u0016퐃\u0012r`\u000bu1)2\u0013j*\"c9󱐿􄔾)h#𨊼m*j\u0013껮~v+@\u000b\n5*ui䎙uS󴼵􀸍sqM\u0002󼈲\u001c𭭳𦌨fG𦈯tv}걡?V\r􀬥]󷤅\u0017\u0010*E+V}𦃡9\u0019*󷿣[o䗚􎐂\u000br2\u0007􄩐\u0005 륅@O痔^􂭝Rꘞ!HLT󸭑.󿧅􇢯\u001d=+D𧈐4|󹾞T\u0000𛊢\u0017󻉘􁛀w􋃑#󽣊b\u0019F\u00073k\u0018*#󱟮3YKs\u0006\u001a\u0006𢗨D\n􉼼(\u0004􈥵&/u㑕W@)pLY󱆶)<{\u001d[LkCU|\\\t\u0018\u0013Q\u000f\u001dS\u0012􈐅􁥁\u0002\u0010󵢳3655H%\u0017󴯌\\k𤰟xl\u001dAf\u0018y\u0015ն𦡤󵊝\u0007uH\u001by0(㛓󰼔q𘏚\u0005󷼨\u001a6l\u001d􌶜H5\u000fX&a1j#e\u0004gf6􌟗{􌩁\u0006a{\r􆝞\u001a󹞬Ih\u000b4㙃􄋠\u0007Sq풼𡁳B #󹮁󰛘%1&ᙲ􌼤\u0000W\u0000󵥀\u000bハ,@p󳕠_s󵟌鱀:􁇫棁3m􆌹\u000b_󻟴\u0016L>\u001cq_1\u0003󷤑I6L\u0005𓅐󲾐󲭛YﯓzH\u001fu􂴦󶈀n\u0016vA\u0017G(\t~\u001dT闍N\u001f􁫇bju\u001e𨋍Z𠢄P ፱\rq\u0006\"K\u0001\u0014\u0007qcJ\u000f{𩽶􂄌K\u0000𪶚F􃮇,蠞\u001a\u0003W\u0014쉘\u0006N\u0004LAE#􁃧hIWO\u000bj𞥂:cP~q󵾵\u0005U4绝॒*z\u0004){\u0000.T􄁆=𫍌𮄾􏡉]\n쎚4s\u000c󷃵𢐦+층\"Z\u0011oN2𪡥\u001coee\u0011wD\u001c\u000eZ𨖯4\u0008\u0000\r󠇁𡨝\u0014:7󼫢" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_2.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_2.json index d0a40ab6719..17f66be22c5 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_2.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_2.json @@ -1,4 +1,4 @@ { - "email": "莟^Oz􏽿􀖮NG󸵚\u001djꦧ\u0000i𨙻@0&\u001bi^i\u0003G\r\u0016", + "email": "some@example", "password": "􋥵OY \u0000􆀲\u0001X昵}􁏀\u001a0@쁑N󿀾\u001c\u000c.+v1n{F'󷏺u𣮶.v=𭵷ᇎ\u0010af\r\u0015e\u0000\u000e\u001d􊞽𡫳\u0001)􈏘hw\u0001\r\u001c\u0013Z6僭X􋽭h]0%no\u0011󹦌 JEJk􆾒󺽣􌱹dTrtvuz􄥱𬁹f󱫿8\u0008$W0\"\u000e􄝑􌴇~3𔔀~f\u0005z\u0010f\u0003𡧶`1\u0006t􈯅@󺌇_󶞾\"]󺰹󸮍𥢡o0>Qd\u0006`GD\u0000󷙦p쥚JF\u0007w\u001cCo𨛚\u0012\u0005\u0008q\\;\u001a󰺊to/Q/3⢺F𓋂j𤁫}yi_/\u0008j*y󶮴&\u001c#KG󾈵\t{\u001fn󶛵󲮖|\th;CJ|S𦮤D􌧩rh󾰤\u0013\u001e5VV󸕦ux9\u000eu𢛮?􍰨_\tx>4󲵔䑿-d:y`*T7𨫧_7\u001d\u0016迯bkb\u0019\u0002􀱓\u0014$󹭉i\u0000𫢓𨻿xUT󴿦a\u0003𠯎V^\u001dvz?;E𥡬𐘷炙\n8X𬳲󳠖\u0018ot\r\u0017\u0000V9>𞹮`c\u001a𐍱W𧚎졳𝝳rh\u0013&𢊂􍌣RC\u000b&=2\u0008􉒙\u0003󲲷)⇿7Uj󻛸A,ckL\u0018𦁑o27𤲅\u0006L\rucW\u0013𮎶Bw_=\u0008" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_20.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_20.json index dd21aa39c15..b785dce2c83 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_20.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_20.json @@ -1,4 +1,4 @@ { - "email": "#႑|䤉􆨕@\\\u0003􊘆A \u0003DT\r<", + "email": "some@example", "password": "y\u001aWK􏯩\u001f:d󳕈1󾴚d\n]煚\u0001󼍉Q\u001a\u0010􀋏\u0018󰖝#7$􄧙'i2/%u84\u001e*󱧻󽫠4^BB?OxꄨsW\r􏗬2\u0015F.􂓒I㎐8\u0002\u0003`\u001d!􀴭<5Y􁌨d󻕺P􈽷P􏎁?1􌦺n\u0015a𮫲-𩛄╭\u0001c\u001cne_􉿗􆊛c7⓮Yoe\u0000_>𫦈\u0015\u0000N𝖺󸓅J\u001a⎎OkZ~yV$\u0001R\u0000Q􈷼[􅳱􍟏?\u001e嬣󵬡t`๏}t^p\u001d偣􈞡qA\u0008R^폗􅔮f𠘢qZr\u0003K\u00051\u001a$󿺞󾐰􂉗󸂝\u000b_C\u001ab[g𦉇i􃢕j%jZ9L\u000f6􊭶l~\t{}>N󺿆􋄆;\u0018󷒚#\u0013:\u000c\u0004.󼢨|󴇆%iMr𠒭:\u0003\r􂱰\u0017B󻡲;\u000eT_KJ=b0󴰃/u\u000eq\u000ccV[h𣫻}`\u0015N\u0010P🠇H\u0018:F쉧󱔃+󽋯%2}|s1숓[Rp\u0001O\t_E\rK\u001a|\u0001\u0013x$󹉼􈞅.ﲻ\u001bZ𑄦\u0001U`\t^\t0'8\"O\u001a􀆌\u0002nbGo3_􁾈􈥫󳇍\u0016.%\u001e瘿􊠾Q\u001dD:gJ=Q-0􏽟q\u0002x\u0003r\u0005T?\u0003+$獶>􍙎㼘ƌ\u000c𣰹Qj\u000f􄭖Fs.\u000e𝁼Y.󹾊J荥\u000c𥲐A𦑤ᏽP5\u001c\u0005𝂓\u0001\u0001􂤣􃴕䎬􃉉/x?\u0016I𭓍\u0015U󵾰㜧Ig𣛦L]^.𨯉}&(𭿟𮂌vGg3󺢃\u000el\u0004𢱠\u001e\u000cs𨎁p5LS󳮊\u0001|\nvm6#􍟀􍝒\u001d\u0011\u001e\u0008𦣯󵫎\u0001\u001b𬻒􁆽bE\u001e\u0006`wj#󷦙>Q\u001f􀭜ss𤶴􊵩ck%>QຶB$\u0002&澪%\u001fT\u001f\u0017:B\u001b󺓍\u0008C𭔙h;O:?\u000b󼯿j𡜘j󶺇;t􇲢\t\u000e@ዷ󼯥p𮠏\u0007󳍵㷲@,𩒝duBfR\u0010𑲯!\u0005<\u0012\u000ej􇷠XFs󽒇'W\u0000@SDA􉨥" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_3.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_3.json index decd3266f04..431930dea36 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_3.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_3.json @@ -1,4 +1,4 @@ { - "email": "h)샓Z􃮐􅣒@𭦧\u0019上󳉅f\u0008", + "email": "some@example", "password": "K༁|􉄞HU\n\\Qik\u0019\u001f𥡰xG𨘵1g鐟9􍩩v4􈙇\u0008\u0005􄪒􉀚2\u0018􈀏|󶐴\u0015&\u000f;2\u0011\u0008󴮏A\u0007=u\u000f鲣\tz!pwH1\u0014\u000e.lexJ\u001e􂍦3\u0019&s𫑾󵋈\u000eN􊻦\u000b􀧎쁨􊞆#n\u001eY􉧦\u0010#%ObL=s(\u000c\u0019󰤹􇯭y@i\u0018/o\nof}Z𝩘V\u0000DqD<\u0004󱀁𖡐 ;\\\u001dJ)\u001b𩤤Sth\u0002^'󲞛󲶃𩵝/ct9\"\u0005$s*􏡋󰉓T󴌮\u0008\u0018􁉕󼁤9Drm𒄠$3𓉽j鉉y*R@󿏲􆣙mGM󺈅^~\u0003\u000e*\"2#􋘰S\u001c\u0018|Y]\u000eWy&N𦾐Dw\u000c\u0013Y팀%^+􅖘s􈍛\u000ee􇖙\u0016󽦄𑚫잙o\r𭈵z\"jn2\u000c'(B\u0016?s#*d\u0005\u001c恘\n𧋛\u0005􉴊$f)n揊}&N\t􆏞\"1􇝈u_QCU\u0016eR`󷀔N\ty􀇡㶶\u0011􎏏k;􉬬𣐧t~JX=vN6\u0007_~C\u001f󴚶\u000e􉿣?$b󻷡\u0019A\u001a=vm\n\u0004󻵊'g@\\𧨥AhVW銶𣆎𠂹󿹀ဲ\u000bO)U󾚢󻩣\\Ly髽eA4ꎍ\ni󲭧ꫨ\u0004_𗶱n\u0014𢙎\\S%쏻􇫬e\u0018u󳪋􀨿'V-D󻍓X,q\u0013\u0014𩘩\u0005$땯ﬢ.e2l􃊡\u0015\u0003{TW\n칀ԱH􀋝􂨲󲸢􋔋\u000f<\u0015M;[d\u0014\u0008}y3Jt𘝵%l.\u000cU󾳱H\u0002-.#\u0001\u000c𥂺!\u0018Y{\u0017d𥊑[(󾇘]j𬐻am!Z[w\u0002'*Tv𪨝PM~󼚕0\u001e!b😒\\P}3v\u0019n􃽻9F2gW4𡾪󹦃\t𫇘𤀃𪷬hR\u0005%鑎z󰕥󷚐?*㘈(#\u0019r\u0014\u0008r𧡝WN[ N{􋞱\u0019v틈\u001a󱠨􌭋􁛙\u0011[S\u00138\u001e?􁴏\u0007A\u001f󿳐A\u001db𘊨􂐆-\u0018(jᮬt\u0005;}\u000fHX\u0017]_,􏅩xVY𖡊\u0004H󶟹i𗛽\u001duu'>S\u000f\u0001灋䯲󻒦\u001db󰨀𧙰/\u0019􆌋7\u0011/X\u0014oZ󴿝𩴑橤|FN'􉨃\u0011􋖬wB\u0010]뺝 V􆟅􃪕\u0007n􌚟\t1􀉊 [}C}gG􏧭#o󱲻kF\u0003\u0008K􄝬g<\u000fn\u0018Z𗔷$}h\u001f\u0016𣺺5m𓐛󹫟<󺍝隴h\u0010鋬󰅖\r#_􇾑fD\u0016`󿽼_{n~I\u0004#7\"K\u000b{溃L􆵑\u0010󻅨󱾍濾]l x\u001f􂲹𬃪U\\\u0006𡟿鐵\u0008>\u001e$Y􍷋\u0004\u0014𡀅𦿦𐋸gE[Z🢇󻤻\u0016@\u0003􉼢uὨ뛫(.\u0005" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_4.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_4.json index d8e2569373d..9714e72d3aa 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_4.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_4.json @@ -1,4 +1,4 @@ { - "email": "𭿈%𡉋𩞄@wY0nE뎟d,", + "email": "some@example", "password": "󳠀`h𠻃[/\u0010\u0016􂉌𮤺c\u0010𣔼\u0002$􀕶V\u0012}\"􁸤1蕹K\u001fC𬫻\u0005'󳫓70\u001ee󼁅\u0012w_꒫\u0016" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_5.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_5.json index 4e3d168c6b3..736dfafdff2 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_5.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_5.json @@ -1,4 +1,4 @@ { - "email": "􊔞􆵆\u0006f?􃱘\u0011R`@稺c?\u001d󶑤bOP", + "email": "some@example", "password": "K𑘭\u0019r쵦s>qsz􁙱􍞑㤲\u0019<*󼞒鎛\u0018^&💱9N <􊬐YR.𧺍A\u0006)u3􇋂6]𠬦IwBZ\u0002󲂩󵫐mA}2㊞y𞠫𩐘􅟏󼃡/Q%E\u0008L<\u0002I\ra/󷒉q-얤 ]U:^YR0){\u000c{g𩽦\u0006F\u001f<:𦵧]ICꩢ𘘓𗵰O($h󱼼\u0011\u0001e\u001c\u0001xQ)􂴟􅜖𫘡􀙴OZ􁧲\u0015\"󱦉/\u0013x[𥥯𭵑\u0010V𭪋𘎿\t𥺾E)H\u0007cIsa\u00184\u001a敖8𑑃\u0007i\u0003DD4w𬡠\tEjf\u0013h􂒹􂝘aߑ\u0006痕𠓙𝀉\u0016<,󴎲+Q2'o\u0019\u0002]\u0019%\u00123PQ&H󶓭􀤄\u0016\u001f󴾲YR󻘿䝌\u0000Ex\u000b\u0001𛀂𥬛𠏐O\u001f>FG\u0019ី󼹀(𭔲B4,𧢜􈒘\u0005󷁇HUJ\u0016􅎮*􌭹w;j7󾗻1TJx\u000f󿇞􌛧Pn\u0003*O\u0000t>a_X|_MmL៓\u0011󰒺󰭹:􋔽Cu[r>󵛘vp\u0014" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_6.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_6.json index c8274a44a88..e319feb7d9e 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_6.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_6.json @@ -1,4 +1,4 @@ { - "email": "z_󸷴\u0012@𬊩VDG", + "email": "some@example", "password": "R\u0015䬧m侯|𩋹|&\u0014펀_/􇦤Te𫸁" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_7.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_7.json index 77fce9a2b0b..83423c6a1bb 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_7.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_7.json @@ -1,4 +1,4 @@ { - "email": "R􎛏􌸽Tk뫀4\u000cm1r@`O|Q%", + "email": "some@example", "password": "AV$\r:\u000e􂒅\twBe#aP\u001df\\D􊸋\u000c\"􃙊\u0014􃩫N\u000bW󻿥𤹨; O􋤉󽱏𧿮􁉀󳙇𦩔𬀃*a󼁴w,9⺜芽󾞷Q$\u0015\u0015pv󲚺(Cl\u001b\"cg&2j\u0017𘢢|\u0005v&e븈gfK'j{F\u001e𪪻zn~棣a?󱞒\u0006 桸Z,#🞒:/􆌳󳽇/sGF\u0017i;{|⏺?np慗_pCi;1j􆩨w𠕊􍁦\u001e&􋉷q$\u0012p\u001dC\u0005V\r𫲎g\u001als,𨮳d\u0016C 󵇂<}􊎒)x>~𛰠VV󽧢\u0005m𫖰\u001e\u0012𣪐\u0002ykNo􎴹]\u001c\n\nZ\u0003p􊺵󾄼㦿𑐻oy!%m􁳺Pg煳Mz𤹿𢴥\u0005\u001doiO􇞷R\u001cHG(𥻡󾲨<[nAlz'\u0019N\u0007X^J-腭\u001a`ubS2a\u001c􊆡I\u0010\u0015􄖸u8rSJT蝝󰂉􍙏|\u0008󳥙V\u001a\u0018K.(𞲱\u0007􏙛􌕿y&t(픒𢊴Y@t\n]\rlJ-𐂇H\u000eEp_Nv<=^鐣\u001e>A\u001et\u0015L􈜵消t\u0004󹝹|/똱\u0000s50\u0004𨿈􁊛\u000f1􈈈\u0007󵕂\u001eᾲW𦗮vsqz\u0018耧m󰮙~j󿍯󻼇l𩒕Zd󻞀\u0001lKy\u0000ITt僥𭀈\u000eS(蚺K눟*\u0000n灼^𮄆0󽽸\u0013􎱧𤤾􈩵\u001c\\坚f􎈽&N(*𤹣󼊵\u0000~*_\u0015󼥹󹹭6s󿞀ꄡz\u0000s8!0m\u001fb𢲙\\1lu2?>7x2^t3퓙L􇴤盅|j[Hi\u000e75&\u0004!郫_pFu;'ᰍ(𩕑Jk\u000f;\n𖭯@mꅊ~\u0016\u000f长\u001c󾐞\u0006(qh%\"\u0016\u0017C􉼍C臘8Ff,O\u001bks.q阴J+W󲫼3/\u0007ͲQ/🏐&\u0004l\u000555]5\u001a<`\t\u000c[#\t7!YI\tei𐓷鐬1yUS􋢨X2o󻠚\t𗫥{\t\u0003Op\tf$B\u0002?\u0000xv󻓂P-u:CW?󷩺z\u0004hO&𗹺𫸇\u001b󿏖z\u0012𫦨7\u000e㹶@|𩺝5" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_8.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_8.json index 42dbd8365af..be7e48b51fb 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_8.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_8.json @@ -1,4 +1,4 @@ { - "email": "\u0007@z􄏊㘒V:𫩆\u0014𡙺", + "email": "some@example", "password": "󼉧,+柃:􅪤%G\u0015󳨌/\u0010@󺵖搷l7\u0010w\u0004e\u0003\u001fV󻆘i􍆙\tJ󹸺𭺳k𢻲Hz%|aG󽮼\r쮫Q\u001f&\u001d\u001942\u0006C{t䇣]g\u001cdO뻈`.𭀚`m[\u0003􇊡-\u0005㽕x\u001b􌿺N𢺒\u0010K8𭼹[󱜼󶎋k|e\u0000}\u000e󲋋<\u0001g󻹊G𗯂K|^􋙭\u001e𩀞M𩌲/i􋂔!#𭠔}ṡ\u00190􎞈2 B^\u0011e@,I\u001a􃁼𖼥v󶦰\u0000]\"c𡭼릶\u001e\u0001B\u0014;+N􎫘UI:\\Zc􄒉zm|+{W󱖆IৢMU\u0019󲙪\n\u0000H\u001b!n󹉝q.8{󵓌𧦻T>🎡􂤜_ \u001b\u0015Z\u00049𓅫󱸙r\u0017|\u0001𔙂'#ැ\u0003{|=P𥤷VTH\u0008󲊎℄'𗙥\u0001vSk󾜜컛瀖K#^備O!'_}􊾃󾩽\u0010\u0002Qp}\u0010fd🝬(\u0007;󵀋`_\u0007&𛃙LI\u001d\"𦵴\\71\u0007\u0012F󳬵𬪍L􅗔a@\u0016\r􆸃𗭩h\u0006P;_YjဪCH~V9\u0003\u0012M\u001a􎝞􂛡%5;\u0001d]" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_9.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_9.json index 2127624ac43..615f6e284d4 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_9.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_9.json @@ -1,4 +1,4 @@ { - "email": "?!@\u000c󿱚v睽\u0006炬", + "email": "some@example", "password": "𮩥R\tF3{7󰳃Qᎌ\u001b𮕾h[\u0000V_m8\u0003:K\r󴺞(􉮗\u0014􄓐\rKz\u0017𗪁^o/&!6\u001aV\u0008UῚ-1n-󹪡r\u0003觼F\u0007􉧒+󱲍,\\Xm\u001b̕􎠎|텐加\u0019=\u001a􀊺fR\n󻄌cC>jhZ-\u000bBnq\u0005\u0006\"\u0012L􊄎\u001f;Q󸫀D\"Q@󺳴Qq儝" } diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_1.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_1.json index 39865211232..4c99e46ce63 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_1.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_1.json @@ -1,6 +1,6 @@ { "description": "3즽)S", - "email": "OR胆c@\u001e\u0005r", + "email": "some@example", "id": "00000002-0000-0001-0000-000700000006", "name": "딂\u0014", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_10.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_10.json index cf3dadd23ee..108ba95f964 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_10.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_10.json @@ -1,6 +1,6 @@ { "description": "󺔣隬E\u0001", - "email": "gd󿡐􅶲e@", + "email": "some@example", "id": "00000004-0000-0008-0000-000500000008", "name": "󺴓|\u001f𭰔-C:\u000b\u0018󵹅;|\u0013d\u0006>@𗛳𨠏2\u0010𡱷\u001aM7/􅂢b\u0006O3m[{", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_11.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_11.json index d28c998398a..d9352dc4b45 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_11.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_11.json @@ -1,6 +1,6 @@ { "description": "𓄃ME\u0011[", - "email": "@W", + "email": "some@example", "id": "00000001-0000-0003-0000-000700000004", "name": "T襶nJwq𐆗[\u001cV\u0012I𫱃\u0001\u0005J:\"ay󹕌󳸲󺟖L\u0001&%lT[l/?󾿛_\u0010rW󷑇󸕑]𡧻􈐋𔒡wM\u0014#颷", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_12.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_12.json index 1de307dbc97..5e1569b746f 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_12.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_12.json @@ -1,6 +1,6 @@ { "description": "\u000cE󽇵󿚪f\u0005", - "email": "Z𤕓@!󿕫𑀡\u000c嬾", + "email": "some@example", "id": "00000007-0000-0004-0000-000100000000", "name": "h-\rE,𤋍 􉪺t\u0013S\u0019㟏&8Nf\u0004E$;;𧽷\u000e\u0016𮬲D,pE\u0002?'X*\u0002\u0011>&􍕂WCGM=Ey􉫺,귅$", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_13.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_13.json index 4b7f555698d..52714a62ad2 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_13.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_13.json @@ -1,6 +1,6 @@ { "description": "\u000bY/\r", - "email": "\u0001#𠶗@u\u000e䮙", + "email": "some@example", "id": "00000006-0000-0005-0000-000800000004", "name": "r􋭯\u0012%_izF腯e딬\u0011/\r`u󹰖\u0007\u000ff􌅛f􆃯oP\u0019&𠀬\u001a􂧦􎯧jY󲓅􏚣\u0003d􃰊e󴥟HK🥏\u0003y􎋊TE\u0001?𤎅 \u0005\u001b󷿣q#\u000e\"(Q^𬰩&\u0016䥴PI]Q\"X\u001a㩚𣡦x '", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_14.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_14.json index 5688761f5ee..d5f80061f1f 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_14.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_14.json @@ -1,6 +1,6 @@ { "description": "-)\u0005/\r", - "email": "<@M\u000f", + "email": "some@example", "id": "00000007-0000-0006-0000-000700000007", "name": "\u00014\u0005\u0006>\rx~J$k!~\t\u00114󰢆\u0010\u0017\r\u0017y!9", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_15.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_15.json index df1f33f905d..03ce82319f2 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_15.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_15.json @@ -1,6 +1,6 @@ { "description": "$\u0013\u0003E", - "email": "ळ\u0018:c5@G􀐎E`", + "email": "some@example", "id": "00000001-0000-0006-0000-000300000005", "name": "uU1;袙b \u0005%􊇛쫏פּ2f􁗰\u0008􆴉\u00140({󰈩􋦲󳨤}z𩍖Lq\u00025.\u0002􊬡s󺉃[\t\u0018E𐠑𗏠W􍹱'󽆒_􍚸6g\u0007𧉏󼚱X5j󾒲Q𘩼텵o/<\u0013wm􅋾'𡌤F=𨲱d𨈦da󶯠6fbnN\u0017=􃟍\u0017x\u000b?{3됐u𥳩K𥣷朻", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_16.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_16.json index 6c3a2697049..172f35d92b8 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_16.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_16.json @@ -1,6 +1,6 @@ { "description": "\u000b", - "email": "𬊣@蕉z䇋'", + "email": "some@example", "id": "00000008-0000-0001-0000-000300000001", "name": "sl\n􍣷\u001a󱬙\u0007𢼩󸝷>\u0014\u000e󸇗𧅢7m7\u0000誐\u001e퍚uXwwA􃚆!𦌸\u0019N􈖆𨌉ᑞ\u001bEZa󻞿𭤷ZWY𮋜|&唙ikM\u001a􃆓􁷔𧇹󱚺󹊤K\u0001Y:\u001b*Wzc\"\u0006󽣕􊯎\rWB8jSl\u001d\u0019󵁂屋\u0002f`𠓤LN\nje", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_17.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_17.json index 8b71f3c17fd..b94aeda5e8e 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_17.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_17.json @@ -1,6 +1,6 @@ { "description": "𡪥5䁫", - "email": "X􀜨J1\u0016@󺇢%", + "email": "some@example", "id": "00000008-0000-0000-0000-000400000006", "name": "鄚|_;􊋼뱾\u00024/㍄yqDttZ\u001a􄍳y䔳𫓚", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_18.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_18.json index c6b3ce84320..105d1f609da 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_18.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_18.json @@ -1,6 +1,6 @@ { "description": "$󽋼3􄺁 L", - "email": "@\u0008\u0005<󶔈g\u0013󸘺x􀧹*8-T*\u0006'\u0015[/󾫄g𢪸VT\u0004。,𫰪􄿖kC𘄴s\u0010+\u001dz𓇑󾄗`l", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_19.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_19.json index 4e2c8c153df..b2c7466e28b 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_19.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_19.json @@ -1,6 +1,6 @@ { "description": "^ﭠo", - "email": "뒭`kmI@􍤻", + "email": "some@example", "id": "00000006-0000-0005-0000-000200000008", "name": "\u001e\u00161𭯇,%7\u000e􏭎\u000e\u0014󸩴k0*󴚍j\u0016㢎W󸽪]za粥𝢋\"9O-o", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_2.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_2.json index b8e14f08877..ac8e9c44717 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_2.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_2.json @@ -1,6 +1,6 @@ { "description": "}[N", - "email": "@\u00027𘐄", + "email": "some@example", "id": "00000000-0000-0008-0000-000000000004", "name": "?􀜟ヽ$?𤰔uTY􌸥fH\u0017\u0002\u0005\u0008\u0010%:!Y\u0017𖢍튑􋥤󷅺*]/Z􀗭>-\u0004󾌗󺘧!_*--7\u000ftEg\t󻍦\u0013􇪚\u0018vE\u0010𠼌?=\u000e󺡆齭𩀩O恁k\u0000󸳠纏.\u000f", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_4.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_4.json index b48c1863fd2..4da43c38fd2 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_4.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_4.json @@ -1,6 +1,6 @@ { "description": "𭃲j>W", - "email": "h𧟨\t􋽒G@𠼫/\u0014", + "email": "some@example", "id": "00000000-0000-0002-0000-000400000004", "name": "\u000e󿚃n󸑏7f􇉐i\u000f8|\u0002e\nN~$[vAUr1`\u0015\u000c/\u0008~􈵉PEhV={󽑌𧎸\u000c\u0019􃫟}}ుx󲹀󲹾􅇱%\u000coA씚𘉄~t𬢴⼰\t􆋲\rWA𭣅􍧟t", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_5.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_5.json index 0a523adb34e..0d7e9416b2d 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_5.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_5.json @@ -1,6 +1,6 @@ { "description": "T󻥮]\u0016/o", - "email": "%>@􆧊䋈q", + "email": "some@example", "id": "00000003-0000-0007-0000-000700000003", "name": "ᬋgr\n詥-鄼f\u000cJ9\u001el)\u000c倦_H^Xh\u0008A;O|", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_6.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_6.json index 6bf3095beba..8b82db2d6b1 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_6.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_6.json @@ -1,6 +1,6 @@ { "description": "r", - "email": "󸳘@_q[w\u0000(", + "email": "some@example", "id": "00000008-0000-0000-0000-000600000008", "name": "\u0013 &3\u0010\n𧾋'ᅬ7᷂\u001bEwP\\𞢡\u0012^\"󹽆󴳐\u0013-g珖<\u000bფhAjOZ)󿊓W_𥪔𡋁s|+󻤌\"~D󽬴C\u0010\u0003𗐑\u0017w\u0001\u001191\"\"6D\u0008\u0010\u0000.PC\u001e\u000e􋒾󾽝<𩻦iuN𬢤􉬅U{wgq\u001cD\u000b󼨦\u001a\"\nw{Rl\u0006Ua3\u001eNx\u000f", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_1.json b/libs/wire-api/test/golden/testObject_Provider_provider_1.json index c646977db3e..07a5d2834ab 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_1.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_1.json @@ -1,6 +1,6 @@ { "description": "=󱶚", - "email": "@Mk\u0012󻎹c", + "email": "some@example", "id": "00000000-0000-0003-0000-000700000002", "name": "󰩉j\u00028'\u00145;QDq,z:4􂑎TQdrz齞r󲽝 o&\u000b쭂pVe􃡭x𧀷쑧'w#ﮜX􈌛􎞬𩇞󷒸)𒈜ꅓU[{bK\u0010\u0018狡󰻃𨋈\u001f,􉋟Inu茼E󵁰,󺴋\u0017\u0008\"\r󻫷\u001a@y􋥲𐊎m􃯹epM3Q{\u0015䧛8g2b\u0000􁏔\u00191\u0011\u001c1􆵸Ov𬺲u󱔩󵲿\u0001=\u0005$𭧄\u0015􍣷冯l\u0014/K)Y'⫨M\u001bX\u001e", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_10.json b/libs/wire-api/test/golden/testObject_Provider_provider_10.json index 8f42c362751..a6bfb96e1d4 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_10.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_10.json @@ -1,6 +1,6 @@ { "description": "E🍺:\u001cJ󺵘", - "email": "i􎈈V@\u0006w0\u0003&", + "email": "some@example", "id": "00000001-0000-0008-0000-000400000007", "name": "U𠑇2uXTV\u0012􋤕\"\u000c=K7}ws𛂑𒌥*1􂷗_\u0014⨫\u0005^4xt.􀅺毱m󺆜{\u001bt\u00192󼞀\u000cs\u001ai1⊹T􈼱=e輽횙1", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_11.json b/libs/wire-api/test/golden/testObject_Provider_provider_11.json index 1c12be8affb..b79eac431aa 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_11.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_11.json @@ -1,6 +1,6 @@ { "description": "\u000e\u001d􉵦\"'", - "email": "@\n\u000fX倉.&", + "email": "some@example", "id": "00000006-0000-0007-0000-000100000003", "name": "-\u0012\u001d\u0001q=~ 𨸳d𨙝HF9\u001fxT;x|2@a\u001e|\t,;Z\u0015󺌾嵑󿞌\u0018玨l\u00154\u001a\u00031b𢁡TSP謘\t\"𝃐;P𖡷\u0016\u0005 S\u0006EkM轳h𣌥󴣺𘑤w\u000c@\u0003O\u000c~P\"E6\u0007\u0013[7yu􏴴\u0012-\u0004󿔆2", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_12.json b/libs/wire-api/test/golden/testObject_Provider_provider_12.json index 839f5aefd30..bf691857870 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_12.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_12.json @@ -1,6 +1,6 @@ { "description": "x䍉@I%\r", - "email": "@m靐첇E\u0014󴆟", + "email": "some@example", "id": "00000006-0000-0007-0000-000300000006", "name": ",U5>pD󸉥O𩕽Rbk\u0019\u000f'V\u0011􂈜-]󴣏Q\\r-􅽥\u001a󺠄E󰵚\n!\u0006󷫭F|\u001bz󽯡s}h꣹O󷅢(v󻍓,C6쫃p\u0008󿋏5\n\t䠚􉼢㢿1\u0013}󷥋􌲒h󵿟𤬅>뻺7󼩀LI㊝Ck󳾚𧥋@\u0019\u001f󴊢`&zd7b􃈞Qܫ􏂧", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_13.json b/libs/wire-api/test/golden/testObject_Provider_provider_13.json index 09edc9f7510..fdbacbcba82 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_13.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_13.json @@ -1,6 +1,6 @@ { "description": "\u0012$=l\u00114", - "email": "y@\\", + "email": "some@example", "id": "00000004-0000-0002-0000-000400000008", "name": "뵀\u0010􃫻\r;\u0011T􉽸\u0005\r\u0004jtW󲔋S샎z𤣼(\u0011v󶀁𦬦QOl􉊯󽿤i#\u001a{􍆜(i\u0017J\u0003/s쥴?rre\u0004uf􅐷~\u0019𐀆􅾷E󿵙?-𨚮W6|A\u0019䩘@दp\u0010\r𨬆Y5\u0015p=,\u000e𨞰\rY\t𐩤\u0010𤣮r\u001b3XO􋧣!\u001d<\u001a2\u001e\t\u0006󻛌\u001b6\u000ca\u0013􇞲\"\u001dU|􄢗\u0001\u001b?5EO=4\u0010󽠋+e", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_14.json b/libs/wire-api/test/golden/testObject_Provider_provider_14.json index 3d1641cc6ae..d29399b6a8b 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_14.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_14.json @@ -1,6 +1,6 @@ { "description": "HY", - "email": "*\u001b\u001a󽅋@M/[O+", + "email": "some@example", "id": "00000006-0000-0002-0000-000400000007", "name": "uo;)𫘣/KN\u0006_#D{󼥙𬧩6X􅺉]<󿘴*%#􎁽uHJ󸢵q\u000exu󿶢􃢢󵗝􂈯􆃯M𝥑v<􋫀z覙M𗮚:t:I`Q3Vx䜽𤎉U~ᔒ\u0014`\u0003~󽇈󽮞\u0019`\"{VL툅@􋎇󿗌\u001a", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_15.json b/libs/wire-api/test/golden/testObject_Provider_provider_15.json index df8f252a809..e67b2210b5e 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_15.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_15.json @@ -1,6 +1,6 @@ { "description": "", - "email": "@𣕎\u0013&", + "email": "some@example", "id": "00000000-0000-0001-0000-000300000008", "name": "󼢁㽨\u0016;\t􀋐ln5핽z\r\"hdPTT㵨9S}oV?x>U\u001a𤋶G\u001c\u0010K긷tO􎹌i\u001d_v\\\u0018󸣸\u00118󸩚G", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_4.json b/libs/wire-api/test/golden/testObject_Provider_provider_4.json index b00cf53ab52..5070a3d537a 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_4.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_4.json @@ -1,6 +1,6 @@ { "description": "/㕐", - "email": "󱰹@'x􏑐#.U", + "email": "some@example", "id": "00000000-0000-0006-0000-000300000003", "name": "p󿾼q\u0004JO󳗥󲕦[=\u0018\u0017󳸫D\u000b|{𦯯n", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_5.json b/libs/wire-api/test/golden/testObject_Provider_provider_5.json index 341a08dcf52..7161def65d4 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_5.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_5.json @@ -1,6 +1,6 @@ { "description": "", - "email": "l\\@o:j\u0015", + "email": "some@example", "id": "00000007-0000-0003-0000-000800000005", "name": "𢅷粪#𫭶\u0015<􇕴\u001e㣕罉M􆒇~\u0006Z)x@_\u0017\u0001\u0014𣧠􁃑~b\u0008\u0016\\ꈉᡋj󽒌Cp>󼣳\u001a𞸟{=\u0010C𠵑𢛒\u001a]󴯌􏣷[釤;\u0016\u0006TpXQT|-獵󺯊0\u0018􀬉h]󷂀}􆓤DH𦠁\u000f~\u0013^", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_6.json b/libs/wire-api/test/golden/testObject_Provider_provider_6.json index dd922fd5645..6aeb112c072 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_6.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_6.json @@ -1,6 +1,6 @@ { "description": "j#G\r", - "email": "􀧊쭫4AC@5", + "email": "some@example", "id": "00000008-0000-0005-0000-000300000006", "name": "OT;/hR𔔼葙!~<󰐾𥟑FP\u0010pW.0짃f󶹪\u000f8\u0004ZIy𠿥𐁃|du#k\n2\u001b}W\u0014嶔TI5G\u0013􍞟\u0016)>&?􄰦~\n\u000c", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_7.json b/libs/wire-api/test/golden/testObject_Provider_provider_7.json index ef066b2d5c0..21272ca1dbb 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_7.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_7.json @@ -1,6 +1,6 @@ { "description": ")m", - "email": "@Z", + "email": "some@example", "id": "00000001-0000-0003-0000-000800000001", "name": "좺\u0018𬍥1\\\u001cV󲝕𐫙\u0012􌾇𥎌\u0016㰶Y\u0006}.b𧀛\t;𦋤u%0گ}f\u0002𡢔𭉲I]􇻲c", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_8.json b/libs/wire-api/test/golden/testObject_Provider_provider_8.json index c0744a404ca..650c354b334 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_8.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_8.json @@ -1,6 +1,6 @@ { "description": "𭂕2􇞫4", - "email": "\u001f?h@|^󸪛h𐚠", + "email": "some@example", "id": "00000008-0000-0007-0000-000100000004", "name": "x􍄃\u0013cQ\u0017w󸅙k}\u0017ᤎ8\u0008`폒􆗸JC곡,1\u0013^𧙓{\u0010:c\r+\u0005", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_9.json b/libs/wire-api/test/golden/testObject_Provider_provider_9.json index 5db37c2a993..fb1e4cc4556 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_9.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_9.json @@ -1,6 +1,6 @@ { "description": "", - "email": "@siw", + "email": "some@example", "id": "00000002-0000-0005-0000-000600000006", "name": "+QH􆄋2$DH\u001d𠳉oz&SQ󺪏Apl󾦣Dai𫖠`~ཝG\u001d@$i􏀹b\u001flBR\u000cIg󰢭𡵉4Pg[h\u0003\u0012􁚑4𣪢\u0003M*\\`(U&?yinFa(𪔗J,<\u00115R@󷼝\u0007AH􌩟\u0010\"𬊺𣖮\t", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_PublicSubConversation_v5_2.json b/libs/wire-api/test/golden/testObject_PublicSubConversation_v5_2.json index ac57e7e8e1b..a918c3161ba 100644 --- a/libs/wire-api/test/golden/testObject_PublicSubConversation_v5_2.json +++ b/libs/wire-api/test/golden/testObject_PublicSubConversation_v5_2.json @@ -1,6 +1,7 @@ { "cipher_suite": 1, "epoch": 0, + "epoch_timestamp": null, "group_id": "dGVzdF9ncm91cF8y", "members": [ { diff --git a/libs/wire-api/test/golden/testObject_PushRemove_1.json b/libs/wire-api/test/golden/testObject_PushRemove_1.json new file mode 100644 index 00000000000..6fdce76e9d4 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_PushRemove_1.json @@ -0,0 +1,9 @@ +{ + "token": { + "app": "j{𛂚\u0001_􈷉M", + "client": "6", + "token": "K", + "transport": "APNS_VOIP_SANDBOX" + }, + "type": "user.push-remove" +} diff --git a/libs/wire-api/test/golden/testObject_Push_1.json b/libs/wire-api/test/golden/testObject_Push_1.json new file mode 100644 index 00000000000..9680be52df8 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Push_1.json @@ -0,0 +1,13 @@ +{ + "native_include_origin": false, + "payload": [ + {} + ], + "recipients": [ + { + "clients": [], + "route": "any", + "user_id": "15441ff8-7f14-11ef-aeec-bbe21dc8a204" + } + ] +} diff --git a/libs/wire-api/test/golden/testObject_Push_2.json b/libs/wire-api/test/golden/testObject_Push_2.json new file mode 100644 index 00000000000..cc5e168b15e --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Push_2.json @@ -0,0 +1,52 @@ +{ + "connections": [ + "mempty", + "sdf", + "wire-client" + ], + "native_aps": { + "badge": true, + "loc_args": [ + "1", + "22", + "333" + ], + "loc_key": "asdf", + "sound": null + }, + "native_encrypt": false, + "native_priority": "low", + "origin": "dec9b47a-7f12-11ef-b634-6710e7ae3d33", + "origin_connection": "123", + "payload": [ + { + "bar": true, + "foo": "3" + }, + {}, + { + "growl": "foooood" + }, + { + "lunchtime": "imminent" + } + ], + "recipients": [ + { + "clients": [ + "0" + ], + "route": "direct", + "user_id": "2e18540e-7f14-11ef-9886-d3c2ff21d3d1" + }, + { + "clients": [ + "ea", + "7b" + ], + "route": "direct", + "user_id": "316924ee-7f14-11ef-b6a2-036a4f646914" + } + ], + "transient": true +} diff --git a/libs/wire-api/test/golden/testObject_QualifiedNewOtrMessage_user_10.protobuf b/libs/wire-api/test/golden/testObject_QualifiedNewOtrMessage_user_10.protobuf new file mode 100644 index 00000000000..2d80c883253 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_QualifiedNewOtrMessage_user_10.protobuf @@ -0,0 +1,11 @@ +sender { client: 8 } +recipients { domain: "79-y-r4-9.d" } +recipients { domain: "7f3.ra.9.r37.xavdz88-9vw-z" } +recipients { domain: "7g.hw9aq-1" } +recipients { domain: "8w5.g5l-7.tys" } +recipients { domain: "n.659-s.nfd" } +recipients { domain: "pc5s-p9-48-x.r8cq.ss89h" } +native_push: false +blob: "GL\006" +transient: false +ignore_all { } \ No newline at end of file diff --git a/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_1.json b/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_1.json index 1c15f0a679d..37ecc03db09 100644 --- a/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_1.json +++ b/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_1.json @@ -1,2 +1,3 @@ -{ "qualified_user_client_prekeys" : {} -} \ No newline at end of file +{ + "qualified_user_client_prekeys": {} +} diff --git a/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_2.json b/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_2.json index 1da1f90e2a6..6114c42ac45 100644 --- a/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_2.json +++ b/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_2.json @@ -1,9 +1,10 @@ -{ "qualified_user_client_prekeys" : { - "example.com" : { - "44f9c51e-0dce-4e7f-85ba-b4e5a545ce68" : { - "123456789abcef" : null - } +{ + "failed_to_list": [], + "qualified_user_client_prekeys": { + "example.com": { + "44f9c51e-0dce-4e7f-85ba-b4e5a545ce68": { + "123456789abcef": null + } + } } - } -, "failed_to_list" : [] } diff --git a/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_3.json b/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_3.json index ffd0320a305..52d00bca511 100644 --- a/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_3.json +++ b/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_3.json @@ -1,10 +1,13 @@ -{ "qualified_user_client_prekeys" : {} -, "failed_to_list" : - [ { "domain" : "example.com" - , "id" : "44f9c51e-0dce-4e7f-85ba-b4e5a545ce68" - } - , { "domain" : "test.net" - , "id" : "284c4e8f-78ef-43f4-a77a-015c22e37960" - } - ] -} \ No newline at end of file +{ + "failed_to_list": [ + { + "domain": "example.com", + "id": "44f9c51e-0dce-4e7f-85ba-b4e5a545ce68" + }, + { + "domain": "test.net", + "id": "284c4e8f-78ef-43f4-a77a-015c22e37960" + } + ], + "qualified_user_client_prekeys": {} +} diff --git a/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMap_2.json b/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMap_2.json index 68dd80f79b4..3d60acebf01 100644 --- a/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMap_2.json +++ b/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMap_2.json @@ -161,6 +161,7 @@ } } }, + "meta.example.com": {}, "theta.example.com": {}, "zeta.example.com": { "0000004d-0000-001f-0000-006300000073": { diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClientV0_team_1.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClientV0_team_1.json new file mode 100644 index 00000000000..520fd99c869 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClientV0_team_1.json @@ -0,0 +1,4 @@ +{ + "team_id": "0000002e-0000-006e-0000-004a0000001b", + "user_id": "0000003d-0000-0049-0000-003b00000055" +} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClientV0_team_2.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClientV0_team_2.json new file mode 100644 index 00000000000..e38364be589 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClientV0_team_2.json @@ -0,0 +1,4 @@ +{ + "team_id": "00000049-0000-0059-0000-004e0000001f", + "user_id": "0000001c-0000-0064-0000-003a0000000b" +} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_1.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_1.json index 520fd99c869..d6d5854d098 100644 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_1.json +++ b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_1.json @@ -1,4 +1,7 @@ { - "team_id": "0000002e-0000-006e-0000-004a0000001b", - "user_id": "0000003d-0000-0049-0000-003b00000055" + "qualified_user_id": { + "domain": "example.com", + "id": "0000007d-0000-0054-0000-000900000018" + }, + "team_id": "0000005d-0000-001f-0000-006300000019" } diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_10.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_10.json deleted file mode 100644 index ec0971407a0..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_10.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000029-0000-003f-0000-004d00000076", - "user_id": "00000063-0000-004c-0000-00730000000a" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_11.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_11.json deleted file mode 100644 index 6c1111da5f4..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_11.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000025-0000-005e-0000-00800000007b", - "user_id": "00000006-0000-0058-0000-005500000045" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_12.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_12.json deleted file mode 100644 index 91d9ffa5a64..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_12.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000005e-0000-0005-0000-007900000008", - "user_id": "00000019-0000-0066-0000-003e0000005b" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_13.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_13.json deleted file mode 100644 index cdb55dc5a51..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_13.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000000f-0000-007b-0000-00390000005b", - "user_id": "00000007-0000-0024-0000-005700000006" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_14.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_14.json deleted file mode 100644 index 8628292655b..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_14.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000002d-0000-0028-0000-004500000077", - "user_id": "00000004-0000-0007-0000-003500000079" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_15.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_15.json deleted file mode 100644 index e50c3f74ae6..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_15.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000005f-0000-0072-0000-005a00000009", - "user_id": "00000001-0000-002b-0000-001900000031" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_16.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_16.json deleted file mode 100644 index b6ad4023017..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_16.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000070-0000-0020-0000-004d00000058", - "user_id": "00000073-0000-006d-0000-006100000043" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_17.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_17.json deleted file mode 100644 index 979de7c1e16..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_17.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000059-0000-001e-0000-005b00000033", - "user_id": "0000003b-0000-006c-0000-006e00000048" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_18.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_18.json deleted file mode 100644 index 067f03d5829..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_18.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000002c-0000-0017-0000-002d00000008", - "user_id": "0000004a-0000-000e-0000-005900000065" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_19.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_19.json deleted file mode 100644 index e6a872b49be..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_19.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000078-0000-005c-0000-004900000023", - "user_id": "00000024-0000-006b-0000-006000000011" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_2.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_2.json index e38364be589..eb30cd98300 100644 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_2.json +++ b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_2.json @@ -1,4 +1,7 @@ { - "team_id": "00000049-0000-0059-0000-004e0000001f", - "user_id": "0000001c-0000-0064-0000-003a0000000b" + "qualified_user_id": { + "domain": "example.com", + "id": "00000025-0000-0077-0000-002d00000045" + }, + "team_id": "0000001a-0000-002c-0000-004e0000005c" } diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_20.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_20.json deleted file mode 100644 index 44512e4c9da..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_20.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000020-0000-0044-0000-002200000020", - "user_id": "00000059-0000-003b-0000-00410000006c" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_3.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_3.json deleted file mode 100644 index 7ad5e1808ab..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_3.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000005d-0000-001f-0000-006300000019", - "user_id": "0000007d-0000-0054-0000-000900000018" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_4.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_4.json deleted file mode 100644 index f5c4b003ee2..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_4.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000001a-0000-002c-0000-004e0000005c", - "user_id": "00000025-0000-0077-0000-002d00000045" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_5.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_5.json deleted file mode 100644 index ad84d5ccce2..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_5.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000000c-0000-0003-0000-00750000006f", - "user_id": "00000066-0000-0055-0000-007f0000002f" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_6.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_6.json deleted file mode 100644 index d6893b562e2..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_6.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000018-0000-0074-0000-004800000077", - "user_id": "0000005c-0000-0039-0000-007b0000005d" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_7.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_7.json deleted file mode 100644 index 6880ebd7819..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_7.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000077-0000-005f-0000-00290000006e", - "user_id": "0000000d-0000-0057-0000-00270000003b" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_8.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_8.json deleted file mode 100644 index 64ff30b8973..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_8.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000064-0000-0008-0000-004400000064", - "user_id": "00000033-0000-0004-0000-00670000003f" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_9.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_9.json deleted file mode 100644 index a9ef3873622..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_9.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000005-0000-0079-0000-003300000036", - "user_id": "00000007-0000-0062-0000-006600000015" -} 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 539ec4765c9..027adc7fe7d 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 @@ -3,7 +3,7 @@ { "accent_id": 0, "created_at": "1864-05-09T20:48:17.263Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": null, "id": "00000001-0000-0001-0000-000100000000", @@ -18,7 +18,7 @@ { "accent_id": 0, "created_at": "1864-05-09T17:17:18.225Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0000-0000-000100000000", 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 62473fd9b9f..af0345204ac 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 @@ -18,7 +18,7 @@ { "accent_id": 0, "created_at": "1864-05-09T09:36:08.567Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": null, "id": "00000001-0000-0000-0000-000000000001", @@ -33,7 +33,7 @@ { "accent_id": null, "created_at": "1864-05-09T11:56:16.082Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0000-0000-000100000000", @@ -63,7 +63,7 @@ { "accent_id": 0, "created_at": null, - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0001-0000-000000000000", @@ -78,7 +78,7 @@ { "accent_id": null, "created_at": "1864-05-09T02:39:28.838Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0001-0000-000100000001", @@ -93,7 +93,7 @@ { "accent_id": 0, "created_at": null, - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0001-0000-000100000001", @@ -108,7 +108,7 @@ { "accent_id": 0, "created_at": "1864-05-09T01:15:59.694Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0001-0000-000000000001", 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 ffd2a8ea9db..3c1d41e23af 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 @@ -3,7 +3,7 @@ { "accent_id": 0, "created_at": "1864-05-09T06:59:36.374Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0000-0000-000000000001", 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 230deefba01..5616b3ee0bb 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 @@ -3,7 +3,7 @@ { "accent_id": 0, "created_at": "1864-05-09T17:55:15.951Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0001-0000-000100000001", @@ -18,7 +18,7 @@ { "accent_id": null, "created_at": "1864-05-09T05:08:55.558Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": null, "id": "00000001-0000-0000-0000-000000000001", @@ -33,7 +33,7 @@ { "accent_id": 0, "created_at": "1864-05-09T11:18:47.121Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0001-0000-000100000001", 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 9f98be5bfc3..c903b0f64dd 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 @@ -3,7 +3,7 @@ { "accent_id": null, "created_at": null, - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0000-0000-000100000001", 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 926331afd80..5c4e87f3ca2 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 @@ -3,7 +3,7 @@ { "accent_id": 0, "created_at": "1864-05-09T23:38:23.560Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0000-0000-000000000001", @@ -21,7 +21,7 @@ { "accent_id": null, "created_at": null, - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0001-0000-000000000000", @@ -36,7 +36,7 @@ { "accent_id": 0, "created_at": "1864-05-09T18:46:45.154Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0000-0000-000000000000", 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 76aba23d150..a38d13d0654 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 @@ -3,7 +3,7 @@ { "accent_id": null, "created_at": "1864-05-09T12:35:16.437Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0000-0000-000000000000", 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 294ff7981f9..e08a9b2e2fe 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 @@ -3,8 +3,8 @@ { "accent_id": null, "created_at": null, - "email": "@", - "email_unvalidated": "foobar@example.com", + "email": "some@example", + "email_unvalidated": "some@example", "handle": null, "id": "00000000-0000-0000-0000-000000000001", "managed_by": "wire", @@ -57,7 +57,7 @@ { "accent_id": 0, "created_at": null, - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0000-0000-000100000000", 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 531cb1926ec..aadc9a62d95 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 @@ -3,7 +3,7 @@ { "accent_id": 0, "created_at": "1864-05-09T12:39:20.984Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0000-0000-000100000000", 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 9be9dd54244..dbfdd882e1e 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 @@ -18,8 +18,8 @@ { "accent_id": 0, "created_at": null, - "email": "@", - "email_unvalidated": "foobar@example.com", + "email": "some@example", + "email_unvalidated": "some@example", "handle": "", "id": "00000001-0000-0001-0000-000100000001", "managed_by": "wire", @@ -33,7 +33,7 @@ { "accent_id": null, "created_at": "1864-05-09T10:59:12.538Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0000-0000-000100000001", @@ -48,7 +48,7 @@ { "accent_id": 0, "created_at": "1864-05-09T23:24:12.000Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0000-0000-000000000000", @@ -63,7 +63,7 @@ { "accent_id": 0, "created_at": null, - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0001-0000-000000000001", @@ -111,8 +111,8 @@ { "accent_id": 0, "created_at": "1864-05-09T01:45:42.970Z", - "email": "@", - "email_unvalidated": "foobar@example.com", + "email": "some@example", + "email_unvalidated": "some@example", "handle": null, "id": "00000001-0000-0001-0000-000000000000", "managed_by": null, @@ -130,7 +130,7 @@ "accent_id": null, "created_at": null, "email": null, - "email_unvalidated": "foobar@example.com", + "email_unvalidated": "some@example", "handle": "", "id": "00000001-0000-0000-0000-000100000000", "managed_by": "wire", @@ -144,8 +144,8 @@ { "accent_id": 0, "created_at": "1864-05-09T23:36:06.671Z", - "email": "@", - "email_unvalidated": "foobar@example.com", + "email": "some@example", + "email_unvalidated": "some@example", "handle": "", "id": "00000000-0000-0000-0000-000000000001", "managed_by": "scim", @@ -160,7 +160,7 @@ "accent_id": 0, "created_at": "1864-05-09T14:01:50.906Z", "email": null, - "email_unvalidated": "foobar@example.com", + "email_unvalidated": "some@example", "handle": "", "id": "00000000-0000-0000-0000-000100000001", "managed_by": "scim", @@ -174,7 +174,7 @@ { "accent_id": 0, "created_at": null, - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0000-0000-000100000000", @@ -192,7 +192,7 @@ { "accent_id": 0, "created_at": null, - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0000-0000-000000000001", 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 eadcd2b4997..8c36301dac5 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 @@ -3,7 +3,7 @@ { "accent_id": null, "created_at": "1864-05-09T19:22:39.660Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0000-0000-000100000001", @@ -33,8 +33,8 @@ { "accent_id": 0, "created_at": null, - "email": "@", - "email_unvalidated": "foobar@example.com", + "email": "some@example", + "email_unvalidated": "some@example", "handle": "", "id": "00000001-0000-0000-0000-000000000000", "managed_by": "wire", @@ -49,7 +49,7 @@ "accent_id": 0, "created_at": null, "email": null, - "email_unvalidated": "foobar@example.com", + "email_unvalidated": "some@example", "handle": "", "id": "00000001-0000-0001-0000-000000000001", "managed_by": "wire", @@ -64,7 +64,7 @@ "accent_id": 0, "created_at": "1864-05-09T00:45:08.016Z", "email": null, - "email_unvalidated": "foobar@example.com", + "email_unvalidated": "some@example", "handle": "", "id": "00000000-0000-0001-0000-000100000001", "managed_by": "scim", 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 4857e4d39f8..f1bc59bcaae 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 @@ -3,7 +3,7 @@ { "accent_id": null, "created_at": null, - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0000-0000-000100000001", @@ -18,7 +18,7 @@ { "accent_id": 0, "created_at": "1864-05-09T13:46:22.701Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": null, "id": "00000000-0000-0000-0000-000000000000", @@ -33,7 +33,7 @@ { "accent_id": 0, "created_at": "1864-05-09T09:25:11.685Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0000-0000-000000000000", @@ -48,7 +48,7 @@ { "accent_id": 0, "created_at": "1864-05-09T11:37:20.763Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0001-0000-000000000000", 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 b3d92014d44..bb8d4a58d9e 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 @@ -18,7 +18,7 @@ { "accent_id": 0, "created_at": "1864-05-09T16:22:05.429Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0000-0000-000100000001", 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 953ed882d45..a3a2b2be525 100644 --- a/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json +++ b/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json @@ -1,7 +1,7 @@ { "accent_id": 1, "assets": [], - "email": "\u0007@", + "email": "some@example", "expires_at": "1864-05-07T21:09:29.342Z", "handle": "do9-5", "id": "00000001-0000-0000-0000-000000000002", @@ -17,8 +17,10 @@ "id": "00000000-0000-0001-0000-000000000000", "provider": "00000000-0000-0001-0000-000000000001" }, + "status": "active", "supported_protocols": [ "proteus" ], - "team": "00000001-0000-0002-0000-000000000002" + "team": "00000001-0000-0002-0000-000000000002", + "text_status": "text status" } diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_1.json b/libs/wire-api/test/golden/testObject_SendActivationCode_1.json new file mode 100644 index 00000000000..1144f68d89c --- /dev/null +++ b/libs/wire-api/test/golden/testObject_SendActivationCode_1.json @@ -0,0 +1,3 @@ +{ + "email": "some@example" +} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_2.json b/libs/wire-api/test/golden/testObject_SendActivationCode_2.json new file mode 100644 index 00000000000..f4224036325 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_SendActivationCode_2.json @@ -0,0 +1,4 @@ +{ + "email": "some@example", + "locale": "cu-VI" +} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_user_1.json b/libs/wire-api/test/golden/testObject_SendActivationCode_user_1.json deleted file mode 100644 index a39d00e47d9..00000000000 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_user_1.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "locale": "cu-VI", - "phone": "+77566129334842", - "voice_call": false -} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_user_10.json b/libs/wire-api/test/golden/testObject_SendActivationCode_user_10.json deleted file mode 100644 index e795282853d..00000000000 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_user_10.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "email": "\t󽿸\u00002𧎖t𥓵@^s", - "voice_call": true -} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_user_11.json b/libs/wire-api/test/golden/testObject_SendActivationCode_user_11.json deleted file mode 100644 index e04d0ceeb17..00000000000 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_user_11.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": "rT@a\tL\u0014", - "locale": "hy-BB", - "voice_call": false -} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_user_12.json b/libs/wire-api/test/golden/testObject_SendActivationCode_user_12.json deleted file mode 100644 index 0a19921bde4..00000000000 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_user_12.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "locale": "ve-MU", - "phone": "+6599921229041", - "voice_call": true -} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_user_13.json b/libs/wire-api/test/golden/testObject_SendActivationCode_user_13.json deleted file mode 100644 index d2eabe6a707..00000000000 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_user_13.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "locale": "kk", - "phone": "+260369295110", - "voice_call": false -} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_user_14.json b/libs/wire-api/test/golden/testObject_SendActivationCode_user_14.json deleted file mode 100644 index 18965585b6e..00000000000 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_user_14.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": "B;b𨈅\u0011\u000fHm\u0013{@?犯Jd\u000c", - "locale": "kw-PM", - "voice_call": false -} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_user_15.json b/libs/wire-api/test/golden/testObject_SendActivationCode_user_15.json deleted file mode 100644 index baa6bade271..00000000000 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_user_15.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": "󺌼\u0011@t=𑁦꓂󼃹,AG2", - "locale": "iu-FR", - "voice_call": false -} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_user_16.json b/libs/wire-api/test/golden/testObject_SendActivationCode_user_16.json deleted file mode 100644 index b8fa488f950..00000000000 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_user_16.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": "O_酛󹰔^t@", - "locale": "fi", - "voice_call": true -} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_user_17.json b/libs/wire-api/test/golden/testObject_SendActivationCode_user_17.json deleted file mode 100644 index 60571ae2c55..00000000000 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_user_17.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": "T\u000bI9H}C\u0002\u000e󸠬@𤾡諣=N", - "locale": "pa-AO", - "voice_call": true -} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_user_18.json b/libs/wire-api/test/golden/testObject_SendActivationCode_user_18.json deleted file mode 100644 index 3257e9ba67b..00000000000 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_user_18.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "locale": "tg", - "phone": "+715068856505655", - "voice_call": true -} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_user_19.json b/libs/wire-api/test/golden/testObject_SendActivationCode_user_19.json deleted file mode 100644 index 0e4d72c8efa..00000000000 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_user_19.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "locale": "gd-FI", - "phone": "+22888251856", - "voice_call": true -} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_user_2.json b/libs/wire-api/test/golden/testObject_SendActivationCode_user_2.json deleted file mode 100644 index 5b837631353..00000000000 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_user_2.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": "󹛃@nK", - "locale": "de", - "voice_call": false -} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_user_20.json b/libs/wire-api/test/golden/testObject_SendActivationCode_user_20.json deleted file mode 100644 index d0eaf2f61a7..00000000000 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_user_20.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "phone": "+8943652812", - "voice_call": true -} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_user_3.json b/libs/wire-api/test/golden/testObject_SendActivationCode_user_3.json deleted file mode 100644 index b8458051991..00000000000 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_user_3.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": "#\u0006􍖄l􅋋F𤀞@-\u00132􌳵1\u0012󷮾𨿲\u000e𐨵", - "locale": "gn-VU", - "voice_call": true -} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_user_4.json b/libs/wire-api/test/golden/testObject_SendActivationCode_user_4.json deleted file mode 100644 index a8e10dca5f2..00000000000 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_user_4.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "email": "b@4M􆳤P𤣛$[\u0012j", - "voice_call": false -} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_user_5.json b/libs/wire-api/test/golden/testObject_SendActivationCode_user_5.json deleted file mode 100644 index 8c1b83e463e..00000000000 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_user_5.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "email": "test@example.com", - "voice_call": false -} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_user_6.json b/libs/wire-api/test/golden/testObject_SendActivationCode_user_6.json deleted file mode 100644 index 5bbfc19425c..00000000000 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_user_6.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "locale": "dv-IN", - "phone": "+38093636958", - "voice_call": false -} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_user_7.json b/libs/wire-api/test/golden/testObject_SendActivationCode_user_7.json deleted file mode 100644 index 0dc4689fc29..00000000000 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_user_7.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "email": "B+l􁕧􈌤@\u0007%", - "voice_call": true -} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_user_8.json b/libs/wire-api/test/golden/testObject_SendActivationCode_user_8.json deleted file mode 100644 index 6cd81e432ad..00000000000 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_user_8.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": "\u00003@g𦅞⺖Ea?\u0012\\\u0014", - "locale": "ho", - "voice_call": true -} diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_user_9.json b/libs/wire-api/test/golden/testObject_SendActivationCode_user_9.json deleted file mode 100644 index 6ddc3167e02..00000000000 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_user_9.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "email": "Rn\u0002v@(\u0000N", - "voice_call": false -} 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 c7d636499aa..100e28b6e58 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_1.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_1.json @@ -1,7 +1,7 @@ { "accent_id": null, "created_at": "1864-05-11T12:52:22.086Z", - "email": "({@q", + "email": "some@example", "email_unvalidated": null, "handle": null, "id": "00000000-0000-0001-0000-000200000001", 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 231eca48c3c..5ea5a511e1c 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_11.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_11.json @@ -1,7 +1,7 @@ { "accent_id": -3, "created_at": null, - "email": "m@𬯅", + "email": "some@example", "email_unvalidated": null, "handle": null, "id": "00000000-0000-0000-0000-000000000002", 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 bfa2dc5b6c7..5c355ec41d7 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_12.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_12.json @@ -1,7 +1,7 @@ { "accent_id": null, "created_at": "1864-05-06T13:09:44.601Z", - "email": "@(-", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000002-0000-0000-0000-000200000000", 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 a54789ad3f7..ea2a4b7b2d6 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_13.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_13.json @@ -1,7 +1,7 @@ { "accent_id": 0, "created_at": null, - "email": "\u0001㗅@_C", + "email": "some@example", "email_unvalidated": null, "handle": "S", "id": "00000002-0000-0002-0000-000100000001", 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 f7211944180..1522c9b809c 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_14.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_14.json @@ -1,7 +1,7 @@ { "accent_id": -3, "created_at": "1864-05-08T20:31:37.388Z", - "email": "4)=@I\u0010", + "email": "some@example", "email_unvalidated": null, "handle": "\"\u001f\u0014", "id": "00000001-0000-0001-0000-000100000000", 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 0fada5ea3a4..8189e7c0912 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_15.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_15.json @@ -1,8 +1,8 @@ { "accent_id": null, "created_at": "1864-05-11T14:15:19.890Z", - "email": "9L@(", - "email_unvalidated": "foobar@example.com", + "email": "some@example", + "email_unvalidated": "some@example", "handle": "J", "id": "00000002-0000-0002-0000-000100000002", "managed_by": null, 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 08f6d920126..906af84cc8f 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_16.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_16.json @@ -1,8 +1,8 @@ { "accent_id": -1, "created_at": "1864-05-08T15:43:05.866Z", - "email": "@j", - "email_unvalidated": "foobar@example.com", + "email": "some@example", + "email_unvalidated": "some@example", "handle": null, "id": "00000001-0000-0000-0000-000000000002", "managed_by": "wire", 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 aa3c6db73e2..421e450a4a0 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_17.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_17.json @@ -1,7 +1,7 @@ { "accent_id": -3, "created_at": "1864-05-10T20:50:28.410Z", - "email": "X󵿆@D(0", + "email": "some@example", "email_unvalidated": null, "handle": null, "id": "00000000-0000-0001-0000-000200000001", 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 eeeb205a10f..8e8d71fc661 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_18.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_18.json @@ -1,7 +1,7 @@ { "accent_id": 3, "created_at": null, - "email": "􎲮]L@屰", + "email": "some@example", "email_unvalidated": null, "handle": null, "id": "00000002-0000-0001-0000-000000000002", 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 14b5de5a6b8..e3320f0d3b6 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_19.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_19.json @@ -1,7 +1,7 @@ { "accent_id": -3, "created_at": "1864-05-10T11:20:36.673Z", - "email": "N@", + "email": "some@example", "email_unvalidated": null, "handle": null, "id": "00000001-0000-0002-0000-000200000002", 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 06b1c2db6b4..ecfd6aad83e 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_2.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_2.json @@ -1,7 +1,7 @@ { "accent_id": 2, "created_at": "1864-05-08T03:35:20.125Z", - "email": "\u000f5g@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0001-0000-000000000002", 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 9985b7e57d2..6b57101d2a2 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_20.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_20.json @@ -1,8 +1,8 @@ { "accent_id": -3, "created_at": "1864-05-06T18:23:32.240Z", - "email": ". +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} module Test.Wire.API.Password where import Control.Concurrent.Async +import Crypto.KDF.Argon2 qualified as Argon2 import Data.Misc import Imports import Test.Tasty @@ -29,13 +31,27 @@ tests = testGroup "Password" $ [ testCase "hash password argon2id" testHashPasswordArgon2id, testCase "update pwd hash" testUpdateHash, - testCase "verify old scrypt password still works" testHashingOldScrypt + testCase "verify old scrypt password still works" testHashingOldScrypt, + testCase "test hash scrypt" testHashPasswordScrypt ] +defaultOptions :: Argon2.Options +defaultOptions = + let hashParallelism = 4 + in Argon2.Options + { variant = Argon2.Argon2id, + version = Argon2.Version13, + iterations = 1, + parallelism = hashParallelism, + -- This needs to be min 8 * hashParallelism, otherewise we get an + -- unsafe error + memory = 8 * hashParallelism + } + testHashPasswordScrypt :: IO () testHashPasswordScrypt = do pwd <- genPassword - hashed <- mkSafePasswordScrypt pwd + hashed <- mkSafePassword defaultOptions pwd let (correct, status) = verifyPasswordWithStatus pwd hashed assertBool "Password could not be verified" correct assertEqual "Password could not be verified" status PasswordStatusOk @@ -43,21 +59,21 @@ testHashPasswordScrypt = do testHashPasswordArgon2id :: IO () testHashPasswordArgon2id = do pwd <- genPassword - hashed <- mkSafePasswordArgon2id pwd + hashed <- mkSafePassword defaultOptions pwd let (correct, status) = verifyPasswordWithStatus pwd hashed - assertBool "Password could not be verified" correct assertEqual "Password could not be verified" status PasswordStatusOk + assertBool "Password could not be verified" correct testUpdateHash :: IO () testUpdateHash = do let orig = plainTextPassword8Unsafe "Test password scrypt to argon2id." -- password hashed with scrypt and random salt - expected = unsafeMkPassword "14|8|1|ktYx5i1DMOEfm+tXpw9i7ZVPdeqbxgxYxUbmDVLSAzQ=|Fzy0sNfXQQnJW98ncyN51PUChFWH1tpVJCxjz5JRZEReVa0//zJ6MeopiEh84Ny8lzwdvRPHDqnSS/lkPEB7Ow==" + Right expected = parsePassword "14|8|1|ktYx5i1DMOEfm+tXpw9i7ZVPdeqbxgxYxUbmDVLSAzQ=|Fzy0sNfXQQnJW98ncyN51PUChFWH1tpVJCxjz5JRZEReVa0//zJ6MeopiEh84Ny8lzwdvRPHDqnSS/lkPEB7Ow==" -- password re-hashed with argon2id and re-used salt for simplicity - newHash = unsafeMkPassword "$argon2id$v=19$m=131072,t=5,p=4$ktYx5i1DMOEfm+tXpw9i7ZVPdeqbxgxYxUbmDVLSAzQ=$iS/9tVk49W8bO/APETqNzMmREerdETTvSXcA7nSpqrsGrV1N33+MVaKnhWhBHqIxM92HFPsV5GP0dpgCUHmJRg==" -- verify password with scrypt (correct, status) = verifyPasswordWithStatus orig expected + newHash <- either assertFailure pure $ parsePassword "$argon2id$v=19$m=4194304,t=1,p=8$lj6+HdIcCpO1zvz8An56fg$Qx8OzYTq0hDNqGG9tW1dug" assertBool "Password did not match hash." correct assertEqual "Password could not be verified" status PasswordStatusNeedsUpdate @@ -70,7 +86,7 @@ testHashingOldScrypt :: IO () testHashingOldScrypt = forConcurrently_ pwds $ \pwd -> do let orig = plainTextPassword8Unsafe (fst pwd) - expected = unsafeMkPassword (snd pwd) + Right expected = parsePassword (snd pwd) (correct, status) = verifyPasswordWithStatus orig expected assertBool "Password did not match hash." correct assertEqual "Password could not be verified" status PasswordStatusNeedsUpdate 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 bec9d3c96f1..ee312a10edf 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 @@ -203,10 +203,11 @@ tests = testRoundTrip @Push.Token.PushToken, testRoundTrip @Push.Token.PushTokenList, testRoundTrip @Scim.CreateScimToken, + testRoundTrip @Scim.CreateScimTokenResponse, testRoundTrip @SystemSettings.SystemSettings, testRoundTrip @SystemSettings.SystemSettingsPublic, testRoundTrip @SystemSettings.SystemSettingsInternal, - testRoundTrip @Team.BindingNewTeam, + testRoundTrip @Team.NewTeam, testRoundTrip @Team.TeamBinding, testRoundTrip @Team.Team, testRoundTrip @Team.TeamList, @@ -214,11 +215,11 @@ tests = testRoundTrip @Team.TeamDeleteData, testRoundTrip @Team.Conversation.TeamConversation, testRoundTrip @Team.Conversation.TeamConversationList, - testRoundTrip @(Team.Feature.WithStatus Team.Feature.LegalholdConfig), - testRoundTrip @(Team.Feature.WithStatusPatch Team.Feature.LegalholdConfig), - testRoundTrip @(Team.Feature.WithStatusPatch Team.Feature.SelfDeletingMessagesConfig), - testRoundTrip @(Team.Feature.WithStatusNoLock Team.Feature.LegalholdConfig), - testRoundTrip @Team.Feature.AllFeatureConfigs, + testRoundTrip @(Team.Feature.LockableFeature Team.Feature.LegalholdConfig), + testRoundTrip @(Team.Feature.LockableFeaturePatch Team.Feature.LegalholdConfig), + testRoundTrip @(Team.Feature.LockableFeaturePatch Team.Feature.SelfDeletingMessagesConfig), + testRoundTrip @(Team.Feature.Feature Team.Feature.LegalholdConfig), + testRoundTrip @Team.Feature.AllTeamFeatures, testRoundTrip @Team.Feature.FeatureStatus, testRoundTrip @Team.Feature.LockStatus, testRoundTrip @Team.Invitation.InvitationRequest, @@ -231,9 +232,12 @@ tests = testRoundTrip @Team.LegalHold.RemoveLegalHoldSettingsRequest, testRoundTrip @Team.LegalHold.DisableLegalHoldForUserRequest, testRoundTrip @Team.LegalHold.ApproveLegalHoldForUserRequest, + testRoundTrip @Team.LegalHold.External.RequestNewLegalHoldClientV0, testRoundTrip @Team.LegalHold.External.RequestNewLegalHoldClient, testRoundTrip @Team.LegalHold.External.NewLegalHoldClient, + testRoundTrip @Team.LegalHold.External.LegalHoldServiceConfirmV0, testRoundTrip @Team.LegalHold.External.LegalHoldServiceConfirm, + testRoundTrip @Team.LegalHold.External.LegalHoldServiceRemoveV0, testRoundTrip @Team.LegalHold.External.LegalHoldServiceRemove, testRoundTrip @Team.LegalHold.LegalholdProtectee, testRoundTrip @Team.Member.TeamMember, @@ -312,7 +316,7 @@ tests = testRoundTrip @User.Client.Prekey.PrekeyBundle, testRoundTrip @User.Handle.UserHandleInfo, testRoundTrip @User.Handle.CheckHandles, - testRoundTrip @User.Identity.Email, + testRoundTrip @User.Identity.EmailAddress, testRoundTrip @User.Identity.Phone, testRoundTrip @User.Identity.UserSSOId, testRoundTrip @User.Password.NewPasswordReset, diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/ByteString.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/ByteString.hs index 2ecbfa31def..c021bba4c9d 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/ByteString.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/ByteString.hs @@ -70,7 +70,7 @@ tests = testRoundTrip @User.Activation.ActivationCode, testRoundTrip @User.Activation.ActivationKey, testRoundTrip @User.Auth.CookieLabel, - testRoundTrip @User.Identity.Email, + testRoundTrip @User.Identity.EmailAddress, testRoundTrip @User.Identity.Phone, testRoundTrip @User.InvitationCode, testRoundTrip @User.Password.PasswordResetCode, diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/CSV.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/CSV.hs index 3844143e128..477b154cf67 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/CSV.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/CSV.hs @@ -19,17 +19,32 @@ module Test.Wire.API.Roundtrip.CSV where import Control.Arrow ((>>>)) import Data.Csv +import Data.Time.Clock import Data.Vector qualified as V import Imports import Test.Tasty qualified as T -import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty, (===)) +import Test.Tasty.QuickCheck import Type.Reflection (typeRep) -import Wire.API.Team.Export qualified as Team.Export +import Wire.API.Team.Export + +newtype ValidTeamExportUser = ValidTeamExportUser + {unValidTeamExportUser :: TeamExportUser} + deriving newtype (FromNamedRecord, ToNamedRecord, DefaultOrdered, Eq, Show) + +instance Arbitrary ValidTeamExportUser where + arbitrary = do + u <- arbitrary + let resetTime (UTCTime d _) = UTCTime d 0 + pure $ + ValidTeamExportUser + u + { tExportLastActive = fmap resetTime (tExportLastActive u) + } tests :: T.TestTree tests = T.localOption (T.Timeout (60 * 1000000) "60s") . T.testGroup "CSV roundtrip tests" $ - [testRoundTrip @Team.Export.TeamExportUser] + [testRoundTrip @ValidTeamExportUser] testRoundTrip :: forall a. diff --git a/libs/wire-api/test/unit/Test/Wire/API/Run.hs b/libs/wire-api/test/unit/Test/Wire/API/Run.hs index 417d543e0e4..5301f44cdc9 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Run.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Run.hs @@ -37,7 +37,6 @@ import Test.Wire.API.Routes.Version qualified as Routes.Version import Test.Wire.API.Routes.Version.Wai qualified as Routes.Version.Wai import Test.Wire.API.Swagger qualified as Swagger import Test.Wire.API.Team.Export qualified as Team.Export -import Test.Wire.API.Team.Feature qualified as Team.Feature import Test.Wire.API.Team.Member qualified as Team.Member import Test.Wire.API.User qualified as User import Test.Wire.API.User.Auth qualified as User.Auth @@ -70,6 +69,5 @@ main = unsafePerformIO Routes.Version.Wai.tests, RawJson.tests, OAuth.tests, - Password.tests, - Team.Feature.tests + Password.tests ] diff --git a/libs/wire-api/test/unit/Test/Wire/API/Team/Feature.hs b/libs/wire-api/test/unit/Test/Wire/API/Team/Feature.hs deleted file mode 100644 index 60b634c9d17..00000000000 --- a/libs/wire-api/test/unit/Test/Wire/API/Team/Feature.hs +++ /dev/null @@ -1,92 +0,0 @@ -{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2024 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Test.Wire.API.Team.Feature (tests) where - -import Imports -import Test.Tasty -import Test.Tasty.HUnit -import Wire.API.Team.Feature - -tests :: TestTree -tests = - testGroup - "Wire.API.Team.Feature" - [ testCase "no lock status in DB" testComputeFeatureConfigForTeamUserLsIsNothing, - testCase "feature is locked in DB" testComputeFeatureConfigForTeamUserLocked, - testCase "feature is unlocked in DB but has no feature status" testComputeFeatureConfigForTeamUserUnlocked, - testCase "feature is unlocked in DB and has feature status" testComputeFeatureConfigForTeamWithDbStatus - ] - -testComputeFeatureConfigForTeamUserLsIsNothing :: Assertion -testComputeFeatureConfigForTeamUserLsIsNothing = do - let mStatusDb = undefined - let mLockStatusDb = Nothing - let defStatus = - withStatus - FeatureStatusEnabled - LockStatusLocked - ExposeInvitationURLsToTeamAdminConfig - FeatureTTLUnlimited - let expected = defStatus - let actual = computeFeatureConfigForTeamUser @ExposeInvitationURLsToTeamAdminConfig mStatusDb mLockStatusDb defStatus - actual @?= expected - -testComputeFeatureConfigForTeamUserLocked :: Assertion -testComputeFeatureConfigForTeamUserLocked = do - let mStatusDb = undefined - let mLockStatusDb = Just LockStatusLocked - let defStatus = - withStatus - FeatureStatusEnabled - LockStatusLocked - ExposeInvitationURLsToTeamAdminConfig - FeatureTTLUnlimited - let expected = defStatus - let actual = computeFeatureConfigForTeamUser @ExposeInvitationURLsToTeamAdminConfig mStatusDb mLockStatusDb defStatus - actual @?= expected - -testComputeFeatureConfigForTeamUserUnlocked :: Assertion -testComputeFeatureConfigForTeamUserUnlocked = do - let mStatusDb = Nothing - let mLockStatusDb = Just LockStatusUnlocked - let defStatus = - withStatus - FeatureStatusEnabled - LockStatusLocked - ExposeInvitationURLsToTeamAdminConfig - FeatureTTLUnlimited - let expected = defStatus & setLockStatus LockStatusUnlocked - let actual = computeFeatureConfigForTeamUser @ExposeInvitationURLsToTeamAdminConfig mStatusDb mLockStatusDb defStatus - actual @?= expected - -testComputeFeatureConfigForTeamWithDbStatus :: Assertion -testComputeFeatureConfigForTeamWithDbStatus = do - let mStatusDb = - Just . forgetLock $ - withStatus - FeatureStatusDisabled - LockStatusUnlocked - ExposeInvitationURLsToTeamAdminConfig - FeatureTTLUnlimited - let mLockStatusDb = Just LockStatusUnlocked - let defStatus = undefined - let (Just expected) = withUnlocked <$> mStatusDb - let actual = computeFeatureConfigForTeamUser @ExposeInvitationURLsToTeamAdminConfig mStatusDb mLockStatusDb defStatus - actual @?= expected diff --git a/libs/wire-api/test/unit/Test/Wire/API/Team/Member.hs b/libs/wire-api/test/unit/Test/Wire/API/Team/Member.hs index 8a44da25f23..9795ac54f6c 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Team/Member.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Team/Member.hs @@ -21,7 +21,6 @@ module Test.Wire.API.Team.Member (tests) where -import Control.Lens ((^.)) import Data.Aeson import Data.Set (isSubsetOf) import Data.Set qualified as Set @@ -57,8 +56,8 @@ permissionTests = -- now it's true, and it's nice to have that written down somewhere. forM_ [(r1, r2) | r1 <- [minBound ..], r2 <- drop 1 [r1 ..]] $ \(r1, r2) -> do - assertBool "owner.self" ((rolePermissions r2 ^. self) `isSubsetOf` (rolePermissions r1 ^. self)) - assertBool "owner.copy" ((rolePermissions r2 ^. copy) `isSubsetOf` (rolePermissions r1 ^. copy)), + assertBool "owner.self" (((rolePermissions r2).self) `isSubsetOf` ((rolePermissions r1).self)) + assertBool "owner.copy" (((rolePermissions r2).copy) `isSubsetOf` ((rolePermissions r1).copy)), testGroup "permissionsRole, rolePermissions" [ testCase "'Role' maps to expected permissions" $ do @@ -76,15 +75,15 @@ permissionTests = case permissionsRole perms of Just role -> do let perms' = rolePermissions role - assertEqual "eq" (perms' ^. self) (perms' ^. copy) - assertBool "self" ((perms' ^. self) `Set.isSubsetOf` (perms ^. self)) - assertBool "copy" ((perms' ^. copy) `Set.isSubsetOf` (perms ^. copy)) + assertEqual "eq" perms'.self perms'.copy + assertBool "self" (perms'.self `Set.isSubsetOf` perms.self) + assertBool "copy" (perms'.copy `Set.isSubsetOf` perms.copy) Nothing -> do let leastPermissions = rolePermissions maxBound assertBool "no role for perms, but strictly more perms than max role" $ not - ( (leastPermissions ^. self) `Set.isSubsetOf` w - && (leastPermissions ^. copy) `Set.isSubsetOf` w' + ( (leastPermissions.self) `Set.isSubsetOf` w + && (leastPermissions.copy) `Set.isSubsetOf` w' ) ] ] @@ -93,8 +92,8 @@ permissionConversionTests :: TestTree permissionConversionTests = testGroup "permsToInt / rolePermissions / serialization of `Role`s" - [ testCase "partner" $ assertEqual "" (permsToInt . _self $ rolePermissions RoleExternalPartner) 1025, - testCase "member" $ assertEqual "" (permsToInt . _self $ rolePermissions RoleMember) 1587, - testCase "admin" $ assertEqual "" (permsToInt . _self $ rolePermissions RoleAdmin) 5951, - testCase "owner" $ assertEqual "" (permsToInt . _self $ rolePermissions RoleOwner) 8191 + [ testCase "partner" $ assertEqual "" (permsToInt . self $ rolePermissions RoleExternalPartner) 1025, + testCase "member" $ assertEqual "" (permsToInt . self $ rolePermissions RoleMember) 1587, + testCase "admin" $ assertEqual "" (permsToInt . self $ rolePermissions RoleAdmin) 5951, + testCase "owner" $ assertEqual "" (permsToInt . self $ rolePermissions RoleOwner) 8191 ] 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 2a5fa7d31e1..0944fce6638 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/User.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/User.hs @@ -117,7 +117,7 @@ testUserProfile = do uid <- Id <$> UUID.nextRandom let domain = Domain "example.com" let colour = ColourId 0 - let userProfile = UserProfile (Qualified uid domain) (Name "name") (Pict []) [] colour False Nothing Nothing Nothing Nothing Nothing UserLegalHoldNoConsent defSupportedProtocols + let userProfile = UserProfile (Qualified uid domain) (Name "name") Nothing (Pict []) [] colour False Nothing Nothing Nothing Nothing Nothing UserLegalHoldNoConsent defSupportedProtocols let profileJSONAsText = show $ Aeson.encode userProfile let msg = "toJSON encoding must not convert Nothing to null, but instead omit those json fields for backwards compatibility. UserProfileJSON:" <> profileJSONAsText assertBool msg (not $ "null" `isInfixOf` profileJSONAsText) @@ -142,7 +142,7 @@ parseIdentityTests = Right Nothing =#= [("something_unrelated", "#")] ] where - hemail = Email "me" "example.com" + hemail = unsafeEmailAddress "me" "example.com" email = ("email", "me@example.com") bademail = ("email", "justme") badphone = ("phone", "__@@") diff --git a/libs/wire-api/test/unit/Test/Wire/API/User/Auth.hs b/libs/wire-api/test/unit/Test/Wire/API/User/Auth.hs index c455977297e..4b01d5724b0 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/User/Auth.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/User/Auth.hs @@ -24,12 +24,12 @@ import Wire.API.User import Wire.API.User.Auth tests :: T.TestTree -tests = T.testGroup "Auth" [loginIdHappyCase, loginIdFailFastOnEmail, loginIdFailFastOnPhone] +tests = T.testGroup "Auth" [loginIdHappyCase, loginIdFailFastOnEmail] loginIdHappyCase :: T.TestTree loginIdHappyCase = testCase "LoginId parser: valid email" $ do let actual :: Maybe LoginId = Aeson.decode "{\"email\":\"foo@bar.com\"}" - let expected = Just $ LoginByEmail (Email {emailLocal = "foo", emailDomain = "bar.com"}) + let expected = Just $ LoginByEmail (unsafeEmailAddress "foo" "bar.com") assertEqual "should succeed" expected actual loginIdFailFastOnEmail :: T.TestTree @@ -37,9 +37,3 @@ loginIdFailFastOnEmail = testCase "LoginId parser: invalid email, valid phone" $ let actual :: Maybe LoginId = Aeson.decode "{\"email\":\"invalid-email\",\"phone\":\"+123456789\"}" let expected = Nothing assertEqual "should fail if any provided login id is invalid" expected actual - -loginIdFailFastOnPhone :: T.TestTree -loginIdFailFastOnPhone = testCase "LoginId parser: invalid phone, valid email" $ do - let actual :: Maybe LoginId = Aeson.decode "{\"email\":\"foo@bar.com\",\"phone\":\"invalid-phone\"}" - let expected = Nothing - assertEqual "should fail if any provided login id is invalid" expected actual diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 5c37e1dbca2..1b61c678a71 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -74,8 +74,9 @@ library Wire.API.Bot Wire.API.Bot.Service Wire.API.Call.Config + Wire.API.CannonId + Wire.API.Component Wire.API.Connection - Wire.API.ConverProtoLens Wire.API.Conversation Wire.API.Conversation.Action Wire.API.Conversation.Action.Tag @@ -97,6 +98,7 @@ library Wire.API.Event.Conversation Wire.API.Event.FeatureConfig Wire.API.Event.Federation + Wire.API.Event.Gundeck Wire.API.Event.LeaveReason Wire.API.Event.Team Wire.API.FederationStatus @@ -104,7 +106,6 @@ library Wire.API.Internal.BulkPush Wire.API.Internal.Notification Wire.API.Locale - Wire.API.MakesFederatedCall Wire.API.Message Wire.API.Message.Proto Wire.API.MLS.AuthenticatedContent @@ -137,6 +138,7 @@ library Wire.API.Notification Wire.API.OAuth Wire.API.Password + Wire.API.Presence Wire.API.Properties Wire.API.Provider Wire.API.Provider.Bot @@ -144,6 +146,7 @@ library Wire.API.Provider.Service Wire.API.Provider.Service.Tag Wire.API.Push.Token + Wire.API.Push.V2 Wire.API.Push.V2.Token Wire.API.RawJson Wire.API.Routes.API @@ -152,6 +155,7 @@ library Wire.API.Routes.ClientAlgebra Wire.API.Routes.Cookies Wire.API.Routes.CSV + Wire.API.Routes.Features Wire.API.Routes.FederationDomainConfig Wire.API.Routes.Internal.Brig Wire.API.Routes.Internal.Brig.Connection @@ -164,7 +168,7 @@ library Wire.API.Routes.Internal.Galley.ConversationsIntra Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti Wire.API.Routes.Internal.Galley.TeamsIntra - Wire.API.Routes.Internal.LegalHold + Wire.API.Routes.Internal.Gundeck Wire.API.Routes.Internal.Spar Wire.API.Routes.LowLevelStream Wire.API.Routes.MultiTablePaging @@ -229,11 +233,13 @@ library Wire.API.User.Client Wire.API.User.Client.DPoPAccessToken Wire.API.User.Client.Prekey + Wire.API.User.EmailAddress Wire.API.User.Handle Wire.API.User.Identity Wire.API.User.IdentityProvider Wire.API.User.Orphans Wire.API.User.Password + Wire.API.User.Phone Wire.API.User.Profile Wire.API.User.RichInfo Wire.API.User.Saml @@ -263,7 +269,6 @@ library , cereal , comonad , conduit - , constraints , containers >=0.5 , cookie , crypton @@ -272,7 +277,6 @@ library , data-default , deriving-aeson >=0.2 , deriving-swagger2 - , either , email-validate >=2.0 , errors , extended @@ -294,11 +298,13 @@ library , iso3166-country-codes >=0.2 , iso639 >=0.1 , jose + , kan-extensions , lens >=4.12 , memory , metrics-wai , mime >=0.4 , mtl + , network-uri , openapi3 , pem >=0.2 , polysemy @@ -317,6 +323,7 @@ library , servant-client-core , servant-conduit , servant-multipart + , servant-multipart-api , servant-openapi3 , servant-server , singletons @@ -325,9 +332,9 @@ library , sop-core , tagged , text >=0.11 + , these , time >=1.4 , tinylog - , transitive-anns , types-common >=0.16 , unordered-containers >=0.2 , uri-bytestring >=0.2 @@ -358,7 +365,6 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Generated.AccessRoleLegacy_user Test.Wire.API.Golden.Generated.AccessToken_user Test.Wire.API.Golden.Generated.Action_user - Test.Wire.API.Golden.Generated.Activate_user Test.Wire.API.Golden.Generated.ActivationCode_user Test.Wire.API.Golden.Generated.ActivationKey_user Test.Wire.API.Golden.Generated.ActivationResponse_user @@ -372,7 +378,6 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Generated.AssetSettings_user Test.Wire.API.Golden.Generated.AssetSize_user Test.Wire.API.Golden.Generated.AssetToken_user - Test.Wire.API.Golden.Generated.BindingNewTeam_team Test.Wire.API.Golden.Generated.BindingNewTeamUser_user Test.Wire.API.Golden.Generated.BotConvView_provider Test.Wire.API.Golden.Generated.BotUserView_provider @@ -422,7 +427,7 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Generated.Event_user Test.Wire.API.Golden.Generated.EventType_team Test.Wire.API.Golden.Generated.EventType_user - Test.Wire.API.Golden.Generated.FeatureStatus_team + Test.Wire.API.Golden.Generated.Feature_team Test.Wire.API.Golden.Generated.HandleUpdate_user Test.Wire.API.Golden.Generated.Invitation_team Test.Wire.API.Golden.Generated.InvitationCode_user @@ -436,10 +441,10 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Generated.ListType_team Test.Wire.API.Golden.Generated.Locale_user Test.Wire.API.Golden.Generated.LocaleUpdate_user - Test.Wire.API.Golden.Generated.Login_user + Test.Wire.API.Golden.Generated.LockableFeature_team + Test.Wire.API.Golden.Generated.LockableFeaturePatch_team Test.Wire.API.Golden.Generated.LoginCode_user Test.Wire.API.Golden.Generated.LoginCodeTimeout_user - Test.Wire.API.Golden.Generated.LoginId_user Test.Wire.API.Golden.Generated.ManagedBy_user Test.Wire.API.Golden.Generated.Member_user Test.Wire.API.Golden.Generated.MemberUpdate_user @@ -460,6 +465,7 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Generated.NewProviderResponse_provider Test.Wire.API.Golden.Generated.NewService_provider Test.Wire.API.Golden.Generated.NewServiceResponse_provider + Test.Wire.API.Golden.Generated.NewTeam_team Test.Wire.API.Golden.Generated.NewTeamMember_team Test.Wire.API.Golden.Generated.NewUser_user Test.Wire.API.Golden.Generated.NewUserPublic_user @@ -512,7 +518,6 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Generated.SearchResult_20Contact_user Test.Wire.API.Golden.Generated.SearchResult_20TeamContact_user Test.Wire.API.Golden.Generated.SelfProfile_user - Test.Wire.API.Golden.Generated.SendActivationCode_user Test.Wire.API.Golden.Generated.SendLoginCode_user Test.Wire.API.Golden.Generated.Service_provider Test.Wire.API.Golden.Generated.ServiceKey_provider @@ -568,11 +573,10 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Generated.VerifyDeleteUser_user Test.Wire.API.Golden.Generated.ViewLegalHoldService_team Test.Wire.API.Golden.Generated.ViewLegalHoldServiceInfo_team - Test.Wire.API.Golden.Generated.WithStatus_team - Test.Wire.API.Golden.Generated.WithStatusNoLock_team - Test.Wire.API.Golden.Generated.WithStatusPatch_team 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.CannonId Test.Wire.API.Golden.Manual.ClientCapability Test.Wire.API.Golden.Manual.ClientCapabilityList Test.Wire.API.Golden.Manual.Contact @@ -584,6 +588,7 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Manual.ConvIdsPage Test.Wire.API.Golden.Manual.CreateGroupConversation Test.Wire.API.Golden.Manual.CreateScimToken + Test.Wire.API.Golden.Manual.CreateScimTokenResponse Test.Wire.API.Golden.Manual.FeatureConfigEvent Test.Wire.API.Golden.Manual.FederationDomainConfig Test.Wire.API.Golden.Manual.FederationRestriction @@ -592,9 +597,15 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Manual.GroupId Test.Wire.API.Golden.Manual.ListConversations Test.Wire.API.Golden.Manual.ListUsersById + Test.Wire.API.Golden.Manual.Login_user + Test.Wire.API.Golden.Manual.LoginId_user Test.Wire.API.Golden.Manual.MLSKeys + Test.Wire.API.Golden.Manual.Presence + Test.Wire.API.Golden.Manual.Push + Test.Wire.API.Golden.Manual.PushRemove Test.Wire.API.Golden.Manual.QualifiedUserClientPrekeyMap Test.Wire.API.Golden.Manual.SearchResultContact + Test.Wire.API.Golden.Manual.SendActivationCode_user Test.Wire.API.Golden.Manual.SubConversation Test.Wire.API.Golden.Manual.TeamSize Test.Wire.API.Golden.Manual.Token @@ -616,7 +627,6 @@ test-suite wire-api-golden-tests , bytestring-conversion , containers >=0.5 , currency-codes - , either , imports , iso3166-country-codes , iso639 @@ -662,7 +672,6 @@ test-suite wire-api-tests Test.Wire.API.Run Test.Wire.API.Swagger Test.Wire.API.Team.Export - Test.Wire.API.Team.Feature Test.Wire.API.Team.Member Test.Wire.API.User Test.Wire.API.User.Auth @@ -704,6 +713,7 @@ test-suite wire-api-tests , tasty-hunit , tasty-quickcheck , text + , time , types-common >=0.16 , unliftio , uuid diff --git a/libs/wire-message-proto-lens/wire-message-proto-lens.cabal b/libs/wire-message-proto-lens/wire-message-proto-lens.cabal index 3caba005121..8087bfa00f8 100644 --- a/libs/wire-message-proto-lens/wire-message-proto-lens.cabal +++ b/libs/wire-message-proto-lens/wire-message-proto-lens.cabal @@ -16,7 +16,7 @@ extra-source-files: custom-setup setup-depends: base - , Cabal + , Cabal >=3.12 , proto-lens-setup library diff --git a/libs/wire-otel/CHANGELOG.md b/libs/wire-otel/CHANGELOG.md new file mode 100644 index 00000000000..799270ba251 --- /dev/null +++ b/libs/wire-otel/CHANGELOG.md @@ -0,0 +1,5 @@ +# Revision history for wire-otel + +## 0.1.0.0 -- YYYY-mm-dd + +* First version. Released on an unsuspecting world. diff --git a/libs/gundeck-types/LICENSE b/libs/wire-otel/LICENSE similarity index 100% rename from libs/gundeck-types/LICENSE rename to libs/wire-otel/LICENSE diff --git a/libs/wire-otel/default.nix b/libs/wire-otel/default.nix new file mode 100644 index 00000000000..e3cec5ff487 --- /dev/null +++ b/libs/wire-otel/default.nix @@ -0,0 +1,34 @@ +# WARNING: GENERATED FILE, DO NOT EDIT. +# This file is generated by running hack/bin/generate-local-nix-packages.sh and +# must be regenerated whenever local packages are added or removed, or +# dependencies are added or removed. +{ mkDerivation +, base +, gitignoreSource +, hs-opentelemetry-instrumentation-http-client +, hs-opentelemetry-instrumentation-wai +, hs-opentelemetry-sdk +, http-client +, kan-extensions +, lib +, text +, unliftio +}: +mkDerivation { + pname = "wire-otel"; + version = "0.1.0.0"; + src = gitignoreSource ./.; + libraryHaskellDepends = [ + base + hs-opentelemetry-instrumentation-http-client + hs-opentelemetry-instrumentation-wai + hs-opentelemetry-sdk + http-client + kan-extensions + text + unliftio + ]; + testHaskellDepends = [ base ]; + homepage = "https://wire.com/"; + license = lib.licenses.agpl3Only; +} diff --git a/libs/wire-otel/src/Wire/OpenTelemetry.hs b/libs/wire-otel/src/Wire/OpenTelemetry.hs new file mode 100644 index 00000000000..aa76dc500b4 --- /dev/null +++ b/libs/wire-otel/src/Wire/OpenTelemetry.hs @@ -0,0 +1,52 @@ +-- FUTUREWORK(mangoiv): +-- instrument http/2 request similarly to how it was done for http-client here: +-- https://github.com/iand675/hs-opentelemetry/blob/0b3c854a88113fc18df8561202a76357e593a294/instrumentation/http-client/src/OpenTelemetry/Instrumentation/HttpClient/Raw.hs#L60 +-- This is non-trivial because http/2 forgets the structure on the out objs. +module Wire.OpenTelemetry + ( -- * instrumentation helpers + withTracer, + withTracerC, + + -- * outbound instrumentation + + -- ** http client + withClientInstrumentation, + ) +where + +import Control.Monad.Codensity (Codensity (Codensity)) +import Data.Text (Text) +import Network.HTTP.Client (Request, Response) +import OpenTelemetry.Context.ThreadLocal (getContext) +import OpenTelemetry.Instrumentation.HttpClient.Raw +import OpenTelemetry.Trace +import UnliftIO (MonadUnliftIO, bracket, liftIO) + +-- | a tracer for a service like brig, galley, etc. +withTracer :: (MonadUnliftIO m) => (Tracer -> m r) -> m r +withTracer k = + bracket + (liftIO initializeGlobalTracerProvider) + shutdownTracerProvider + \tp -> k $ makeTracer tp "wire-otel" tracerOptions + +-- | like 'withTracer' but in 'Codensity' +withTracerC :: Codensity IO Tracer +withTracerC = Codensity withTracer + +-- | instrument a http client +withClientInstrumentation :: + (MonadUnliftIO m) => + -- | name of the caller + Text -> + -- | continuation that takes a continuation that takes a request and a way to respond to a request + ((Request -> (Request -> m (Response a)) -> m (Response a)) -> m b) -> + m b +withClientInstrumentation info k = do + tracer <- httpTracerProvider + inSpan tracer info defaultSpanArguments {kind = Client} do + otelCtx <- getContext + k \req respond -> do + resp <- respond =<< instrumentRequest httpClientInstrumentationConfig otelCtx req + instrumentResponse httpClientInstrumentationConfig otelCtx resp + pure resp diff --git a/libs/wire-otel/test/Main.hs b/libs/wire-otel/test/Main.hs new file mode 100644 index 00000000000..3e2059e31f5 --- /dev/null +++ b/libs/wire-otel/test/Main.hs @@ -0,0 +1,4 @@ +module Main (main) where + +main :: IO () +main = putStrLn "Test suite not yet implemented." diff --git a/libs/wire-otel/wire-otel.cabal b/libs/wire-otel/wire-otel.cabal new file mode 100644 index 00000000000..450c2dc3ea0 --- /dev/null +++ b/libs/wire-otel/wire-otel.cabal @@ -0,0 +1,46 @@ +cabal-version: 3.4 +name: wire-otel +version: 0.1.0.0 +description: wire open-telemetry-instrumentation +homepage: https://wire.com/ +license: AGPL-3.0-only +license-file: LICENSE +author: Wire Swiss GmbH +maintainer: backend@wire.com +copyright: (c) 2020 Wire Swiss GmbH +build-type: Simple +extra-doc-files: CHANGELOG.md + +common common-all + ghc-options: -O2 -Wall + default-extensions: + BlockArguments + OverloadedLists + OverloadedRecordDot + OverloadedStrings + +library + import: common-all + exposed-modules: Wire.OpenTelemetry + build-depends: + , base + , hs-opentelemetry-instrumentation-http-client + , hs-opentelemetry-instrumentation-wai + , hs-opentelemetry-sdk + , http-client + , kan-extensions + , text + , unliftio + + hs-source-dirs: src + default-language: GHC2021 + +test-suite wire-otel-test + import: common-all + default-language: GHC2021 + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Main.hs + build-depends: + , base + , wire-otel diff --git a/libs/wire-subsystems/default.nix b/libs/wire-subsystems/default.nix index ab5d6d19a48..768fcdc5cac 100644 --- a/libs/wire-subsystems/default.nix +++ b/libs/wire-subsystems/default.nix @@ -8,12 +8,16 @@ , amazonka-core , amazonka-ses , async +, attoparsec , base , base16-bytestring , bilge +, bloodhound , bytestring , bytestring-conversion +, case-insensitive , cassandra-util +, conduit , containers , cql , crypton @@ -25,7 +29,6 @@ , extended , extra , gitignoreSource -, gundeck-types , HaskellNet , HaskellNet-SSL , HsOpenSSL @@ -50,12 +53,16 @@ , polysemy-time , polysemy-wire-zoo , postie +, prometheus-client , QuickCheck , quickcheck-instances , random , resource-pool , resourcet , retry +, saml2-web-sso +, schema-profunctor +, scientific , servant , servant-client-core , stomp-queue @@ -63,19 +70,22 @@ , string-conversions , template , text +, text-icu-translit , time , time-out , time-units , tinylog , transformers -, transitive-anns , types-common , unliftio , unordered-containers +, uri-bytestring , uuid , wai-utilities , wire-api , wire-api-federation +, wire-otel +, witherable }: mkDerivation { pname = "wire-subsystems"; @@ -87,12 +97,16 @@ mkDerivation { amazonka-core amazonka-ses async + attoparsec base base16-bytestring bilge + bloodhound bytestring bytestring-conversion + case-insensitive cassandra-util + conduit containers cql crypton @@ -103,7 +117,6 @@ mkDerivation { exceptions extended extra - gundeck-types HaskellNet HaskellNet-SSL HsOpenSSL @@ -124,28 +137,34 @@ mkDerivation { polysemy-plugin polysemy-time polysemy-wire-zoo + prometheus-client QuickCheck resource-pool resourcet retry + saml2-web-sso + schema-profunctor servant servant-client-core stomp-queue template text + text-icu-translit time time-out time-units tinylog transformers - transitive-anns types-common unliftio unordered-containers + uri-bytestring uuid wai-utilities wire-api wire-api-federation + wire-otel + witherable ]; testHaskellDepends = [ aeson @@ -153,12 +172,12 @@ mkDerivation { base bilge bytestring + cassandra-util containers crypton data-default errors extended - gundeck-types hspec imports iso639 @@ -174,6 +193,7 @@ mkDerivation { QuickCheck quickcheck-instances random + scientific servant-client-core streaming-commons string-conversions diff --git a/services/gundeck/src/Gundeck/API.hs b/libs/wire-subsystems/src/Wire/ActivationCodeStore.hs similarity index 69% rename from services/gundeck/src/Gundeck/API.hs rename to libs/wire-subsystems/src/Wire/ActivationCodeStore.hs index eca27d6cfed..9473bd16f58 100644 --- a/services/gundeck/src/Gundeck/API.hs +++ b/libs/wire-subsystems/src/Wire/ActivationCodeStore.hs @@ -14,16 +14,17 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . +{-# LANGUAGE TemplateHaskell #-} -module Gundeck.API - ( sitemap, - ) -where +module Wire.ActivationCodeStore where -import Gundeck.API.Internal qualified as Internal -import Gundeck.Monad (Gundeck) -import Network.Wai.Routing (Routes) +import Data.Id +import Imports +import Polysemy +import Wire.API.User.Activation +import Wire.UserKeyStore -sitemap :: Routes () Gundeck () -sitemap = do - Internal.sitemap +data ActivationCodeStore :: Effect where + LookupActivationCode :: EmailKey -> ActivationCodeStore m (Maybe (Maybe UserId, ActivationCode)) + +makeSem ''ActivationCodeStore diff --git a/libs/wire-subsystems/src/Wire/ActivationCodeStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/ActivationCodeStore/Cassandra.hs new file mode 100644 index 00000000000..7f0ba27ba03 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/ActivationCodeStore/Cassandra.hs @@ -0,0 +1,37 @@ +module Wire.ActivationCodeStore.Cassandra where + +import Cassandra +import Data.Id +import Data.Text.Ascii qualified as Ascii +import Data.Text.Encoding qualified as T +import Imports +import OpenSSL.EVP.Digest +import Polysemy +import Polysemy.Embed +import Wire.API.User.Activation +import Wire.ActivationCodeStore +import Wire.UserKeyStore (EmailKey, emailKeyUniq) + +interpretActivationCodeStoreToCassandra :: (Member (Embed IO) r) => ClientState -> InterpreterFor ActivationCodeStore r +interpretActivationCodeStoreToCassandra casClient = + interpret $ + runEmbedded (runClient casClient) . \case + LookupActivationCode ek -> embed do + liftIO (mkActivationKey ek) + >>= retry x1 . query1 cql . params LocalQuorum . Identity + where + cql :: PrepQuery R (Identity ActivationKey) (Maybe UserId, ActivationCode) + cql = + [sql| + SELECT user, code FROM activation_keys WHERE key = ? + |] + +mkActivationKey :: EmailKey -> IO ActivationKey +mkActivationKey k = do + Just d <- getDigestByName "SHA256" + pure do + ActivationKey + . Ascii.encodeBase64Url + . digestBS d + . T.encodeUtf8 + $ emailKeyUniq k diff --git a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs index 9b669979bd8..9d098c0ead9 100644 --- a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs @@ -18,16 +18,26 @@ module Wire.AuthenticationSubsystem where +import Data.Id import Data.Misc +import Data.Qualified import Imports import Polysemy +import Wire.API.Password (Password, PasswordStatus) import Wire.API.User -import Wire.API.User.Password +import Wire.API.User.Password (PasswordResetCode, PasswordResetIdentity) +import Wire.AuthenticationSubsystem.Error import Wire.UserKeyStore data AuthenticationSubsystem m a where + AuthenticateEither :: UserId -> PlainTextPassword6 -> AuthenticationSubsystem m (Either AuthError ()) + ReauthenticateEither :: UserId -> Maybe PlainTextPassword6 -> AuthenticationSubsystem m (Either ReAuthError ()) CreatePasswordResetCode :: EmailKey -> AuthenticationSubsystem m () ResetPassword :: PasswordResetIdentity -> PasswordResetCode -> PlainTextPassword8 -> AuthenticationSubsystem m () + VerifyPassword :: PlainTextPassword6 -> Password -> AuthenticationSubsystem m (Bool, PasswordStatus) + VerifyUserPassword :: UserId -> PlainTextPassword6 -> AuthenticationSubsystem r (Bool, PasswordStatus) + VerifyUserPasswordError :: Local UserId -> PlainTextPassword6 -> AuthenticationSubsystem m () + VerifyProviderPassword :: ProviderId -> PlainTextPassword6 -> AuthenticationSubsystem r (Bool, PasswordStatus) -- For testing InternalLookupPasswordResetCode :: EmailKey -> AuthenticationSubsystem m (Maybe PasswordResetPair) diff --git a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Error.hs b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Error.hs index 5efede38c26..79d4d57dfd5 100644 --- a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Error.hs +++ b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Error.hs @@ -14,23 +14,42 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Wire.AuthenticationSubsystem.Error - ( AuthenticationSubsystemError (..), - authenticationSubsystemErrorToHttpError, - ) -where +module Wire.AuthenticationSubsystem.Error where import Imports import Wire.API.Error import Wire.API.Error.Brig qualified as E import Wire.Error +-- | Authentication errors. +data AuthError + = AuthInvalidUser + | AuthInvalidCredentials + | AuthSuspended + | AuthEphemeral + | AuthPendingInvitation + deriving (Show, Eq) + +instance Exception AuthError + +-- | Re-authentication errors. +data ReAuthError + = ReAuthError !AuthError + | ReAuthMissingPassword + | ReAuthCodeVerificationRequired + | ReAuthCodeVerificationNoPendingCode + | ReAuthCodeVerificationNoEmail + deriving (Show, Eq) + +instance Exception ReAuthError + data AuthenticationSubsystemError = AuthenticationSubsystemInvalidPasswordResetKey | AuthenticationSubsystemResetPasswordMustDiffer | AuthenticationSubsystemInvalidPasswordResetCode | AuthenticationSubsystemInvalidPhone | AuthenticationSubsystemAllowListError + | AuthenticationSubsystemBadCredentials deriving (Eq, Show) instance Exception AuthenticationSubsystemError @@ -43,3 +62,4 @@ authenticationSubsystemErrorToHttpError = AuthenticationSubsystemResetPasswordMustDiffer -> errorToWai @E.ResetPasswordMustDiffer AuthenticationSubsystemInvalidPhone -> errorToWai @E.InvalidPhone AuthenticationSubsystemAllowListError -> errorToWai @E.AllowlistError + AuthenticationSubsystemBadCredentials -> errorToWai @E.BadCredentials diff --git a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs index 94024d5b4cf..3c6b5a63280 100644 --- a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs @@ -23,11 +23,12 @@ module Wire.AuthenticationSubsystem.Interpreter where import Data.ByteString.Conversion +import Data.HavePendingInvitations import Data.Id import Data.Misc import Data.Qualified import Data.Time -import Imports hiding (lookup) +import Imports hiding (local, lookup) import Polysemy import Polysemy.Error import Polysemy.Input @@ -36,7 +37,9 @@ import Polysemy.TinyLog qualified as Log import System.Logger import Wire.API.Allowlists (AllowlistEmailDomains) import Wire.API.Allowlists qualified as AllowLists -import Wire.API.Password +import Wire.API.Password (Password, PasswordStatus (..)) +import Wire.API.Password qualified as Password +import Wire.API.Password qualified as Pasword import Wire.API.User import Wire.API.User.Password import Wire.AuthenticationSubsystem @@ -44,12 +47,15 @@ import Wire.AuthenticationSubsystem.Error import Wire.EmailSubsystem import Wire.HashPassword import Wire.PasswordResetCodeStore -import Wire.PasswordStore +import Wire.PasswordStore (PasswordStore, upsertHashedPassword) +import Wire.PasswordStore qualified as PasswordStore import Wire.Sem.Now import Wire.Sem.Now qualified as Now import Wire.SessionStore import Wire.UserKeyStore -import Wire.UserSubsystem (UserSubsystem, getLocalUserAccountByUserKey) +import Wire.UserStore +import Wire.UserSubsystem (UserSubsystem, getLocalAccountBy) +import Wire.UserSubsystem qualified as User interpretAuthenticationSubsystem :: forall r. @@ -61,15 +67,25 @@ interpretAuthenticationSubsystem :: Member SessionStore r, Member (Input (Local ())) r, Member (Input (Maybe AllowlistEmailDomains)) r, - Member UserSubsystem r, Member PasswordStore r, - Member EmailSubsystem r + Member EmailSubsystem r, + Member UserStore r ) => + InterpreterFor UserSubsystem r -> InterpreterFor AuthenticationSubsystem r -interpretAuthenticationSubsystem = interpret $ \case - CreatePasswordResetCode userKey -> createPasswordResetCodeImpl userKey - ResetPassword ident resetCode newPassword -> resetPasswordImpl ident resetCode newPassword - InternalLookupPasswordResetCode userKey -> internalLookupPasswordResetCodeImpl userKey +interpretAuthenticationSubsystem userSubsystemInterpreter = + interpret $ + userSubsystemInterpreter . \case + AuthenticateEither uid pwd -> authenticateEitherImpl uid pwd + ReauthenticateEither uid pwd -> reauthenticateEitherImpl uid pwd + CreatePasswordResetCode userKey -> createPasswordResetCodeImpl userKey + ResetPassword ident resetCode newPassword -> resetPasswordImpl ident resetCode newPassword + VerifyPassword plaintext pwd -> verifyPasswordImpl plaintext pwd + VerifyUserPassword uid plaintext -> verifyUserPasswordImpl uid plaintext + VerifyUserPasswordError luid plaintext -> verifyUserPasswordErrorImpl luid plaintext + VerifyProviderPassword pid plaintext -> verifyProviderPasswordImpl pid plaintext + -- Testing + InternalLookupPasswordResetCode userKey -> internalLookupPasswordResetCodeImpl userKey maxAttempts :: Int32 maxAttempts = 3 @@ -89,6 +105,70 @@ instance Exception PasswordResetError where displayException InvalidResetKey = "invalid reset key for password reset" displayException InProgress = "password reset already in progress" +authenticateEitherImpl :: + ( Member UserStore r, + Member HashPassword r, + Member PasswordStore r + ) => + UserId -> + PlainTextPassword6 -> + Sem r (Either AuthError ()) +authenticateEitherImpl uid plaintext = do + runError $ + getUserAuthenticationInfo uid >>= \case + Nothing -> throw AuthInvalidUser + Just (_, Deleted) -> throw AuthInvalidUser + Just (_, Suspended) -> throw AuthSuspended + Just (_, Ephemeral) -> throw AuthEphemeral + Just (_, PendingInvitation) -> throw AuthPendingInvitation + Just (Nothing, _) -> throw AuthInvalidCredentials + Just (Just password, Active) -> do + case Pasword.verifyPasswordWithStatus plaintext password of + (False, _) -> throw AuthInvalidCredentials + (True, PasswordStatusNeedsUpdate) -> do + (hashAndUpdatePwd uid plaintext) + (True, _) -> pure () + where + hashAndUpdatePwd u pwd = do + hashed <- hashPassword6 pwd + upsertHashedPassword u hashed + +-- | Password reauthentication. If the account has a password, reauthentication +-- is mandatory. If +-- * User has no password, re-auth is a no-op +-- * User is an SSO user and no password is given, re-auth is a no-op. +reauthenticateEitherImpl :: + ( Member UserStore r, + Member UserSubsystem r, + Member (Input (Local ())) r + ) => + UserId -> + Maybe (PlainTextPassword' t) -> + Sem r (Either ReAuthError ()) +reauthenticateEitherImpl user plaintextMaybe = + getUserAuthenticationInfo user + >>= runError + . \case + Nothing -> throw (ReAuthError AuthInvalidUser) + Just (_, Deleted) -> throw (ReAuthError AuthInvalidUser) + Just (_, Suspended) -> throw (ReAuthError AuthSuspended) + Just (_, PendingInvitation) -> throw (ReAuthError AuthPendingInvitation) + Just (Nothing, _) -> for_ plaintextMaybe $ const (throw $ ReAuthError AuthInvalidCredentials) + Just (Just pw', Active) -> maybeReAuth pw' + Just (Just pw', Ephemeral) -> maybeReAuth pw' + where + maybeReAuth pw' = case plaintextMaybe of + Nothing -> do + local <- input + musr <- getLocalAccountBy NoPendingInvitations (qualifyAs local user) + let isSaml = maybe False isSamlUser musr + -- If this is a SAML user, re-auth should be no-op so no error is thrown. + unless isSaml $ + throw ReAuthMissingPassword + Just p -> + unless (Password.verifyPassword p pw') do + throw (ReAuthError AuthInvalidCredentials) + createPasswordResetCodeImpl :: forall r. ( Member PasswordResetCodeStore r, @@ -134,27 +214,31 @@ createPasswordResetCodeImpl target = Right v -> pure v lookupActiveUserIdByUserKey :: - (Member UserSubsystem r, Member (Input (Local ())) r) => + ( Member UserSubsystem r, + Member (Input (Local ())) r + ) => EmailKey -> Sem r (Maybe UserId) lookupActiveUserIdByUserKey target = userId <$$> lookupActiveUserByUserKey target lookupActiveUserByUserKey :: - (Member UserSubsystem r, Member (Input (Local ())) r) => + ( Member UserSubsystem r, + Member (Input (Local ())) r + ) => EmailKey -> Sem r (Maybe User) lookupActiveUserByUserKey target = do localUnit <- input - let ltarget = qualifyAs localUnit target - mUser <- getLocalUserAccountByUserKey ltarget + let ltarget = qualifyAs localUnit [emailKeyOrig target] + mUser <- User.getAccountsByEmailNoFilter ltarget case mUser of - Just user -> do + [user] -> do pure $ - if user.accountStatus == Active - then Just user.accountUser + if user.userStatus == Active + then Just user else Nothing - Nothing -> pure Nothing + _ -> pure Nothing internalLookupPasswordResetCodeImpl :: ( Member PasswordResetCodeStore r, @@ -212,8 +296,8 @@ resetPasswordImpl ident code pw = do Just uid -> do Log.debug $ field "user" (toByteString uid) . field "action" (val "User.completePasswordReset") checkNewIsDifferent uid pw - hashedPw <- hashPassword pw - upsertHashedPassword uid hashedPw + hashedPw <- hashPassword8 pw + PasswordStore.upsertHashedPassword uid hashedPw codeDelete key deleteAllCookies uid where @@ -229,10 +313,10 @@ resetPasswordImpl ident code pw = do checkNewIsDifferent :: UserId -> PlainTextPassword' t -> Sem r () checkNewIsDifferent uid newPassword = do - mCurrentPassword <- lookupHashedPassword uid + mCurrentPassword <- PasswordStore.lookupHashedPassword uid case mCurrentPassword of Just currentPassword - | (verifyPassword newPassword currentPassword) -> throw AuthenticationSubsystemResetPasswordMustDiffer + | (Password.verifyPassword newPassword currentPassword) -> throw AuthenticationSubsystemResetPasswordMustDiffer _ -> pure () verify :: PasswordResetPair -> Sem r (Maybe UserId) @@ -249,3 +333,48 @@ resetPasswordImpl ident code pw = do pure Nothing Just PRQueryData {} -> codeDelete k $> Nothing Nothing -> pure Nothing + +verifyPasswordImpl :: + PlainTextPassword6 -> + Password -> + Sem r (Bool, PasswordStatus) +verifyPasswordImpl plaintext password = do + pure $ Password.verifyPasswordWithStatus plaintext password + +verifyProviderPasswordImpl :: + ( Member PasswordStore r, + Member (Error AuthenticationSubsystemError) r + ) => + ProviderId -> + PlainTextPassword6 -> + Sem r (Bool, PasswordStatus) +verifyProviderPasswordImpl pid plaintext = do + -- We type-erase uid here + password <- + PasswordStore.lookupHashedProviderPassword pid + >>= maybe (throw AuthenticationSubsystemBadCredentials) pure + verifyPasswordImpl plaintext password + +verifyUserPasswordImpl :: + ( Member PasswordStore r, + Member (Error AuthenticationSubsystemError) r + ) => + UserId -> + PlainTextPassword6 -> + Sem r (Bool, PasswordStatus) +verifyUserPasswordImpl uid plaintext = do + password <- + PasswordStore.lookupHashedPassword uid + >>= maybe (throw AuthenticationSubsystemBadCredentials) pure + verifyPasswordImpl plaintext password + +verifyUserPasswordErrorImpl :: + ( Member PasswordStore r, + Member (Error AuthenticationSubsystemError) r + ) => + Local UserId -> + PlainTextPassword6 -> + Sem r () +verifyUserPasswordErrorImpl (tUnqualified -> uid) password = do + unlessM (fst <$> verifyUserPasswordImpl uid password) do + throw AuthenticationSubsystemBadCredentials diff --git a/libs/wire-subsystems/src/Wire/BlockListStore.hs b/libs/wire-subsystems/src/Wire/BlockListStore.hs new file mode 100644 index 00000000000..55ce155d46f --- /dev/null +++ b/libs/wire-subsystems/src/Wire/BlockListStore.hs @@ -0,0 +1,14 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.BlockListStore where + +import Imports +import Polysemy +import Wire.UserKeyStore + +data BlockListStore m a where + Insert :: EmailKey -> BlockListStore m () + Exists :: EmailKey -> BlockListStore m Bool + Delete :: EmailKey -> BlockListStore m () + +makeSem ''BlockListStore diff --git a/services/brig/src/Brig/Effects/BlacklistStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/BlockListStore/Cassandra.hs similarity index 73% rename from services/brig/src/Brig/Effects/BlacklistStore/Cassandra.hs rename to libs/wire-subsystems/src/Wire/BlockListStore/Cassandra.hs index 45ada1cebc9..f00431d8201 100644 --- a/services/brig/src/Brig/Effects/BlacklistStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/BlockListStore/Cassandra.hs @@ -1,28 +1,28 @@ -module Brig.Effects.BlacklistStore.Cassandra - ( interpretBlacklistStoreToCassandra, +module Wire.BlockListStore.Cassandra + ( interpretBlockListStoreToCassandra, ) where -import Brig.Effects.BlacklistStore (BlacklistStore (..)) import Cassandra import Imports import Polysemy +import Wire.BlockListStore (BlockListStore (..)) import Wire.UserKeyStore -interpretBlacklistStoreToCassandra :: - forall m r a. - (MonadClient m, Member (Embed m) r) => - Sem (BlacklistStore ': r) a -> - Sem r a -interpretBlacklistStoreToCassandra = +interpretBlockListStoreToCassandra :: + forall r. + (Member (Embed IO) r) => + ClientState -> + InterpreterFor BlockListStore r +interpretBlockListStoreToCassandra casClient = interpret $ - embed @m . \case + embed @IO . runClient casClient . \case Insert uk -> insert uk Exists uk -> exists uk Delete uk -> delete uk -------------------------------------------------------------------------------- --- UserKey blacklisting +-- UserKey block listing insert :: (MonadClient m) => EmailKey -> m () insert uk = retry x5 $ write keyInsert (params LocalQuorum (Identity $ emailKeyUniq uk)) diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem.hs index 13f0093ddd8..f397bce9441 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem.hs @@ -3,6 +3,7 @@ module Wire.EmailSubsystem where import Data.Code qualified as Code +import Data.Id import Imports import Polysemy import Wire.API.Locale @@ -11,15 +12,19 @@ import Wire.API.User.Activation (ActivationCode, ActivationKey) import Wire.API.User.Client (Client (..)) data EmailSubsystem m a where - SendPasswordResetMail :: Email -> PasswordResetPair -> Maybe Locale -> EmailSubsystem m () - SendVerificationMail :: Email -> ActivationKey -> ActivationCode -> Maybe Locale -> EmailSubsystem m () - SendCreateScimTokenVerificationMail :: Email -> Code.Value -> Maybe Locale -> EmailSubsystem m () - SendLoginVerificationMail :: Email -> Code.Value -> Maybe Locale -> EmailSubsystem m () - SendActivationMail :: Email -> Name -> ActivationKey -> ActivationCode -> Maybe Locale -> EmailSubsystem m () - SendEmailAddressUpdateMail :: Email -> Name -> ActivationKey -> ActivationCode -> Maybe Locale -> EmailSubsystem m () - SendNewClientEmail :: Email -> Name -> Client -> Locale -> EmailSubsystem m () - SendAccountDeletionEmail :: Email -> Name -> Code.Key -> Code.Value -> Locale -> EmailSubsystem m () - SendTeamActivationMail :: Email -> Name -> ActivationKey -> ActivationCode -> Maybe Locale -> Text -> EmailSubsystem m () - SendTeamDeletionVerificationMail :: Email -> Code.Value -> Maybe Locale -> EmailSubsystem m () + SendPasswordResetMail :: EmailAddress -> PasswordResetPair -> Maybe Locale -> EmailSubsystem m () + SendVerificationMail :: EmailAddress -> ActivationKey -> ActivationCode -> Maybe Locale -> EmailSubsystem m () + SendCreateScimTokenVerificationMail :: EmailAddress -> Code.Value -> Maybe Locale -> EmailSubsystem m () + SendLoginVerificationMail :: EmailAddress -> Code.Value -> Maybe Locale -> EmailSubsystem m () + SendActivationMail :: EmailAddress -> Name -> ActivationKey -> ActivationCode -> Maybe Locale -> EmailSubsystem m () + SendEmailAddressUpdateMail :: EmailAddress -> Name -> ActivationKey -> ActivationCode -> Maybe Locale -> EmailSubsystem m () + SendNewClientEmail :: EmailAddress -> Name -> Client -> Locale -> EmailSubsystem m () + SendAccountDeletionEmail :: EmailAddress -> Name -> Code.Key -> Code.Value -> Locale -> EmailSubsystem m () + SendTeamActivationMail :: EmailAddress -> Name -> ActivationKey -> ActivationCode -> Maybe Locale -> Text -> EmailSubsystem m () + SendTeamDeletionVerificationMail :: EmailAddress -> Code.Value -> Maybe Locale -> EmailSubsystem m () + -- | send invitation to an unknown email address. + SendTeamInvitationMail :: EmailAddress -> TeamId -> EmailAddress -> InvitationCode -> Maybe Locale -> EmailSubsystem m Text + -- | send invitation to an email address associated with a personal user account. + SendTeamInvitationMailPersonalUser :: EmailAddress -> TeamId -> EmailAddress -> InvitationCode -> Maybe Locale -> EmailSubsystem m Text makeSem ''EmailSubsystem diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs index 519c5101cb0..ef03dadc7db 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs @@ -7,6 +7,7 @@ module Wire.EmailSubsystem.Interpreter where import Data.Code qualified as Code +import Data.Id import Data.Json.Util import Data.Range (fromRange) import Data.Text qualified as Text @@ -24,18 +25,20 @@ import Wire.EmailSending (EmailSending, sendMail) import Wire.EmailSubsystem import Wire.EmailSubsystem.Template -emailSubsystemInterpreter :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> InterpreterFor EmailSubsystem r -emailSubsystemInterpreter tpls branding = interpret \case - SendPasswordResetMail email (key, code) mLocale -> sendPasswordResetMailImpl tpls branding email key code mLocale - SendVerificationMail email key code mLocale -> sendVerificationMailImpl tpls branding email key code mLocale - SendTeamDeletionVerificationMail email code mLocale -> sendTeamDeletionVerificationMailImpl tpls branding email code mLocale - SendCreateScimTokenVerificationMail email code mLocale -> sendCreateScimTokenVerificationMailImpl tpls branding email code mLocale - SendLoginVerificationMail email code mLocale -> sendLoginVerificationMailImpl tpls branding email code mLocale - SendActivationMail email name key code mLocale -> sendActivationMailImpl tpls branding email name key code mLocale - SendEmailAddressUpdateMail email name key code mLocale -> sendEmailAddressUpdateMailImpl tpls branding email name key code mLocale - SendTeamActivationMail email name key code mLocale teamName -> sendTeamActivationMailImpl tpls branding email name key code mLocale teamName - SendNewClientEmail email name client locale -> sendNewClientEmailImpl tpls branding email name client locale - SendAccountDeletionEmail email name key code locale -> sendAccountDeletionEmailImpl tpls branding email name key code locale +emailSubsystemInterpreter :: (Member EmailSending r) => Localised UserTemplates -> Localised TeamTemplates -> TemplateBranding -> InterpreterFor EmailSubsystem r +emailSubsystemInterpreter userTpls teamTpls branding = interpret \case + SendPasswordResetMail email (key, code) mLocale -> sendPasswordResetMailImpl userTpls branding email key code mLocale + SendVerificationMail email key code mLocale -> sendVerificationMailImpl userTpls branding email key code mLocale + SendTeamDeletionVerificationMail email code mLocale -> sendTeamDeletionVerificationMailImpl userTpls branding email code mLocale + SendCreateScimTokenVerificationMail email code mLocale -> sendCreateScimTokenVerificationMailImpl userTpls branding email code mLocale + SendLoginVerificationMail email code mLocale -> sendLoginVerificationMailImpl userTpls branding email code mLocale + SendActivationMail email name key code mLocale -> sendActivationMailImpl userTpls branding email name key code mLocale + SendEmailAddressUpdateMail email name key code mLocale -> sendEmailAddressUpdateMailImpl userTpls branding email name key code mLocale + SendTeamActivationMail email name key code mLocale teamName -> sendTeamActivationMailImpl userTpls branding email name key code mLocale teamName + SendNewClientEmail email name client locale -> sendNewClientEmailImpl userTpls branding email name client locale + SendAccountDeletionEmail email name key code locale -> sendAccountDeletionEmailImpl userTpls branding email name key code locale + SendTeamInvitationMail email tid from code loc -> sendTeamInvitationMailImpl teamTpls branding email tid from code loc + SendTeamInvitationMailPersonalUser email tid from code loc -> sendTeamInvitationMailPersonalUserImpl teamTpls branding email tid from code loc ------------------------------------------------------------------------------- -- Verification Email for @@ -47,7 +50,7 @@ sendTeamDeletionVerificationMailImpl :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> - Email -> + EmailAddress -> Code.Value -> Maybe Locale -> Sem r () @@ -59,7 +62,7 @@ sendCreateScimTokenVerificationMailImpl :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> - Email -> + EmailAddress -> Code.Value -> Maybe Locale -> Sem r () @@ -71,7 +74,7 @@ sendLoginVerificationMailImpl :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> - Email -> + EmailAddress -> Code.Value -> Maybe Locale -> Sem r () @@ -80,7 +83,7 @@ sendLoginVerificationMailImpl userTemplates branding email code mLocale = do sendMail $ renderSecondFactorVerificationEmail email code tpl branding renderSecondFactorVerificationEmail :: - Email -> + EmailAddress -> Code.Value -> SecondFactorVerificationEmailTemplate -> TemplateBranding -> @@ -114,7 +117,7 @@ sendActivationMailImpl :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> - Email -> + EmailAddress -> Name -> ActivationKey -> ActivationCode -> @@ -128,7 +131,7 @@ sendEmailAddressUpdateMailImpl :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> - Email -> + EmailAddress -> Name -> ActivationKey -> ActivationCode -> @@ -138,7 +141,7 @@ sendEmailAddressUpdateMailImpl userTemplates branding email name akey acode mLoc let tpl = activationEmailUpdate . snd $ forLocale mLocale userTemplates sendMail $ renderActivationMail email name akey acode tpl branding -renderActivationMail :: Email -> Name -> ActivationKey -> ActivationCode -> ActivationEmailTemplate -> TemplateBranding -> Mail +renderActivationMail :: EmailAddress -> Name -> ActivationKey -> ActivationCode -> ActivationEmailTemplate -> TemplateBranding -> Mail renderActivationMail email name akey@(ActivationKey key) acode@(ActivationCode code) ActivationEmailTemplate {..} branding = (emptyMail from) { mailTo = [to], @@ -184,7 +187,7 @@ sendTeamActivationMailImpl :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> - Email -> + EmailAddress -> Name -> ActivationKey -> ActivationCode -> @@ -195,7 +198,7 @@ sendTeamActivationMailImpl userTemplates branding email name akey acode mLocale let tpl = teamActivationEmail . snd $ forLocale mLocale userTemplates sendMail $ renderTeamActivationMail email name teamName akey acode tpl branding -renderTeamActivationMail :: Email -> Name -> Text -> ActivationKey -> ActivationCode -> TeamActivationEmailTemplate -> TemplateBranding -> Mail +renderTeamActivationMail :: EmailAddress -> Name -> Text -> ActivationKey -> ActivationCode -> TeamActivationEmailTemplate -> TemplateBranding -> Mail renderTeamActivationMail email name teamName akey@(ActivationKey key) acode@(ActivationCode code) TeamActivationEmailTemplate {..} branding = (emptyMail from) { mailTo = [to], @@ -229,7 +232,7 @@ sendVerificationMailImpl :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> - Email -> + EmailAddress -> ActivationKey -> ActivationCode -> Maybe Locale -> @@ -238,7 +241,7 @@ sendVerificationMailImpl userTemplates branding email akey acode mLocale = do let tpl = verificationEmail . snd $ forLocale mLocale userTemplates sendMail $ renderVerificationMail email akey acode tpl branding -renderVerificationMail :: Email -> ActivationKey -> ActivationCode -> VerificationEmailTemplate -> TemplateBranding -> Mail +renderVerificationMail :: EmailAddress -> ActivationKey -> ActivationCode -> VerificationEmailTemplate -> TemplateBranding -> Mail renderVerificationMail email akey acode VerificationEmailTemplate {..} branding = (emptyMail from) { mailTo = [to], @@ -269,7 +272,7 @@ sendPasswordResetMailImpl :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> - Email -> + EmailAddress -> PasswordResetKey -> PasswordResetCode -> Maybe Locale -> @@ -278,7 +281,7 @@ sendPasswordResetMailImpl userTemplates branding email pkey pcode mLocale = do let tpl = passwordResetEmail . snd $ forLocale mLocale userTemplates sendMail $ renderPwResetMail email pkey pcode tpl branding -renderPwResetMail :: Email -> PasswordResetKey -> PasswordResetCode -> PasswordResetEmailTemplate -> TemplateBranding -> Mail +renderPwResetMail :: EmailAddress -> PasswordResetKey -> PasswordResetCode -> PasswordResetEmailTemplate -> TemplateBranding -> Mail renderPwResetMail email pkey pcode PasswordResetEmailTemplate {..} branding = (emptyMail from) { mailTo = [to], @@ -315,7 +318,7 @@ sendNewClientEmailImpl :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> - Email -> + EmailAddress -> Name -> Client -> Locale -> @@ -324,7 +327,7 @@ sendNewClientEmailImpl userTemplates branding email name client locale = do let tpl = newClientEmail . snd $ forLocale (Just locale) userTemplates sendMail $ renderNewClientEmail email name locale client tpl branding -renderNewClientEmail :: Email -> Name -> Locale -> Client -> NewClientEmailTemplate -> TemplateBranding -> Mail +renderNewClientEmail :: EmailAddress -> Name -> Locale -> Client -> NewClientEmailTemplate -> TemplateBranding -> Mail renderNewClientEmail email name locale Client {..} NewClientEmailTemplate {..} branding = (emptyMail from) { mailTo = [to], @@ -341,8 +344,8 @@ renderNewClientEmail email name locale Client {..} NewClientEmailTemplate {..} b html = renderHtmlWithBranding newClientEmailBodyHtml replace branding subj = renderTextWithBranding newClientEmailSubject replace branding replace "name" = fromName name - replace "label" = fromMaybe "N/A" clientLabel - replace "model" = fromMaybe "N/A" clientModel + replace "label" = fromMaybe defRequestId clientLabel + replace "model" = fromMaybe defRequestId clientModel replace "date" = formatDateTime "%A %e %B %Y, %H:%M - %Z" @@ -357,7 +360,7 @@ sendAccountDeletionEmailImpl :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> - Email -> + EmailAddress -> Name -> Code.Key -> Code.Value -> @@ -367,7 +370,7 @@ sendAccountDeletionEmailImpl userTemplates branding email name key code locale = let tpl = deletionEmail . snd $ forLocale (Just locale) userTemplates sendMail $ renderDeletionEmail email name key code tpl branding -renderDeletionEmail :: Email -> Name -> Code.Key -> Code.Value -> DeletionEmailTemplate -> TemplateBranding -> Mail +renderDeletionEmail :: EmailAddress -> Name -> Code.Key -> Code.Value -> DeletionEmailTemplate -> TemplateBranding -> Mail renderDeletionEmail email name cKey cValue DeletionEmailTemplate {..} branding = (emptyMail from) { mailTo = [to], @@ -395,6 +398,65 @@ renderDeletionEmail email name cKey cValue DeletionEmailTemplate {..} branding = replace2 "code" = code replace2 x = x +------------------------------------------------------------------------------- +-- Invitation Email + +sendTeamInvitationMailImpl :: (Member EmailSending r) => Localised TeamTemplates -> TemplateBranding -> EmailAddress -> TeamId -> EmailAddress -> InvitationCode -> Maybe Locale -> Sem r Text +sendTeamInvitationMailImpl teamTemplates branding to tid from code loc = do + let tpl = invitationEmail . snd $ forLocale loc teamTemplates + mail = InvitationEmail to tid code from + (renderedMail, renderedInvitaitonUrl) = renderInvitationEmail mail tpl branding + sendMail renderedMail + pure renderedInvitaitonUrl + +sendTeamInvitationMailPersonalUserImpl :: (Member EmailSending r) => Localised TeamTemplates -> TemplateBranding -> EmailAddress -> TeamId -> EmailAddress -> InvitationCode -> Maybe Locale -> Sem r Text +sendTeamInvitationMailPersonalUserImpl teamTemplates branding to tid from code loc = do + let tpl = existingUserInvitationEmail . snd $ forLocale loc teamTemplates + mail = InvitationEmail to tid code from + (renderedMail, renderedInvitaitonUrl) = renderInvitationEmail mail tpl branding + sendMail renderedMail + pure renderedInvitaitonUrl + +data InvitationEmail = InvitationEmail + { invTo :: !EmailAddress, + invTeamId :: !TeamId, + invInvCode :: !InvitationCode, + invInviter :: !EmailAddress + } + +renderInvitationEmail :: InvitationEmail -> InvitationEmailTemplate -> TemplateBranding -> (Mail, Text) +renderInvitationEmail InvitationEmail {..} InvitationEmailTemplate {..} branding = + ( (emptyMail from) + { mailTo = [to], + mailHeaders = + [ ("Subject", toStrict subj), + ("X-Zeta-Purpose", "TeamInvitation"), + ("X-Zeta-Code", Ascii.toText code) + ], + mailParts = [[plainPart txt, htmlPart html]] + }, + invitationUrl + ) + where + (InvitationCode code) = invInvCode + from = Address (Just invitationEmailSenderName) (fromEmail invitationEmailSender) + to = Address Nothing (fromEmail invTo) + txt = renderTextWithBranding invitationEmailBodyText replace branding + html = renderHtmlWithBranding invitationEmailBodyHtml replace branding + subj = renderTextWithBranding invitationEmailSubject replace branding + invitationUrl = renderInvitationUrl invitationEmailUrl invTeamId invInvCode branding + replace "url" = invitationUrl + replace "inviter" = fromEmail invInviter + replace x = x + +renderInvitationUrl :: Template -> TeamId -> InvitationCode -> TemplateBranding -> Text +renderInvitationUrl t tid (InvitationCode c) branding = + toStrict $ renderTextWithBranding t replace branding + where + replace "team" = idToText tid + replace "code" = Ascii.toText c + replace x = x + ------------------------------------------------------------------------------- -- MIME Conversions @@ -403,7 +465,7 @@ renderDeletionEmail email name cKey cValue DeletionEmailTemplate {..} branding = -- in SMTP, which is a safe limit for most mail servers (including those of -- Amazon SES). The display name is only included if it fits within that -- limit, otherwise it is dropped. -mkMimeAddress :: Name -> Email -> Address +mkMimeAddress :: Name -> EmailAddress -> Address mkMimeAddress name email = let addr = Address (Just (fromName name)) (fromEmail email) in if Text.compareLength (renderAddress addr) 320 == GT diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs index 9c123e1c0e3..ca79185fccc 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs @@ -18,30 +18,10 @@ -- with this program. If not, see . module Wire.EmailSubsystem.Template - ( Localised (..), - TemplateBranding, - forLocale, - - -- * templates - UserTemplates (..), - ActivationSmsTemplate (..), - VerificationEmailTemplate (..), - ActivationEmailTemplate (..), - TeamActivationEmailTemplate (..), - ActivationCallTemplate (..), - PasswordResetSmsTemplate (..), - PasswordResetEmailTemplate (..), - LoginSmsTemplate (..), - LoginCallTemplate (..), - DeletionSmsTemplate (..), - DeletionEmailTemplate (..), - NewClientEmailTemplate (..), - SecondFactorVerificationEmailTemplate (..), + ( module Wire.EmailSubsystem.Template, -- * Re-exports Template, - renderTextWithBranding, - renderHtmlWithBranding, ) where @@ -126,7 +106,7 @@ data VerificationEmailTemplate = VerificationEmailTemplate verificationEmailSubject :: Template, verificationEmailBodyText :: Template, verificationEmailBodyHtml :: Template, - verificationEmailSender :: Email, + verificationEmailSender :: EmailAddress, verificationEmailSenderName :: Text } @@ -135,7 +115,7 @@ data ActivationEmailTemplate = ActivationEmailTemplate activationEmailSubject :: Template, activationEmailBodyText :: Template, activationEmailBodyHtml :: Template, - activationEmailSender :: Email, + activationEmailSender :: EmailAddress, activationEmailSenderName :: Text } @@ -144,7 +124,7 @@ data TeamActivationEmailTemplate = TeamActivationEmailTemplate teamActivationEmailSubject :: Template, teamActivationEmailBodyText :: Template, teamActivationEmailBodyHtml :: Template, - teamActivationEmailSender :: Email, + teamActivationEmailSender :: EmailAddress, teamActivationEmailSenderName :: Text } @@ -153,7 +133,7 @@ data DeletionEmailTemplate = DeletionEmailTemplate deletionEmailSubject :: Template, deletionEmailBodyText :: Template, deletionEmailBodyHtml :: Template, - deletionEmailSender :: Email, + deletionEmailSender :: EmailAddress, deletionEmailSenderName :: Text } @@ -162,7 +142,7 @@ data PasswordResetEmailTemplate = PasswordResetEmailTemplate passwordResetEmailSubject :: Template, passwordResetEmailBodyText :: Template, passwordResetEmailBodyHtml :: Template, - passwordResetEmailSender :: Email, + passwordResetEmailSender :: EmailAddress, passwordResetEmailSenderName :: Text } @@ -191,7 +171,7 @@ data NewClientEmailTemplate = NewClientEmailTemplate { newClientEmailSubject :: Template, newClientEmailBodyText :: Template, newClientEmailBodyHtml :: Template, - newClientEmailSender :: Email, + newClientEmailSender :: EmailAddress, newClientEmailSenderName :: Text } @@ -199,6 +179,60 @@ data SecondFactorVerificationEmailTemplate = SecondFactorVerificationEmailTempla { sndFactorVerificationEmailSubject :: Template, sndFactorVerificationEmailBodyText :: Template, sndFactorVerificationEmailBodyHtml :: Template, - sndFactorVerificationEmailSender :: Email, + sndFactorVerificationEmailSender :: EmailAddress, sndFactorVerificationEmailSenderName :: Text } + +data InvitationEmailTemplate = InvitationEmailTemplate + { invitationEmailUrl :: !Template, + invitationEmailSubject :: !Template, + invitationEmailBodyText :: !Template, + invitationEmailBodyHtml :: !Template, + invitationEmailSender :: !EmailAddress, + invitationEmailSenderName :: !Text + } + +data CreatorWelcomeEmailTemplate = CreatorWelcomeEmailTemplate + { creatorWelcomeEmailUrl :: !Text, + creatorWelcomeEmailSubject :: !Template, + creatorWelcomeEmailBodyText :: !Template, + creatorWelcomeEmailBodyHtml :: !Template, + creatorWelcomeEmailSender :: !EmailAddress, + creatorWelcomeEmailSenderName :: !Text + } + +data MemberWelcomeEmailTemplate = MemberWelcomeEmailTemplate + { memberWelcomeEmailUrl :: !Text, + memberWelcomeEmailSubject :: !Template, + memberWelcomeEmailBodyText :: !Template, + memberWelcomeEmailBodyHtml :: !Template, + memberWelcomeEmailSender :: !EmailAddress, + memberWelcomeEmailSenderName :: !Text + } + +data PersonalUserMemberWelcomeEmailTemplate = PersonalUserMemberWelcomeEmailTemplate + { personalUserMemberWelcomeEmailUrl :: !Text, + personalUserMemberWelcomeEmailSubject :: !Template, + personalUserMemberWelcomeEmailBodyText :: !Template, + personalUserMemberWelcomeEmailBodyHtml :: !Template, + personalUserMemberWelcomeEmailSender :: !EmailAddress, + personalUserMemberWelcomeEmailSenderName :: !Text + } + +data PersonalUserCreatorWelcomeEmailTemplate = PersonalUserCreatorWelcomeEmailTemplate + { personalUserCreatorWelcomeEmailUrl :: !Text, + personalUserCreatorWelcomeEmailSubject :: !Template, + personalUserCreatorWelcomeEmailBodyText :: !Template, + personalUserCreatorWelcomeEmailBodyHtml :: !Template, + personalUserCreatorWelcomeEmailSender :: !EmailAddress, + personalUserCreatorWelcomeEmailSenderName :: !Text + } + +data TeamTemplates = TeamTemplates + { invitationEmail :: !InvitationEmailTemplate, + existingUserInvitationEmail :: !InvitationEmailTemplate, + creatorWelcomeEmail :: !CreatorWelcomeEmailTemplate, + memberWelcomeEmail :: !MemberWelcomeEmailTemplate, + personalUserMemberWelcomeEmail :: !PersonalUserMemberWelcomeEmailTemplate, + personalUserCreatorWelcomeEmail :: !PersonalUserCreatorWelcomeEmailTemplate + } diff --git a/libs/wire-subsystems/src/Wire/Error.hs b/libs/wire-subsystems/src/Wire/Error.hs index 1710571d161..ffe680d98d1 100644 --- a/libs/wire-subsystems/src/Wire/Error.hs +++ b/libs/wire-subsystems/src/Wire/Error.hs @@ -24,10 +24,6 @@ errorLabel :: HttpError -> LText errorLabel (StdError e) = Wai.label e errorLabel (RichError e _ _) = Wai.label e -errorStatus :: HttpError -> Status -errorStatus (StdError e) = Wai.code e -errorStatus (RichError e _ _) = Wai.code e - instance ToJSON HttpError where toJSON (StdError e) = toJSON e toJSON (RichError e x _) = case (toJSON e, toJSON x) of diff --git a/libs/wire-subsystems/src/Wire/Events.hs b/libs/wire-subsystems/src/Wire/Events.hs new file mode 100644 index 00000000000..57a48e4ac72 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/Events.hs @@ -0,0 +1,14 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.Events where + +import Data.Id +import Imports +import Polysemy +import Wire.API.UserEvent + +data Events m a where + GenerateUserEvent :: UserId -> Maybe ConnId -> UserEvent -> Events m () + GeneratePropertyEvent :: UserId -> ConnId -> PropertyEvent -> Events m () + +makeSem ''Events diff --git a/libs/wire-subsystems/src/Wire/FederationAPIAccess/Interpreter.hs b/libs/wire-subsystems/src/Wire/FederationAPIAccess/Interpreter.hs index 8520f11ff69..39f99b10e64 100644 --- a/libs/wire-subsystems/src/Wire/FederationAPIAccess/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/FederationAPIAccess/Interpreter.hs @@ -22,6 +22,15 @@ data FederationAPIAccessConfig = FederationAPIAccessConfig type FederatedActionRunner fedM r = forall c x. Domain -> fedM c x -> Sem r (Either FederationError x) +noFederationAPIAccess :: + forall r fedM. + (Member (Concurrency 'Unsafe) r) => + InterpreterFor (FederationAPIAccess fedM) r +noFederationAPIAccess = + interpretFederationAPIAccessGeneral + (\_ _ -> pure $ Left FederationNotConfigured) + (pure False) + interpretFederationAPIAccess :: forall r. (Member (Embed IO) r, Member (Concurrency 'Unsafe) r) => diff --git a/services/brig/src/Brig/Effects/FederationConfigStore.hs b/libs/wire-subsystems/src/Wire/FederationConfigStore.hs similarity index 90% rename from services/brig/src/Brig/Effects/FederationConfigStore.hs rename to libs/wire-subsystems/src/Wire/FederationConfigStore.hs index 07ace482740..ead299d37c0 100644 --- a/services/brig/src/Brig/Effects/FederationConfigStore.hs +++ b/libs/wire-subsystems/src/Wire/FederationConfigStore.hs @@ -1,6 +1,6 @@ {-# LANGUAGE TemplateHaskell #-} -module Brig.Effects.FederationConfigStore where +module Wire.FederationConfigStore where import Data.Domain import Data.Id @@ -24,6 +24,8 @@ data AddFederationRemoteTeamResult | AddFederationRemoteTeamDomainNotFound | AddFederationRemoteTeamRestrictionAllowAll +-- FUTUREWORK: This store effect is more than just a store, +-- we should break it up in business logic and store data FederationConfigStore m a where GetFederationConfig :: Domain -> FederationConfigStore m (Maybe FederationDomainConfig) GetFederationConfigs :: FederationConfigStore m FederationDomainConfigs diff --git a/services/brig/src/Brig/Effects/FederationConfigStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/FederationConfigStore/Cassandra.hs similarity index 97% rename from services/brig/src/Brig/Effects/FederationConfigStore/Cassandra.hs rename to libs/wire-subsystems/src/Wire/FederationConfigStore/Cassandra.hs index 32b13005e25..2038fc697ed 100644 --- a/services/brig/src/Brig/Effects/FederationConfigStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/FederationConfigStore/Cassandra.hs @@ -15,14 +15,13 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Brig.Effects.FederationConfigStore.Cassandra +module Wire.FederationConfigStore.Cassandra ( interpretFederationDomainConfig, remotesMapFromCfgFile, AddFederationRemoteResult (..), ) where -import Brig.Effects.FederationConfigStore import Cassandra import Control.Exception (ErrorCall (ErrorCall)) import Control.Lens @@ -34,8 +33,10 @@ import Data.Qualified import Database.CQL.Protocol (SerialConsistency (LocalSerialConsistency), serialConsistency) import Imports import Polysemy +import Polysemy.Embed import Wire.API.Routes.FederationDomainConfig import Wire.API.User.Search +import Wire.FederationConfigStore -- | Interpreter for getting the federation config from the database and the config file. -- The config file is injected into the interpreter and has precedence over the database. @@ -45,17 +46,17 @@ import Wire.API.User.Search -- If a domain is configured in the config file, it is not allowed to add a team restriction to it in the database. -- In the future the config file will be removed and the database will be the only source of truth. interpretFederationDomainConfig :: - forall m r a. - ( MonadClient m, - Member (Embed m) r + forall r a. + ( Member (Embed IO) r ) => + ClientState -> Maybe FederationStrategy -> Map Domain FederationDomainConfig -> Sem (FederationConfigStore ': r) a -> Sem r a -interpretFederationDomainConfig mFedStrategy fedCfgs = +interpretFederationDomainConfig casClient mFedStrategy fedCfgs = interpret $ - embed @m . \case + runEmbedded (runClient casClient) . embed . \case GetFederationConfig d -> getFederationConfig' fedCfgs d GetFederationConfigs -> getFederationConfigs' mFedStrategy fedCfgs AddFederationConfig cnf -> addFederationConfig' fedCfgs cnf diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs index b039bff1303..859fe913628 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs @@ -31,6 +31,7 @@ import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Team import Wire.API.Team.Conversation qualified as Conv import Wire.API.Team.Feature +import Wire.API.Team.LegalHold import Wire.API.Team.Member qualified as Team import Wire.API.Team.Role import Wire.API.Team.SearchVisibility @@ -67,11 +68,12 @@ data GalleyAPIAccess m a where AddTeamMember :: UserId -> TeamId -> - (Maybe (UserId, UTCTimeMillis), Role) -> + Maybe (UserId, UTCTimeMillis) -> + Role -> GalleyAPIAccess m Bool CreateTeam :: UserId -> - BindingNewTeam -> + NewTeam -> TeamId -> GalleyAPIAccess m () GetTeamMember :: @@ -92,7 +94,9 @@ data GalleyAPIAccess m a where GalleyAPIAccess m Team.TeamName GetTeamLegalHoldStatus :: TeamId -> - GalleyAPIAccess m (WithStatus LegalholdConfig) + GalleyAPIAccess m (LockableFeature LegalholdConfig) + GetUserLegalholdStatus :: + Local UserId -> TeamId -> GalleyAPIAccess m UserLegalHoldStatusResponse GetTeamSearchVisibility :: TeamId -> GalleyAPIAccess m TeamSearchVisibility @@ -105,9 +109,15 @@ data GalleyAPIAccess m a where TeamId -> UserId -> GalleyAPIAccess m Bool - GetAllFeatureConfigsForUser :: + GetAllTeamFeaturesForUser :: Maybe UserId -> - GalleyAPIAccess m AllFeatureConfigs + GalleyAPIAccess m AllTeamFeatures + GetFeatureConfigForTeam :: + ( IsFeatureConfig feature, + Typeable feature + ) => + TeamId -> + GalleyAPIAccess m (LockableFeature feature) GetVerificationCodeEnabled :: TeamId -> GalleyAPIAccess m Bool diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs index e05584e9a36..ba9538f12de 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs @@ -46,6 +46,7 @@ import Wire.API.Routes.Version import Wire.API.Team import Wire.API.Team.Conversation qualified as Conv import Wire.API.Team.Feature +import Wire.API.Team.LegalHold import Wire.API.Team.Member as Member import Wire.API.Team.Role import Wire.API.Team.SearchVisibility @@ -71,7 +72,7 @@ interpretGalleyAPIAccessToRpc disabledVersions galleyEndpoint = GetTeamConv id' id'' id'2 -> getTeamConv v id' id'' id'2 NewClient id' ci -> newClient id' ci CheckUserCanJoinTeam id' -> checkUserCanJoinTeam id' - AddTeamMember id' id'' x0 -> addTeamMember id' id'' x0 + AddTeamMember id' id'' a b -> addTeamMember id' id'' a b CreateTeam id' bnt id'' -> createTeam id' bnt id'' GetTeamMember id' id'' -> getTeamMember id' id'' GetTeamMembers id' -> getTeamMembers id' @@ -80,15 +81,35 @@ interpretGalleyAPIAccessToRpc disabledVersions galleyEndpoint = GetTeamName id' -> getTeamName id' GetTeamLegalHoldStatus id' -> getTeamLegalHoldStatus id' GetTeamSearchVisibility id' -> getTeamSearchVisibility id' + GetFeatureConfigForTeam tid -> getFeatureConfigForTeam tid + GetUserLegalholdStatus id' tid -> getUserLegalholdStatus id' tid ChangeTeamStatus id' ts m_al -> changeTeamStatus id' ts m_al MemberIsTeamOwner id' id'' -> memberIsTeamOwner id' id'' - GetAllFeatureConfigsForUser m_id' -> getAllFeatureConfigsForUser m_id' + GetAllTeamFeaturesForUser m_id' -> getAllTeamFeaturesForUser m_id' GetVerificationCodeEnabled id' -> getVerificationCodeEnabled id' GetExposeInvitationURLsToTeamAdmin id' -> getTeamExposeInvitationURLsToTeamAdmin id' IsMLSOne2OneEstablished lusr qother -> checkMLSOne2OneEstablished lusr qother UnblockConversation lusr mconn qcnv -> unblockConversation v lusr mconn qcnv GetEJPDConvInfo uid -> getEJPDConvInfo uid +getUserLegalholdStatus :: + ( Member TinyLog r, + Member (Error ParseException) r, + Member Rpc r + ) => + Local UserId -> + TeamId -> + Sem (Input Endpoint : r) UserLegalHoldStatusResponse +getUserLegalholdStatus luid tid = do + debug $ + remote "galley" + . msg (val "get legalhold user status") + decodeBodyOrThrow "galley" =<< galleyRequest do + method GET + . paths ["teams", toByteString' tid, "legalhold", toByteString' (tUnqualified luid)] + . zUser (tUnqualified luid) + . expect2xx + galleyRequest :: (Member Rpc r, Member (Input Endpoint) r) => (Request -> Request) -> Sem r (Response (Maybe LByteString)) galleyRequest req = do ep <- input @@ -234,9 +255,10 @@ addTeamMember :: ) => UserId -> TeamId -> - (Maybe (UserId, UTCTimeMillis), Role) -> + Maybe (UserId, UTCTimeMillis) -> + Role -> Sem r Bool -addTeamMember u tid (minvmeta, role) = do +addTeamMember u tid minvmeta role = do debug $ remote "galley" . msg (val "Adding member to team") @@ -262,7 +284,7 @@ createTeam :: Member TinyLog r ) => UserId -> - BindingNewTeam -> + NewTeam -> TeamId -> Sem r () createTeam u t teamid = do @@ -403,7 +425,7 @@ getTeamLegalHoldStatus :: Member TinyLog r ) => TeamId -> - Sem r (WithStatus LegalholdConfig) + Sem r (LockableFeature LegalholdConfig) getTeamLegalHoldStatus tid = do debug $ remote "galley" . msg (val "Get legalhold settings") galleyRequest req >>= decodeBodyOrThrow "galley" @@ -432,6 +454,25 @@ getTeamSearchVisibility tid = . paths ["i", "teams", toByteString' tid, "search-visibility"] . expect2xx +getFeatureConfigForTeam :: + forall feature r. + ( IsFeatureConfig feature, + Typeable feature, + Member TinyLog r, + Member Rpc r, + Member (Error ParseException) r + ) => + TeamId -> + Sem (Input Endpoint : r) (LockableFeature feature) +getFeatureConfigForTeam tid = do + debug $ remote "galley" . msg (val "Get feature config for team") + galleyRequest req >>= decodeBodyOrThrow "galley" + where + req = + method GET + . paths ["i", "teams", toByteString' tid, "features", featureNameBS @feature] + . expect2xx + getVerificationCodeEnabled :: ( Member (Error ParseException) r, Member Rpc r, @@ -443,7 +484,7 @@ getVerificationCodeEnabled :: getVerificationCodeEnabled tid = do debug $ remote "galley" . msg (val "Get snd factor password challenge settings") response <- galleyRequest req - status <- wsStatus <$> decodeBodyOrThrow @(WithStatus SndFactorPasswordChallengeConfig) "galley" response + status <- (.status) <$> decodeBodyOrThrow @(LockableFeature SndFactorPasswordChallengeConfig) "galley" response case status of FeatureStatusEnabled -> pure True FeatureStatusDisabled -> pure False @@ -456,11 +497,11 @@ getVerificationCodeEnabled tid = do decodeBodyOrThrow :: forall a r. (Typeable a, FromJSON a, Member (Error ParseException) r) => Text -> Response (Maybe BL.ByteString) -> Sem r a decodeBodyOrThrow ctx r = either (throw . ParseException ctx) pure (responseJsonEither r) -getAllFeatureConfigsForUser :: +getAllTeamFeaturesForUser :: (Member Rpc r, Member (Input Endpoint) r) => Maybe UserId -> - Sem r AllFeatureConfigs -getAllFeatureConfigsForUser mbUserId = + Sem r AllTeamFeatures +getAllTeamFeaturesForUser mbUserId = responseJsonUnsafe <$> galleyRequest ( method GET @@ -500,7 +541,7 @@ getTeamExposeInvitationURLsToTeamAdmin :: getTeamExposeInvitationURLsToTeamAdmin tid = do debug $ remote "galley" . msg (val "Get expose invitation URLs to team admin settings") response <- galleyRequest req - status <- wsStatus <$> decodeBodyOrThrow @(WithStatus ExposeInvitationURLsToTeamAdminConfig) "galley" response + status <- (.status) <$> decodeBodyOrThrow @(LockableFeature ExposeInvitationURLsToTeamAdminConfig) "galley" response case status of FeatureStatusEnabled -> pure ShowInvitationUrl FeatureStatusDisabled -> pure HideInvitationUrl @@ -534,8 +575,7 @@ checkMLSOne2OneEstablished self (Qualified other otherDomain) = do method GET . paths [ "i", - "conversations", - "mls-one2one", + "mls-one2one-conversations", toByteString' otherDomain, toByteString' other, "established" diff --git a/libs/wire-subsystems/src/Wire/GundeckAPIAccess.hs b/libs/wire-subsystems/src/Wire/GundeckAPIAccess.hs index 1d0666880cf..c153cf22364 100644 --- a/libs/wire-subsystems/src/Wire/GundeckAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/GundeckAPIAccess.hs @@ -5,11 +5,11 @@ module Wire.GundeckAPIAccess where import Bilge import Data.ByteString.Conversion import Data.Id -import Gundeck.Types.Push.V2 qualified as V2 import Imports import Network.HTTP.Types import Polysemy import Util.Options +import Wire.API.Push.V2 qualified as V2 import Wire.Rpc data GundeckAPIAccess m a where diff --git a/libs/wire-subsystems/src/Wire/HashPassword.hs b/libs/wire-subsystems/src/Wire/HashPassword.hs index 54c65c3ee74..1f58daf794d 100644 --- a/libs/wire-subsystems/src/Wire/HashPassword.hs +++ b/libs/wire-subsystems/src/Wire/HashPassword.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} module Wire.HashPassword where @@ -5,14 +6,29 @@ module Wire.HashPassword where import Data.Misc import Imports import Polysemy -import Wire.API.Password (Password) +import Util.Options +import Wire.API.Password import Wire.API.Password qualified as Password data HashPassword m a where - HashPassword :: PlainTextPassword8 -> HashPassword m Password + HashPassword6 :: PlainTextPassword6 -> HashPassword m Password + HashPassword8 :: PlainTextPassword8 -> HashPassword m Password makeSem ''HashPassword -runHashPassword :: (Member (Embed IO) r) => InterpreterFor HashPassword r -runHashPassword = interpret $ \case - HashPassword pw -> liftIO $ Password.mkSafePasswordScrypt pw +runHashPassword :: + forall r. + ( Member (Embed IO) r + ) => + PasswordHashingOptions -> + InterpreterFor HashPassword r +runHashPassword opts = + interpret $ + \case + HashPassword6 pw6 -> hashFunction pw6 + HashPassword8 pw8 -> hashFunction pw8 + where + hashFunction :: PlainTextPassword' t -> Sem r Password + hashFunction = case opts of + PasswordHashingArgon2id o -> Password.mkSafePassword (argon2OptsFromHashingOpts o) + PasswordHashingScrypt -> Password.mkSafePasswordScrypt diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore.hs new file mode 100644 index 00000000000..c3fe401f4f8 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore.hs @@ -0,0 +1,45 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.IndexedUserStore where + +import Data.Id +import Database.Bloodhound qualified as ES +import Database.Bloodhound.Types hiding (SearchResult) +import Imports +import Polysemy +import Wire.API.Team.Size +import Wire.API.User.Search +import Wire.UserSearch.Types + +data IndexedUserStoreError + = IndexUpdateError ES.EsError + | IndexLookupError ES.EsError + | IndexError Text + deriving (Show) + +instance Exception IndexedUserStoreError + +data IndexedUserStore m a where + Upsert :: DocId -> UserDoc -> VersionControl -> IndexedUserStore m () + UpdateTeamSearchVisibilityInbound :: + TeamId -> + SearchVisibilityInbound -> + IndexedUserStore m () + -- | Will only be applied to main ES index and not the additional one + BulkUpsert :: [(DocId, UserDoc, VersionControl)] -> IndexedUserStore m () + DoesIndexExist :: IndexedUserStore m Bool + SearchUsers :: + UserId -> + Maybe TeamId -> + TeamSearchInfo -> + Text -> + Int -> + IndexedUserStore m (SearchResult UserDoc) + PaginateTeamMembers :: + BrowseTeamFilters -> + Int -> + Maybe PagingState -> + IndexedUserStore m (SearchResult UserDoc) + GetTeamSize :: TeamId -> IndexedUserStore m TeamSize + +makeSem ''IndexedUserStore diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk.hs new file mode 100644 index 00000000000..66969fe61d6 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.IndexedUserStore.Bulk where + +import Polysemy +import Wire.UserSearch.Migration + +-- | Increase this number any time you want to force reindexing. +expectedMigrationVersion :: MigrationVersion +expectedMigrationVersion = MigrationVersion 6 + +-- | Bulk operations, must not be used from any web handler +data IndexedUserStoreBulk m a where + -- | Only changes data if it is not updated since last update, use when users + -- need to be synced because of an outage, or migrating to a new ES instance. + SyncAllUsers :: IndexedUserStoreBulk m () + -- | Overwrite all users in the ES index, use it when trying to fix some + -- inconsistency or while introducing a new field in the mapping. + ForceSyncAllUsers :: IndexedUserStoreBulk m () + MigrateData :: IndexedUserStoreBulk m () + +makeSem ''IndexedUserStoreBulk diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs new file mode 100644 index 00000000000..26ccca02987 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs @@ -0,0 +1,133 @@ +module Wire.IndexedUserStore.Bulk.ElasticSearch where + +import Cassandra.Exec (paginateWithStateC) +import Conduit (ConduitT, runConduit, (.|)) +import Data.Conduit.Combinators qualified as Conduit +import Data.Id +import Data.Map qualified as Map +import Data.Set qualified as Set +import Database.Bloodhound qualified as ES +import Imports +import Polysemy +import Polysemy.Error +import Polysemy.TinyLog +import Polysemy.TinyLog qualified as Log +import System.Logger.Message qualified as Log +import Wire.API.Team.Feature +import Wire.GalleyAPIAccess +import Wire.IndexedUserStore (IndexedUserStore) +import Wire.IndexedUserStore qualified as IndexedUserStore +import Wire.IndexedUserStore.Bulk +import Wire.IndexedUserStore.MigrationStore +import Wire.IndexedUserStore.MigrationStore qualified as MigrationStore +import Wire.Sem.Concurrency (Concurrency, ConcurrencySafety (Unsafe), unsafePooledForConcurrentlyN) +import Wire.UserSearch.Migration +import Wire.UserSearch.Types +import Wire.UserStore +import Wire.UserStore.IndexUser + +interpretIndexedUserStoreBulk :: + ( Member TinyLog r, + Member UserStore r, + Member (Concurrency Unsafe) r, + Member GalleyAPIAccess r, + Member IndexedUserStore r, + Member (Error MigrationException) r, + Member IndexedUserMigrationStore r + ) => + InterpreterFor IndexedUserStoreBulk r +interpretIndexedUserStoreBulk = interpret \case + SyncAllUsers -> syncAllUsersImpl + ForceSyncAllUsers -> forceSyncAllUsersImpl + MigrateData -> migrateDataImpl + +syncAllUsersImpl :: + forall r. + ( Member UserStore r, + Member TinyLog r, + Member (Concurrency 'Unsafe) r, + Member GalleyAPIAccess r, + Member IndexedUserStore r + ) => + Sem r () +syncAllUsersImpl = syncAllUsersWithVersion ES.ExternalGT + +forceSyncAllUsersImpl :: + forall r. + ( Member UserStore r, + Member TinyLog r, + Member (Concurrency 'Unsafe) r, + Member GalleyAPIAccess r, + Member IndexedUserStore r + ) => + Sem r () +forceSyncAllUsersImpl = syncAllUsersWithVersion ES.ExternalGTE + +syncAllUsersWithVersion :: + forall r. + ( Member UserStore r, + Member TinyLog r, + Member (Concurrency 'Unsafe) r, + Member GalleyAPIAccess r, + Member IndexedUserStore r + ) => + (ES.ExternalDocVersion -> ES.VersionControl) -> + Sem r () +syncAllUsersWithVersion mkVersion = + runConduit $ + paginateWithStateC (getIndexUsersPaginated 1000) + .| logPage + .| mkUserDocs + .| Conduit.mapM_ IndexedUserStore.bulkUpsert + where + logPage :: ConduitT [IndexUser] [IndexUser] (Sem r) () + logPage = Conduit.iterM $ \page -> do + info $ + Log.field "size" (length page) + . Log.msg (Log.val "Reindex: processing C* page") + + mkUserDocs :: ConduitT [IndexUser] [(ES.DocId, UserDoc, ES.VersionControl)] (Sem r) () + mkUserDocs = Conduit.mapM $ \page -> do + let teamIds = + Set.fromList $ + mapMaybe (fmap value . ((.teamId))) page + visMap <- fmap Map.fromList . unsafePooledForConcurrentlyN 16 teamIds $ \t -> + (t,) <$> teamSearchVisibilityInbound t + let vis indexUser = fromMaybe defaultSearchVisibilityInbound $ (flip Map.lookup visMap . value =<< indexUser.teamId) + mkUserDoc indexUser = indexUserToDoc (vis indexUser) indexUser + mkDocVersion = mkVersion . ES.ExternalDocVersion . docVersion . indexUserToVersion + pure $ map (\u -> (userIdToDocId u.userId, mkUserDoc u, mkDocVersion u)) page + +migrateDataImpl :: + ( Member IndexedUserStore r, + Member (Error MigrationException) r, + Member IndexedUserMigrationStore r, + Member UserStore r, + Member (Concurrency Unsafe) r, + Member GalleyAPIAccess r, + Member TinyLog r + ) => + Sem r () +migrateDataImpl = do + unlessM IndexedUserStore.doesIndexExist $ + throw TargetIndexAbsent + MigrationStore.ensureMigrationIndex + foundVersion <- MigrationStore.getLatestMigrationVersion + if expectedMigrationVersion > foundVersion + then do + Log.info $ + Log.msg (Log.val "Migration necessary.") + . Log.field "expectedVersion" expectedMigrationVersion + . Log.field "foundVersion" foundVersion + forceSyncAllUsersImpl + MigrationStore.persistMigrationVersion expectedMigrationVersion + else do + Log.info $ + Log.msg (Log.val "No migration necessary.") + . Log.field "expectedVersion" expectedMigrationVersion + . Log.field "foundVersion" foundVersion + +teamSearchVisibilityInbound :: (Member GalleyAPIAccess r) => TeamId -> Sem r SearchVisibilityInbound +teamSearchVisibilityInbound tid = + searchVisibilityInboundFromFeatureStatus . (.status) + <$> getFeatureConfigForTeam @_ @SearchVisibilityInboundConfig tid diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs new file mode 100644 index 00000000000..6f8dd26e89f --- /dev/null +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs @@ -0,0 +1,522 @@ +{-# LANGUAGE RecordWildCards #-} + +module Wire.IndexedUserStore.ElasticSearch where + +import Control.Error (lastMay) +import Control.Exception (throwIO) +import Data.Aeson +import Data.Aeson.Key qualified as Key +import Data.ByteString qualified as LBS +import Data.ByteString.Builder +import Data.ByteString.Conversion +import Data.Id +import Data.Text qualified as Text +import Data.Text.Ascii +import Data.Text.Encoding qualified as Text +import Database.Bloodhound qualified as ES +import Imports +import Network.HTTP.Client +import Network.HTTP.Types +import Polysemy +import Wire.API.Team.Size (TeamSize (TeamSize)) +import Wire.API.User.Search +import Wire.IndexedUserStore +import Wire.Sem.Metrics (Metrics) +import Wire.Sem.Metrics qualified as Metrics +import Wire.UserSearch.Metrics +import Wire.UserSearch.Types +import Wire.UserStore.IndexUser + +data ESConn = ESConn + { env :: ES.BHEnv, + indexName :: ES.IndexName + } + +data IndexedUserStoreConfig = IndexedUserStoreConfig + { conn :: ESConn, + additionalConn :: Maybe ESConn + } + +interpretIndexedUserStoreES :: + ( Member (Embed IO) r, + Member Metrics r + ) => + IndexedUserStoreConfig -> + InterpreterFor IndexedUserStore r +interpretIndexedUserStoreES cfg = + interpret $ \case + Upsert docId userDoc versioning -> upsertImpl cfg docId userDoc versioning + UpdateTeamSearchVisibilityInbound tid vis -> + updateTeamSearchVisibilityInboundImpl cfg tid vis + BulkUpsert docs -> bulkUpsertImpl cfg docs + DoesIndexExist -> doesIndexExistImpl cfg + SearchUsers searcherId mSearcherTeam teamSearchInfo term maxResults -> + searchUsersImpl cfg searcherId mSearcherTeam teamSearchInfo term maxResults + PaginateTeamMembers filters maxResults mPagingState -> + paginateTeamMembersImpl cfg filters maxResults mPagingState + GetTeamSize tid -> getTeamSizeImpl cfg tid + +getTeamSizeImpl :: + ( Member (Embed IO) r + ) => + IndexedUserStoreConfig -> + TeamId -> + Sem r TeamSize +getTeamSizeImpl cfg tid = do + let indexName = cfg.conn.indexName + countResEither <- embed $ ES.runBH cfg.conn.env $ ES.countByIndex indexName (ES.CountQuery query) + countRes <- either (liftIO . throwIO . IndexLookupError) pure countResEither + pure . TeamSize $ ES.crCount countRes + where + query = + ES.TermQuery + ES.Term + { ES.termField = "team", + ES.termValue = idToText tid + } + Nothing + +upsertImpl :: + forall r. + ( Member (Embed IO) r, + Member Metrics r + ) => + IndexedUserStoreConfig -> + ES.DocId -> + UserDoc -> + ES.VersionControl -> + Sem r () +upsertImpl cfg docId userDoc versioning = do + void $ runInBothES cfg indexDoc + where + indexDoc :: ES.IndexName -> ES.BH (Sem r) () + indexDoc idx = do + r <- ES.indexDocument idx mappingName settings userDoc docId + unless (ES.isSuccess r || ES.isVersionConflict r) $ do + lift $ Metrics.incCounter indexUpdateErrorCounter + res <- liftIO $ ES.parseEsResponse r + liftIO . throwIO . IndexUpdateError . either id id $ res + lift $ Metrics.incCounter indexUpdateSuccessCounter + + settings = ES.defaultIndexDocumentSettings {ES.idsVersionControl = versioning} + +updateTeamSearchVisibilityInboundImpl :: forall r. (Member (Embed IO) r) => IndexedUserStoreConfig -> TeamId -> SearchVisibilityInbound -> Sem r () +updateTeamSearchVisibilityInboundImpl cfg tid vis = + void $ runInBothES cfg updateAllDocs + where + updateAllDocs :: ES.IndexName -> ES.BH (Sem r) () + updateAllDocs idx = do + r <- ES.updateByQuery idx query (Just script) + unless (ES.isSuccess r || ES.isVersionConflict r) $ do + res <- liftIO $ ES.parseEsResponse r + liftIO . throwIO . IndexUpdateError . either id id $ res + + query :: ES.Query + query = ES.TermQuery (ES.Term "team" $ idToText tid) Nothing + + script :: ES.Script + script = ES.Script (Just (ES.ScriptLanguage "painless")) (Just (ES.ScriptInline scriptText)) Nothing Nothing + + -- Unfortunately ES disallows updating ctx._version with a "Update By Query" + scriptText = + "ctx._source." + <> Key.toText searchVisibilityInboundFieldName + <> " = '" + <> Text.decodeUtf8 (toByteString' vis) + <> "';" + +bulkUpsertImpl :: (Member (Embed IO) r) => IndexedUserStoreConfig -> [(ES.DocId, UserDoc, ES.VersionControl)] -> Sem r () +bulkUpsertImpl cfg docs = do + let bhe = cfg.conn.env + ES.IndexName idx = cfg.conn.indexName + ES.MappingName mpp = mappingName + (ES.Server base) = ES.bhServer bhe + baseReq <- embed $ parseRequest (Text.unpack $ base <> "/" <> idx <> "/" <> mpp <> "/_bulk") + let reqWithoutCreds = + baseReq + { method = "POST", + requestHeaders = [(hContentType, "application/x-ndjson")], + requestBody = RequestBodyLBS (toLazyByteString (foldMap encodeActionAndData docs)) + } + req <- embed $ bhe.bhRequestHook reqWithoutCreds + res <- embed $ httpLbs req (ES.bhManager bhe) + unless (ES.isSuccess res) $ do + parsedRes <- liftIO $ ES.parseEsResponse res + liftIO . throwIO . IndexUpdateError . either id id $ parsedRes + where + encodeJSONToString :: (ToJSON a) => a -> Builder + encodeJSONToString = fromEncoding . toEncoding + + encodeActionAndData :: (ES.DocId, UserDoc, ES.VersionControl) -> Builder + encodeActionAndData (docId, userDoc, versionControl) = + encodeJSONToString (bulkIndexAction docId versionControl) + <> "\n" + <> encodeJSONToString userDoc + <> "\n" + + bulkIndexAction :: ES.DocId -> ES.VersionControl -> Value + bulkIndexAction docId versionControl = + let (versionType :: Maybe Text, version) = case versionControl of + ES.NoVersionControl -> (Nothing, Nothing) + ES.InternalVersion v -> (Nothing, Just v) + ES.ExternalGT (ES.ExternalDocVersion v) -> (Just "external", Just v) + ES.ExternalGTE (ES.ExternalDocVersion v) -> (Just "external_gte", Just v) + ES.ForceVersion (ES.ExternalDocVersion v) -> (Just "force", Just v) + in object + [ "index" + .= object + [ "_id" .= docId, + "_version_type" .= versionType, + "_version" .= version + ] + ] + +doesIndexExistImpl :: (Member (Embed IO) r) => IndexedUserStoreConfig -> Sem r Bool +doesIndexExistImpl cfg = do + (mainExists, fromMaybe True -> additionalExists) <- runInBothES cfg ES.indexExists + pure $ mainExists && additionalExists + +searchUsersImpl :: + (Member (Embed IO) r) => + IndexedUserStoreConfig -> + UserId -> + Maybe TeamId -> + TeamSearchInfo -> + Text -> + Int -> + Sem r (SearchResult UserDoc) +searchUsersImpl cfg searcherId mSearcherTeam teamSearchInfo term maxResults = + queryIndex cfg maxResults $ + defaultUserQuery searcherId mSearcherTeam teamSearchInfo term + +-- | The default or canonical 'IndexQuery'. +-- +-- The intention behind parameterising 'queryIndex' over the 'IndexQuery' is that +-- it allows to experiment with different queries (perhaps in an A/B context). +-- +-- FUTUREWORK: Drop legacyPrefixMatch +defaultUserQuery :: UserId -> Maybe TeamId -> TeamSearchInfo -> Text -> IndexQuery Contact +defaultUserQuery searcher mSearcherTeamId teamSearchInfo (normalized -> term') = + let matchPhraseOrPrefix = + ES.QueryMultiMatchQuery $ + ( ES.mkMultiMatchQuery + [ ES.FieldName "handle.prefix^2", + ES.FieldName "normalized.prefix", + ES.FieldName "normalized^3" + ] + (ES.QueryString term') + ) + { ES.multiMatchQueryType = Just ES.MultiMatchMostFields, + ES.multiMatchQueryOperator = ES.And + } + query = + ES.QueryBoolQuery + boolQuery + { ES.boolQueryMustMatch = + [ ES.QueryBoolQuery + boolQuery + { ES.boolQueryShouldMatch = [matchPhraseOrPrefix], + -- This removes exact handle matches, as they are fetched from cassandra + ES.boolQueryMustNotMatch = [termQ "handle" term'] + } + ], + ES.boolQueryShouldMatch = [ES.QueryExistsQuery (ES.FieldName "handle")] + } + -- This reduces relevance on users not in team of search by 90% (no + -- science behind that number). If the searcher is not part of a team the + -- relevance is not reduced for any users. + queryWithBoost = + ES.QueryBoostingQuery + ES.BoostingQuery + { ES.positiveQuery = query, + ES.negativeQuery = maybe ES.QueryMatchNoneQuery matchUsersNotInTeam mSearcherTeamId, + ES.negativeBoost = ES.Boost 0.1 + } + in mkUserQuery searcher mSearcherTeamId teamSearchInfo queryWithBoost + +paginateTeamMembersImpl :: + (Member (Embed IO) r) => + IndexedUserStoreConfig -> + BrowseTeamFilters -> + Int -> + Maybe PagingState -> + Sem r (SearchResult UserDoc) +paginateTeamMembersImpl cfg BrowseTeamFilters {..} maxResults mPagingState = do + let (IndexQuery q f sortSpecs) = + teamUserSearchQuery teamId mQuery mRoleFilter mSortBy mSortOrder + let search = + (ES.mkSearch (Just q) (Just f)) + { -- we are requesting one more result than the page size to determine if there is a next page + ES.size = ES.Size (fromIntegral maxResults + 1), + ES.sortBody = Just (fmap ES.DefaultSortSpec sortSpecs), + ES.searchAfterKey = toSearchAfterKey =<< mPagingState + } + mkResult <$> searchInMainIndex cfg search + where + toSearchAfterKey ps = decode' . LBS.fromStrict =<< (decodeBase64Url . unPagingState) ps + + fromSearchAfterKey :: ES.SearchAfterKey -> PagingState + fromSearchAfterKey = PagingState . encodeBase64Url . LBS.toStrict . encode + + mkResult es = + let hitsPlusOne = ES.hits . ES.searchHits $ es + hits = take (fromIntegral maxResults) hitsPlusOne + mps = fromSearchAfterKey <$> lastMay (mapMaybe ES.hitSort hits) + results = mapMaybe ES.hitSource hits + in SearchResult + { searchFound = ES.hitsTotal . ES.searchHits $ es, + searchReturned = length results, + searchTook = ES.took es, + searchResults = results, + searchPolicy = FullSearch, + searchPagingState = mps, + searchHasMore = Just $ length hitsPlusOne > length hits + } + +searchInMainIndex :: forall r. (Member (Embed IO) r) => IndexedUserStoreConfig -> ES.Search -> Sem r (ES.SearchResult UserDoc) +searchInMainIndex cfg search = do + r <- ES.runBH cfg.conn.env $ do + res <- ES.searchByType cfg.conn.indexName mappingName search + liftIO $ ES.parseEsResponse res + either (embed . throwIO . IndexLookupError) pure r + +queryIndex :: + (Member (Embed IO) r) => + IndexedUserStoreConfig -> + Int -> + IndexQuery x -> + Sem r (SearchResult UserDoc) +queryIndex cfg s (IndexQuery q f _) = do + let search = (ES.mkSearch (Just q) (Just f)) {ES.size = ES.Size (fromIntegral s)} + mkResult <$> searchInMainIndex cfg search + where + mkResult es = + let results = mapMaybe ES.hitSource . ES.hits . ES.searchHits $ es + in SearchResult + { searchFound = ES.hitsTotal . ES.searchHits $ es, + searchReturned = length results, + searchTook = ES.took es, + searchResults = results, + searchPolicy = FullSearch, + searchPagingState = Nothing, + searchHasMore = Nothing + } + +teamUserSearchQuery :: + TeamId -> + Maybe Text -> + Maybe RoleFilter -> + Maybe TeamUserSearchSortBy -> + Maybe TeamUserSearchSortOrder -> + IndexQuery TeamContact +teamUserSearchQuery tid mbSearchText _mRoleFilter mSortBy mSortOrder = + IndexQuery + ( maybe + (ES.MatchAllQuery Nothing) + matchPhraseOrPrefix + mbQStr + ) + teamFilter + -- in combination with pagination a non-unique search specification can lead to missing results + -- therefore we use the unique `_doc` value as a tie breaker + -- - see https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-sort.html for details on `_doc` + -- - see https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-search-after.html for details on pagination and tie breaker + -- in the latter article it "is advised to duplicate (client side or [...]) the content of the _id field + -- in another field that has doc value enabled and to use this new field as the tiebreaker for the sort" + -- so alternatively we could use the user ID as a tie breaker, but this would require a change in the index mapping + (sorting ++ sortingTieBreaker) + where + sorting :: [ES.DefaultSort] + sorting = + maybe + [defaultSort SortByCreatedAt SortOrderDesc | isNothing mbQStr] + (\tuSortBy -> [defaultSort tuSortBy (fromMaybe SortOrderAsc mSortOrder)]) + mSortBy + sortingTieBreaker :: [ES.DefaultSort] + sortingTieBreaker = [ES.DefaultSort (ES.FieldName "_doc") ES.Ascending Nothing Nothing Nothing Nothing] + + mbQStr :: Maybe Text + mbQStr = + case mbSearchText of + Nothing -> Nothing + Just q -> + case normalized q of + "" -> Nothing + term' -> Just term' + + matchPhraseOrPrefix term' = + ES.QueryMultiMatchQuery $ + ( ES.mkMultiMatchQuery + [ ES.FieldName "email^4", + ES.FieldName "handle^4", + ES.FieldName "normalized^3", + ES.FieldName "email.prefix^3", + ES.FieldName "handle.prefix^2", + ES.FieldName "normalized.prefix" + ] + (ES.QueryString term') + ) + { ES.multiMatchQueryType = Just ES.MultiMatchMostFields, + ES.multiMatchQueryOperator = ES.And + } + + teamFilter = + ES.Filter $ + ES.QueryBoolQuery + boolQuery + { ES.boolQueryMustMatch = [ES.TermQuery (ES.Term "team" $ idToText tid) Nothing] + } + + defaultSort :: TeamUserSearchSortBy -> TeamUserSearchSortOrder -> ES.DefaultSort + defaultSort tuSortBy sortOrder = + ES.DefaultSort + ( case tuSortBy of + SortByName -> ES.FieldName "name" + SortByHandle -> ES.FieldName "handle.keyword" + SortByEmail -> ES.FieldName "email.keyword" + SortBySAMLIdp -> ES.FieldName "saml_idp" + SortByManagedBy -> ES.FieldName "managed_by" + SortByRole -> ES.FieldName "role" + SortByCreatedAt -> ES.FieldName "created_at" + ) + ( case sortOrder of + SortOrderAsc -> ES.Ascending + SortOrderDesc -> ES.Descending + ) + Nothing + Nothing + Nothing + Nothing + +mkUserQuery :: UserId -> Maybe TeamId -> TeamSearchInfo -> ES.Query -> IndexQuery Contact +mkUserQuery searcher mSearcherTeamId teamSearchInfo q = + IndexQuery + q + ( ES.Filter + . ES.QueryBoolQuery + $ boolQuery + { ES.boolQueryMustNotMatch = maybeToList $ matchSelf searcher, + ES.boolQueryMustMatch = + [ restrictSearchSpace mSearcherTeamId teamSearchInfo, + ES.QueryBoolQuery + boolQuery + { ES.boolQueryShouldMatch = + [ termQ "account_status" "active", + -- Also match entries where the account_status field is not present. + -- These must have been inserted before we added the account_status + -- and at that time we only inserted active users in the first place. + -- This should be unnecessary after re-indexing, but let's be lenient + -- here for a while. + ES.QueryBoolQuery + boolQuery + { ES.boolQueryMustNotMatch = + [ES.QueryExistsQuery (ES.FieldName "account_status")] + } + ] + } + ] + } + ) + [] + +termQ :: Text -> Text -> ES.Query +termQ f v = + ES.TermQuery + ES.Term + { ES.termField = f, + ES.termValue = v + } + Nothing + +matchSelf :: UserId -> Maybe ES.Query +matchSelf searcher = Just (termQ "_id" (idToText searcher)) + +-- | See 'TeamSearchInfo' +restrictSearchSpace :: Maybe TeamId -> TeamSearchInfo -> ES.Query +-- restrictSearchSpace (FederatedSearch Nothing) = +-- ES.QueryBoolQuery +-- boolQuery +-- { ES.boolQueryShouldMatch = +-- [ matchNonTeamMemberUsers, +-- matchTeamMembersSearchableByAllTeams +-- ] +-- } +-- restrictSearchSpace (FederatedSearch (Just [])) = +-- ES.QueryBoolQuery +-- boolQuery +-- { ES.boolQueryMustMatch = +-- [ -- if the list of allowed teams is empty, this is impossible to fulfill, and no results will be returned +-- -- this case should be handled earlier, so this is just a safety net +-- ES.TermQuery (ES.Term "team" "must not match any team") Nothing +-- ] +-- } +-- restrictSearchSpace (FederatedSearch (Just teams)) = +-- ES.QueryBoolQuery +-- boolQuery +-- { ES.boolQueryMustMatch = +-- [ matchTeamMembersSearchableByAllTeams, +-- onlyInTeams +-- ] +-- } +-- where +-- onlyInTeams = ES.QueryBoolQuery boolQuery {ES.boolQueryShouldMatch = map matchTeamMembersOf teams} +restrictSearchSpace mteam searchInfo = + case (mteam, searchInfo) of + (Nothing, _) -> matchNonTeamMemberUsers + (Just _, NoTeam) -> matchNonTeamMemberUsers + (Just searcherTeam, TeamOnly team) -> + if searcherTeam == team + then matchTeamMembersOf team + else ES.QueryMatchNoneQuery + (Just searcherTeam, AllUsers) -> + ES.QueryBoolQuery + boolQuery + { ES.boolQueryShouldMatch = + [ matchNonTeamMemberUsers, + matchTeamMembersSearchableByAllTeams, + matchTeamMembersOf searcherTeam + ] + } + +matchTeamMembersOf :: TeamId -> ES.Query +matchTeamMembersOf team = ES.TermQuery (ES.Term "team" $ idToText team) Nothing + +matchTeamMembersSearchableByAllTeams :: ES.Query +matchTeamMembersSearchableByAllTeams = + ES.QueryBoolQuery + boolQuery + { ES.boolQueryMustMatch = + [ ES.QueryExistsQuery $ ES.FieldName "team", + ES.TermQuery (ES.Term (Key.toText searchVisibilityInboundFieldName) "searchable-by-all-teams") Nothing + ] + } + +matchNonTeamMemberUsers :: ES.Query +matchNonTeamMemberUsers = + ES.QueryBoolQuery + boolQuery + { ES.boolQueryMustNotMatch = [ES.QueryExistsQuery $ ES.FieldName "team"] + } + +matchUsersNotInTeam :: TeamId -> ES.Query +matchUsersNotInTeam tid = + ES.QueryBoolQuery + boolQuery + { ES.boolQueryMustNotMatch = [ES.TermQuery (ES.Term "team" $ idToText tid) Nothing] + } + +-------------------------------------------- +-- Utils + +runInBothES :: (Monad m) => IndexedUserStoreConfig -> (ES.IndexName -> ES.BH m a) -> m (a, Maybe a) +runInBothES cfg f = do + x <- ES.runBH cfg.conn.env $ f cfg.conn.indexName + y <- forM cfg.additionalConn $ \additional -> + ES.runBH additional.env $ f additional.indexName + pure (x, y) + +mappingName :: ES.MappingName +mappingName = ES.MappingName "user" + +boolQuery :: ES.BoolQuery +boolQuery = ES.mkBoolQuery [] [] [] [] diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/MigrationStore.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/MigrationStore.hs new file mode 100644 index 00000000000..1cb9c8d51f6 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/MigrationStore.hs @@ -0,0 +1,13 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.IndexedUserStore.MigrationStore where + +import Polysemy +import Wire.UserSearch.Migration + +data IndexedUserMigrationStore m a where + EnsureMigrationIndex :: IndexedUserMigrationStore m () + GetLatestMigrationVersion :: IndexedUserMigrationStore m MigrationVersion + PersistMigrationVersion :: MigrationVersion -> IndexedUserMigrationStore m () + +makeSem ''IndexedUserMigrationStore diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/MigrationStore/ElasticSearch.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/MigrationStore/ElasticSearch.hs new file mode 100644 index 00000000000..9532a54246c --- /dev/null +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/MigrationStore/ElasticSearch.hs @@ -0,0 +1,73 @@ +module Wire.IndexedUserStore.MigrationStore.ElasticSearch where + +import Data.Aeson +import Data.Text qualified as Text +import Database.Bloodhound qualified as ES +import Imports +import Polysemy +import Polysemy.Error +import Polysemy.TinyLog +import System.Logger.Message qualified as Log +import Wire.IndexedUserStore.MigrationStore +import Wire.Sem.Logger qualified as Log +import Wire.UserSearch.Migration + +interpretIndexedUserMigrationStoreES :: (Member (Embed IO) r, Member (Error MigrationException) r, Member TinyLog r) => ES.BHEnv -> InterpreterFor IndexedUserMigrationStore r +interpretIndexedUserMigrationStoreES env = interpret $ \case + EnsureMigrationIndex -> ensureMigrationIndexImpl env + GetLatestMigrationVersion -> getLatestMigrationVersionImpl env + PersistMigrationVersion v -> persistMigrationVersionImpl env v + +ensureMigrationIndexImpl :: (Member TinyLog r, Member (Embed IO) r, Member (Error MigrationException) r) => ES.BHEnv -> Sem r () +ensureMigrationIndexImpl env = do + unlessM (ES.runBH env $ ES.indexExists migrationIndexName) $ do + Log.info $ + Log.msg (Log.val "Creating migrations index, used for tracking which migrations have run") + ES.runBH env (ES.createIndexWith [] 1 migrationIndexName) + >>= throwIfNotCreated CreateMigrationIndexFailed + ES.runBH env (ES.putMapping migrationIndexName migrationMappingName migrationIndexMapping) + >>= throwIfNotCreated PutMappingFailed + where + throwIfNotCreated mkErr response = + unless (ES.isSuccess response) $ + throw $ + mkErr (show response) + +getLatestMigrationVersionImpl :: (Member (Embed IO) r, Member (Error MigrationException) r) => ES.BHEnv -> Sem r MigrationVersion +getLatestMigrationVersionImpl env = do + reply <- ES.runBH env $ ES.searchByIndex migrationIndexName (ES.mkSearch Nothing Nothing) + resp <- liftIO $ ES.parseEsResponse reply + result <- either (throw . FetchMigrationVersionsFailed . show) pure resp + let versions = map ES.hitSource $ ES.hits . ES.searchHits $ result + case versions of + [] -> + pure $ MigrationVersion 0 + vs -> + if any isNothing vs + then throw $ VersionSourceMissing result + else pure $ maximum $ catMaybes vs + +persistMigrationVersionImpl :: (Member (Embed IO) r, Member TinyLog r, Member (Error MigrationException) r) => ES.BHEnv -> MigrationVersion -> Sem r () +persistMigrationVersionImpl env v = do + let docId = ES.DocId . Text.pack . show $ migrationVersion v + persistResponse <- ES.runBH env $ ES.indexDocument migrationIndexName migrationMappingName ES.defaultIndexDocumentSettings v docId + if ES.isCreated persistResponse + then do + Log.info $ + Log.msg (Log.val "Migration success recorded") + . Log.field "migrationVersion" v + else throw $ PersistVersionFailed v $ show persistResponse + +migrationIndexName :: ES.IndexName +migrationIndexName = ES.IndexName "wire_brig_migrations" + +migrationMappingName :: ES.MappingName +migrationMappingName = ES.MappingName "wire_brig_migrations" + +migrationIndexMapping :: Value +migrationIndexMapping = + object + [ "properties" + .= object + ["migration_version" .= object ["index" .= True, "type" .= ("integer" :: Text)]] + ] diff --git a/libs/wire-subsystems/src/Wire/InvitationStore.hs b/libs/wire-subsystems/src/Wire/InvitationStore.hs new file mode 100644 index 00000000000..e691f516bf7 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/InvitationStore.hs @@ -0,0 +1,99 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE StrictData #-} +{-# LANGUAGE TemplateHaskell #-} + +module Wire.InvitationStore where + +import Data.Id (InvitationId, TeamId, UserId) +import Data.Json.Util (UTCTimeMillis) +import Data.Range (Range) +import Database.CQL.Protocol (Record (..), TupleType, recordInstance) +import Imports +import Polysemy +import URI.ByteString +import Util.Timeout +import Wire.API.Team.Invitation (Invitation (inviteeEmail)) +import Wire.API.Team.Invitation qualified as Public +import Wire.API.Team.Role (Role, defaultRole) +import Wire.API.User (EmailAddress, InvitationCode, Name) +import Wire.Arbitrary (Arbitrary, GenericUniform (..)) + +data StoredInvitation = MkStoredInvitation + { teamId :: TeamId, + role :: Maybe Role, + invitationId :: InvitationId, + createdAt :: UTCTimeMillis, + createdBy :: Maybe UserId, + email :: EmailAddress, + name :: Maybe Name, + code :: InvitationCode + } + deriving (Show, Eq, Generic) + deriving (Arbitrary) via (GenericUniform StoredInvitation) + +recordInstance ''StoredInvitation + +data InsertInvitation = MkInsertInvitation + { invitationId :: InvitationId, + teamId :: TeamId, + role :: Role, + createdAt :: UTCTime, + createdBy :: Maybe UserId, + inviteeEmail :: EmailAddress, + inviteeName :: Maybe Name, + code :: InvitationCode + } + deriving (Show, Eq, Generic) + +recordInstance ''InsertInvitation + +data PaginatedResult a + = PaginatedResultHasMore a + | PaginatedResult a + deriving stock (Eq, Ord, Show, Functor, Foldable) + +---------------------------- + +data InvitationStore :: Effect where + InsertInvitation :: InsertInvitation -> Timeout -> InvitationStore m StoredInvitation + LookupInvitation :: TeamId -> InvitationId -> InvitationStore m (Maybe StoredInvitation) + LookupInvitationByCode :: InvitationCode -> InvitationStore m (Maybe StoredInvitation) + LookupInvitationsByEmail :: EmailAddress -> InvitationStore m [StoredInvitation] + -- | Range is page size, it defaults to 100 + LookupInvitationsPaginated :: Maybe (Range 1 500 Int32) -> TeamId -> Maybe InvitationId -> InvitationStore m (PaginatedResult [StoredInvitation]) + CountInvitations :: TeamId -> InvitationStore m Int64 + DeleteInvitation :: TeamId -> InvitationId -> InvitationStore m () + DeleteAllTeamInvitations :: TeamId -> InvitationStore m () + +makeSem ''InvitationStore + +---------------------------- + +invitationFromStored :: Maybe (URIRef Absolute) -> StoredInvitation -> Public.Invitation +invitationFromStored maybeUrl MkStoredInvitation {..} = + Public.Invitation + { team = teamId, + role = fromMaybe defaultRole role, + invitationId = invitationId, + createdAt = createdAt, + createdBy = createdBy, + inviteeEmail = email, + inviteeName = name, + inviteeUrl = maybeUrl + } diff --git a/libs/wire-subsystems/src/Wire/InvitationStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/InvitationStore/Cassandra.hs new file mode 100644 index 00000000000..b9fc0173858 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/InvitationStore/Cassandra.hs @@ -0,0 +1,207 @@ +module Wire.InvitationStore.Cassandra + ( interpretInvitationStoreToCassandra, + ) +where + +import Cassandra +import Control.Monad.Trans.Maybe +import Data.Conduit (runConduit, (.|)) +import Data.Conduit.List qualified as Conduit +import Data.Id +import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) +import Data.Range (Range, fromRange) +import Database.CQL.Protocol (Record (..), TupleType, asRecord) +import Imports +import Polysemy +import Polysemy.Embed +import UnliftIO.Async (pooledMapConcurrentlyN_) +import Util.Timeout +import Wire.API.Team.Role (Role) +import Wire.API.User +import Wire.InvitationStore + +interpretInvitationStoreToCassandra :: (Member (Embed IO) r) => ClientState -> InterpreterFor InvitationStore r +interpretInvitationStoreToCassandra casClient = + interpret $ + runEmbedded (runClient casClient) . \case + InsertInvitation newInv timeout -> embed $ insertInvitationImpl newInv timeout + LookupInvitation tid iid -> embed $ lookupInvitationImpl tid iid + LookupInvitationsByEmail email -> embed $ lookupInvitationsByEmailImpl email + LookupInvitationByCode code -> embed $ lookupInvitationByCodeImpl code + LookupInvitationsPaginated mSize tid miid -> embed $ lookupInvitationsPaginatedImpl mSize tid miid + CountInvitations tid -> embed $ countInvitationsImpl tid + DeleteInvitation tid invId -> embed $ deleteInvitationImpl tid invId + DeleteAllTeamInvitations tid -> embed $ deleteInvitationsImpl tid + +insertInvitationImpl :: + InsertInvitation -> + -- | The timeout for the invitation code. + Timeout -> + Client StoredInvitation +insertInvitationImpl (MkInsertInvitation invId teamId role (toUTCTimeMillis -> now) uid email name code) timeout = do + let inv = + MkStoredInvitation + { teamId = teamId, + role = Just role, + invitationId = invId, + createdAt = now, + createdBy = uid, + email = email, + name = name, + code = code + } + retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + addPrepQuery cqlInsert (teamId, Just role, invId, now, uid, email, name, code, round timeout) + addPrepQuery cqlInsertInfo (code, teamId, invId, round timeout) + addPrepQuery cqlInsertByEmail (email, teamId, invId, code, round timeout) + pure inv + where + cqlInsert :: PrepQuery W (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, EmailAddress, Maybe Name, InvitationCode, Int32) () + cqlInsert = + [sql| + INSERT INTO team_invitation (team, role, id, created_at, created_by, email, name, code) VALUES (?, ?, ?, ?, ?, ?, ?, ?) USING TTL ? + |] + cqlInsertInfo :: PrepQuery W (InvitationCode, TeamId, InvitationId, Int32) () + cqlInsertInfo = + [sql| + INSERT INTO team_invitation_info (code, team, id) VALUES (?, ?, ?) USING TTL ? + |] + -- Note: the edge case of multiple invites to the same team by different admins from the + -- same team results in last-invite-wins in the team_invitation_email table. + cqlInsertByEmail :: PrepQuery W (EmailAddress, TeamId, InvitationId, InvitationCode, Int32) () + cqlInsertByEmail = + [sql| + INSERT INTO team_invitation_email (email, team, invitation, code) VALUES (?, ?, ?, ?) USING TTL ? + |] + +lookupInvitationsPaginatedImpl :: Maybe (Range 1 500 Int32) -> TeamId -> Maybe InvitationId -> Client (PaginatedResult [StoredInvitation]) +lookupInvitationsPaginatedImpl mSize tid miid = do + page <- retry x1 case miid of + Just ref -> paginate cqlSelectFrom (paramsP LocalQuorum (tid, ref) (pageSize + 1)) + Nothing -> paginate cqlSelect (paramsP LocalQuorum (Identity tid) (pageSize + 1)) + pure $ mkPage (hasMore page) $ map asRecord $ trim page + where + pageSize :: Int32 + pageSize = maybe 100 fromRange mSize + + trim :: Page a -> [a] + trim p = take (fromIntegral pageSize) (result p) + + mkPage more invs = if more then PaginatedResultHasMore invs else PaginatedResult invs + + cqlSelect :: PrepQuery R (Identity TeamId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, EmailAddress, Maybe Name, InvitationCode) + cqlSelect = + [sql| + SELECT team, role, id, created_at, created_by, email, name, code FROM team_invitation WHERE team = ? ORDER BY id ASC + |] + cqlSelectFrom :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, EmailAddress, Maybe Name, InvitationCode) + cqlSelectFrom = + [sql| + SELECT team, role, id, created_at, created_by, email, name, code FROM team_invitation WHERE team = ? AND id > ? ORDER BY id ASC + |] + +countInvitationsImpl :: TeamId -> Client (Int64) +countInvitationsImpl t = + maybe 0 runIdentity + <$> retry x1 (query1 cql (params LocalQuorum (Identity t))) + where + cql :: PrepQuery R (Identity TeamId) (Identity Int64) + cql = [sql| SELECT count(*) FROM team_invitation WHERE team = ?|] + +lookupInvitationByCodeImpl :: InvitationCode -> Client (Maybe StoredInvitation) +lookupInvitationByCodeImpl code = runMaybeT do + (teamId, invId, _) <- + MaybeT $ + retry x1 (query1 cqlInfo (params LocalQuorum (Identity code))) + MaybeT $ fmap asRecord <$> retry x1 (query1 cqlMain (params LocalQuorum (teamId, invId))) + where + cqlInfo :: PrepQuery R (Identity InvitationCode) (TeamId, InvitationId, InvitationCode) + cqlInfo = + [sql| + SELECT team, id, code FROM team_invitation_info WHERE code = ? + |] + cqlMain :: PrepQuery R (TeamId, InvitationId) (TupleType StoredInvitation) + cqlMain = + [sql| + SELECT team, role, id, created_at, created_by, email, name, code FROM team_invitation WHERE team = ? AND id = ? + |] + +lookupInvitationsByEmailImpl :: EmailAddress -> Client [StoredInvitation] +lookupInvitationsByEmailImpl email = do + infoList <- + retry x1 (query cqlInfo (params LocalQuorum (Identity email))) + fmap catMaybes $ forM infoList $ \(tid, invId, _invCode) -> + fmap asRecord <$> retry x1 (query1 cqlMain (params LocalQuorum (tid, invId))) + where + cqlInfo :: PrepQuery R (Identity EmailAddress) (TeamId, InvitationId, InvitationCode) + cqlInfo = + [sql| + SELECT team, invitation, code FROM team_invitation_email WHERE email = ? + |] + cqlMain :: PrepQuery R (TeamId, InvitationId) (TupleType StoredInvitation) + cqlMain = + [sql| + SELECT team, role, id, created_at, created_by, email, name, code FROM team_invitation WHERE team = ? AND id = ? + |] + +lookupInvitationImpl :: TeamId -> InvitationId -> Client (Maybe StoredInvitation) +lookupInvitationImpl tid iid = + fmap asRecord + <$> retry x1 (query1 cql (params LocalQuorum (tid, iid))) + where + cql :: PrepQuery R (TeamId, InvitationId) (TupleType StoredInvitation) + cql = + [sql| + SELECT team, role, id, created_at, created_by, email, name, code FROM team_invitation WHERE team = ? AND id = ? + |] + +deleteInvitationImpl :: TeamId -> InvitationId -> Client () +deleteInvitationImpl teamId invId = do + codeEmail <- lookupInvitationCodeEmail + case codeEmail of + Just (invCode, invEmail) -> retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + addPrepQuery cqlInvitation (teamId, invId) + addPrepQuery cqlInvitationInfo (Identity invCode) + addPrepQuery cqlInvitationEmail (invEmail, teamId) + Nothing -> + retry x5 $ write cqlInvitation (params LocalQuorum (teamId, invId)) + where + lookupInvitationCodeEmail :: Client (Maybe (InvitationCode, EmailAddress)) + lookupInvitationCodeEmail = retry x1 (query1 cqlInvitationCodeEmail (params LocalQuorum (teamId, invId))) + + cqlInvitation :: PrepQuery W (TeamId, InvitationId) () + cqlInvitation = + [sql| + DELETE FROM team_invitation where team = ? AND id = ? + |] + + cqlInvitationInfo :: PrepQuery W (Identity InvitationCode) () + cqlInvitationInfo = + [sql| + DELETE FROM team_invitation_info WHERE code = ? + |] + + cqlInvitationEmail :: PrepQuery W (EmailAddress, TeamId) () + cqlInvitationEmail = + [sql| + DELETE FROM team_invitation_email WHERE email = ? AND team = ? + |] + + cqlInvitationCodeEmail :: PrepQuery R (TeamId, InvitationId) (InvitationCode, EmailAddress) + cqlInvitationCodeEmail = + [sql| + SELECT code, email FROM team_invitation WHERE team = ? AND id = ? + |] + +deleteInvitationsImpl :: TeamId -> Client () +deleteInvitationsImpl teamId = + runConduit $ + paginateC cqlSelect (paramsP LocalQuorum (Identity teamId) 100) x1 + .| Conduit.mapM_ (pooledMapConcurrentlyN_ 16 (deleteInvitationImpl teamId . runIdentity)) + where + cqlSelect :: PrepQuery R (Identity TeamId) (Identity InvitationId) + cqlSelect = "SELECT id FROM team_invitation WHERE team = ? ORDER BY id ASC" diff --git a/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs b/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs index 499b1eb12e4..d854c0acb1b 100644 --- a/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs @@ -7,20 +7,18 @@ import Control.Lens (makeLenses) import Data.Aeson import Data.Id import Data.List.NonEmpty (NonEmpty ((:|))) -import Gundeck.Types hiding (Push (..), Recipient, newPush) import Imports import Polysemy +import Wire.API.Push.V2 hiding (Push (..), Recipient, newPush) import Wire.Arbitrary data Recipient = Recipient - { _recipientUserId :: UserId, - _recipientClients :: RecipientClients + { recipientUserId :: UserId, + recipientClients :: RecipientClients } deriving stock (Show, Ord, Eq, Generic) deriving (Arbitrary) via GenericUniform Recipient -makeLenses ''Recipient - data Push = Push { _pushConn :: Maybe ConnId, _pushTransient :: Bool, @@ -47,7 +45,7 @@ data NotificationSubsystem m a where -- send notifications is not critical. -- -- See 'Polysemy.Async' to know more about the 'Maybe' - PushNotificationsAsync :: [Push] -> NotificationSubsystem m (Async (Maybe ())) + PushNotificationAsync :: Push -> NotificationSubsystem m (Async (Maybe ())) CleanupUser :: UserId -> NotificationSubsystem m () UnregisterPushClient :: UserId -> ClientId -> NotificationSubsystem m () GetPushTokens :: UserId -> NotificationSubsystem m [PushToken] diff --git a/libs/wire-subsystems/src/Wire/NotificationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/NotificationSubsystem/Interpreter.hs index f59c79d0c2d..5b2859d1ff1 100644 --- a/libs/wire-subsystems/src/Wire/NotificationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/NotificationSubsystem/Interpreter.hs @@ -11,8 +11,6 @@ import Data.Proxy import Data.Range import Data.Set qualified as Set import Data.Time.Clock.DiffTime -import Gundeck.Types hiding (Push (..), Recipient, newPush) -import Gundeck.Types.Push.V2 qualified as V2 import Imports import Numeric.Natural (Natural) import Polysemy @@ -22,6 +20,8 @@ import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog qualified as P import System.Logger.Class as Log +import Wire.API.Push.V2 hiding (Push (..), Recipient, newPush) +import Wire.API.Push.V2 qualified as V2 import Wire.API.Team.Member import Wire.GundeckAPIAccess (GundeckAPIAccess) import Wire.GundeckAPIAccess qualified as GundeckAPIAccess @@ -42,7 +42,7 @@ runNotificationSubsystemGundeck :: runNotificationSubsystemGundeck cfg = interpret $ \case PushNotifications ps -> runInputConst cfg $ pushImpl ps PushNotificationsSlowly ps -> runInputConst cfg $ pushSlowlyImpl ps - PushNotificationsAsync ps -> runInputConst cfg $ pushAsyncImpl ps + PushNotificationAsync ps -> runInputConst cfg $ pushAsyncImpl ps CleanupUser uid -> GundeckAPIAccess.userDeleted uid UnregisterPushClient uid cid -> GundeckAPIAccess.unregisterPushClient uid cid GetPushTokens uid -> GundeckAPIAccess.getPushTokens uid @@ -75,11 +75,11 @@ pushAsyncImpl :: Member (Final IO) r, Member P.TinyLog r ) => - [Push] -> + Push -> Sem r (Async (Maybe ())) -pushAsyncImpl ps = async $ do +pushAsyncImpl p = async $ do reqId <- inputs requestId - errorToIOFinal @SomeException (fromExceptionSem @SomeException $ pushImpl ps) >>= \case + errorToIOFinal @SomeException (fromExceptionSem @SomeException $ pushImpl [p]) >>= \case Left e -> P.err $ Log.msg (Log.val "Error while pushing notifications") @@ -128,8 +128,8 @@ toV2Push p = recipients = map toRecipient $ toList p._pushRecipients toRecipient :: Recipient -> V2.Recipient toRecipient r = - (recipient r._recipientUserId p._pushRoute) - { V2._recipientClients = r._recipientClients + (recipient r.recipientUserId p._pushRoute) + { V2._recipientClients = r.recipientClients } {-# INLINE [1] chunkPushes #-} diff --git a/libs/wire-subsystems/src/Wire/PasswordResetCodeStore.hs b/libs/wire-subsystems/src/Wire/PasswordResetCodeStore.hs index dbf5502fc4a..b1db6098841 100644 --- a/libs/wire-subsystems/src/Wire/PasswordResetCodeStore.hs +++ b/libs/wire-subsystems/src/Wire/PasswordResetCodeStore.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE QuantifiedConstraints #-} -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -33,11 +34,9 @@ data PRQueryData f = PRQueryData prqdTimeout :: f UTCTime } -deriving instance Show (PRQueryData Identity) +deriving instance (forall a. (Show a) => Show (f a)) => Show (PRQueryData f) -deriving instance Eq (PRQueryData Maybe) - -deriving instance Show (PRQueryData Maybe) +deriving instance (forall a. (Eq a) => Eq (f a)) => Eq (PRQueryData f) mapPRQueryData :: (forall a. (f1 a -> f2 a)) -> PRQueryData f1 -> PRQueryData f2 mapPRQueryData f prqd = prqd {prqdRetries = f prqd.prqdRetries, prqdTimeout = f prqd.prqdTimeout} diff --git a/libs/wire-subsystems/src/Wire/PasswordResetCodeStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/PasswordResetCodeStore/Cassandra.hs index 74bdd0ca1f7..8b923551bc2 100644 --- a/libs/wire-subsystems/src/Wire/PasswordResetCodeStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/PasswordResetCodeStore/Cassandra.hs @@ -58,12 +58,7 @@ passwordResetCodeStoreToCassandra = . write codeInsertQuery . params LocalQuorum $ (prk, prc, uid, runIdentity n, runIdentity ut, ttl) - CodeDelete prk -> - retry x5 - . write codeDeleteQuery - . params LocalQuorum - . Identity - $ prk + CodeDelete prk -> codeDeleteImpl prk where toRecord :: (PasswordResetCode, UserId, Maybe Int32, Maybe UTCTime) -> @@ -79,6 +74,16 @@ genPhoneCode = PasswordResetCode . unsafeFromText . pack . printf "%06d" <$> liftIO (randIntegerZeroToNMinusOne 1000000) +-- FUTUREWORK(fisx,elland): this should be replaced by a method in a +-- future auth subsystem +codeDeleteImpl :: (MonadClient m) => PasswordResetKey -> m () +codeDeleteImpl prk = + retry x5 + . write codeDeleteQuery + . params LocalQuorum + . Identity + $ prk + interpretClientToIO :: (Member (Final IO) r) => ClientState -> diff --git a/libs/wire-subsystems/src/Wire/PasswordStore.hs b/libs/wire-subsystems/src/Wire/PasswordStore.hs index 48a358aa827..54b66aa02ea 100644 --- a/libs/wire-subsystems/src/Wire/PasswordStore.hs +++ b/libs/wire-subsystems/src/Wire/PasswordStore.hs @@ -10,5 +10,6 @@ import Wire.API.Password data PasswordStore m a where UpsertHashedPassword :: UserId -> Password -> PasswordStore m () LookupHashedPassword :: UserId -> PasswordStore m (Maybe Password) + 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 933faeb298d..1503e2152cb 100644 --- a/libs/wire-subsystems/src/Wire/PasswordStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/PasswordStore/Cassandra.hs @@ -16,6 +16,12 @@ interpretPasswordStore casClient = 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) +lookupProviderPasswordImpl u = + (runIdentity =<<) + <$> retry x1 (query1 providerPasswordSelect (params LocalQuorum (Identity u))) lookupPasswordImpl :: (MonadClient m) => UserId -> m (Maybe Password) lookupPasswordImpl u = @@ -29,6 +35,10 @@ updatePasswordImpl u p = do ------------------------------------------------------------------------ -- 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 = ?" diff --git a/libs/wire-subsystems/src/Wire/PropertyStore.hs b/libs/wire-subsystems/src/Wire/PropertyStore.hs new file mode 100644 index 00000000000..77e255581ca --- /dev/null +++ b/libs/wire-subsystems/src/Wire/PropertyStore.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.PropertyStore where + +import Data.Id +import Imports +import Polysemy +import Wire.API.Properties + +data PropertyStore m a where + InsertProperty :: UserId -> PropertyKey -> RawPropertyValue -> PropertyStore m () + LookupProperty :: UserId -> PropertyKey -> PropertyStore m (Maybe RawPropertyValue) + CountProperties :: UserId -> PropertyStore m Int + DeleteProperty :: UserId -> PropertyKey -> PropertyStore m () + ClearProperties :: UserId -> PropertyStore m () + GetPropertyKeys :: UserId -> PropertyStore m [PropertyKey] + GetAllProperties :: UserId -> PropertyStore m [(PropertyKey, RawPropertyValue)] + +makeSem ''PropertyStore diff --git a/libs/wire-subsystems/src/Wire/PropertyStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/PropertyStore/Cassandra.hs new file mode 100644 index 00000000000..f5a7189466e --- /dev/null +++ b/libs/wire-subsystems/src/Wire/PropertyStore/Cassandra.hs @@ -0,0 +1,78 @@ +module Wire.PropertyStore.Cassandra where + +import Cassandra +import Data.Id +import Imports +import Polysemy +import Polysemy.Embed +import Wire.API.Properties +import Wire.PropertyStore + +interpretPropertyStoreCassandra :: (Member (Embed IO) r) => ClientState -> InterpreterFor PropertyStore r +interpretPropertyStoreCassandra casClient = + interpret $ + runEmbedded (runClient @IO casClient) . embed . \case + InsertProperty u k v -> insertPropertyImpl u k v + LookupProperty u k -> lookupPropertyImpl u k + CountProperties u -> countPropertiesImpl u + DeleteProperty u k -> deletePropertyImpl u k + ClearProperties u -> clearPropertieImpl u + GetPropertyKeys u -> lookupPropertyKeyImpl u + GetAllProperties u -> getAllPropertiesImpl u + +insertPropertyImpl :: + (MonadClient m) => + UserId -> + PropertyKey -> + RawPropertyValue -> + m () +insertPropertyImpl u k v = + retry x5 $ write propertyInsert (params LocalQuorum (u, k, v)) + +deletePropertyImpl :: (MonadClient m) => UserId -> PropertyKey -> m () +deletePropertyImpl u k = retry x5 $ write propertyDelete (params LocalQuorum (u, k)) + +clearPropertieImpl :: (MonadClient m) => UserId -> m () +clearPropertieImpl u = retry x5 $ write propertyReset (params LocalQuorum (Identity u)) + +lookupPropertyImpl :: (MonadClient m) => UserId -> PropertyKey -> m (Maybe RawPropertyValue) +lookupPropertyImpl u k = + fmap runIdentity + <$> retry x1 (query1 propertySelect (params LocalQuorum (u, k))) + +lookupPropertyKeyImpl :: (MonadClient m) => UserId -> m [PropertyKey] +lookupPropertyKeyImpl u = + map runIdentity + <$> retry x1 (query propertyKeysSelect (params LocalQuorum (Identity u))) + +countPropertiesImpl :: (MonadClient m) => UserId -> m Int +countPropertiesImpl u = do + maybe 0 fromIntegral <$> retry x1 (query1 propertyCount (params LocalQuorum (Identity u))) + +getAllPropertiesImpl :: (MonadClient m) => UserId -> m [(PropertyKey, RawPropertyValue)] +getAllPropertiesImpl u = + retry x1 (query propertyKeysValuesSelect (params LocalQuorum (Identity u))) + +------------------------------------------------------------------------------- +-- Queries + +propertyInsert :: PrepQuery W (UserId, PropertyKey, RawPropertyValue) () +propertyInsert = "INSERT INTO properties (user, key, value) VALUES (?, ?, ?)" + +propertyDelete :: PrepQuery W (UserId, PropertyKey) () +propertyDelete = "DELETE FROM properties where user = ? and key = ?" + +propertyReset :: PrepQuery W (Identity UserId) () +propertyReset = "DELETE FROM properties where user = ?" + +propertySelect :: PrepQuery R (UserId, PropertyKey) (Identity RawPropertyValue) +propertySelect = "SELECT value FROM properties where user = ? and key = ?" + +propertyKeysSelect :: PrepQuery R (Identity UserId) (Identity PropertyKey) +propertyKeysSelect = "SELECT key FROM properties where user = ?" + +propertyKeysValuesSelect :: PrepQuery R (Identity UserId) (PropertyKey, RawPropertyValue) +propertyKeysValuesSelect = "SELECT key, value FROM properties where user = ?" + +propertyCount :: PrepQuery R (Identity UserId) (Identity Int64) +propertyCount = "SELECT COUNT(*) FROM properties where user = ?" diff --git a/libs/wire-subsystems/src/Wire/PropertySubsystem.hs b/libs/wire-subsystems/src/Wire/PropertySubsystem.hs new file mode 100644 index 00000000000..2a8bef98df2 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/PropertySubsystem.hs @@ -0,0 +1,42 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.PropertySubsystem where + +import Data.Id +import Data.Text.Lazy qualified as LText +import Imports +import Network.HTTP.Types +import Network.Wai.Utilities qualified as Wai +import Polysemy +import Wire.API.Error +import Wire.API.Error.Brig qualified as E +import Wire.API.Properties +import Wire.Error + +data PropertySubsystemError + = TooManyProperties + | PropertyKeyTooLarge + | PropertyValueTooLarge + | PropertyValueInvalid String + | StoredPropertyValueInvalid + deriving (Show, Eq) + +propertySubsystemErrorToHttpError :: PropertySubsystemError -> HttpError +propertySubsystemErrorToHttpError = + StdError . \case + TooManyProperties -> errorToWai @E.TooManyProperties + PropertyKeyTooLarge -> errorToWai @E.PropertyKeyTooLarge + PropertyValueTooLarge -> errorToWai @E.PropertyValueTooLarge + PropertyValueInvalid err -> Wai.mkError status400 "bad-request" (LText.pack err) + StoredPropertyValueInvalid -> Wai.mkError status500 "internal-server-error" "Internal Server Error" + +data PropertySubsystem m a where + SetProperty :: UserId -> ConnId -> PropertyKey -> RawPropertyValue -> PropertySubsystem m () + DeleteProperty :: UserId -> ConnId -> PropertyKey -> PropertySubsystem m () + ClearProperties :: UserId -> ConnId -> PropertySubsystem m () + OnUserDeleted :: UserId -> PropertySubsystem m () + LookupProperty :: UserId -> PropertyKey -> PropertySubsystem m (Maybe RawPropertyValue) + GetPropertyKeys :: UserId -> PropertySubsystem m [PropertyKey] + GetAllProperties :: UserId -> PropertySubsystem m PropertyKeysAndValues + +makeSem ''PropertySubsystem diff --git a/libs/wire-subsystems/src/Wire/PropertySubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/PropertySubsystem/Interpreter.hs new file mode 100644 index 00000000000..5c347928dfb --- /dev/null +++ b/libs/wire-subsystems/src/Wire/PropertySubsystem/Interpreter.hs @@ -0,0 +1,151 @@ +module Wire.PropertySubsystem.Interpreter where + +import Data.Aeson (Value) +import Data.Aeson qualified as Aeson +import Data.ByteString.Lazy qualified as LBS +import Data.Id +import Data.Map qualified as Map +import Data.Text qualified as Text +import Data.Text.Ascii qualified as Ascii +import Imports +import Polysemy +import Polysemy.Error +import Polysemy.Input +import Polysemy.TinyLog (TinyLog) +import Polysemy.TinyLog qualified as Log +import System.Logger.Message qualified as Log +import Wire.API.Properties +import Wire.API.UserEvent +import Wire.Events +import Wire.PropertyStore (PropertyStore) +import Wire.PropertyStore qualified as PropertyStore +import Wire.PropertySubsystem + +data PropertySubsystemConfig = PropertySubsystemConfig + { maxKeyLength :: Int64, + maxValueLength :: Int64, + maxProperties :: Int + } + +interpretPropertySubsystem :: + ( Member PropertyStore r, + Member (Error PropertySubsystemError) r, + Member Events r, + Member TinyLog r + ) => + PropertySubsystemConfig -> + InterpreterFor PropertySubsystem r +interpretPropertySubsystem cfg = + interpret $ + runInputConst cfg . \case + SetProperty uid connId key val -> setPropertyImpl uid connId key val + DeleteProperty uid connId key -> deletePropertyImpl uid connId key + ClearProperties uid connId -> clearPropertiesImpl uid connId + OnUserDeleted uid -> onUserDeletdImpl uid + LookupProperty uid key -> lookupPropertyImpl uid key + GetPropertyKeys uid -> getPropertyKeysImpl uid + GetAllProperties uid -> getAllPropertiesImpl uid + +setPropertyImpl :: + ( Member PropertyStore r, + Member (Input PropertySubsystemConfig) r, + Member (Error PropertySubsystemError) r, + Member Events r + ) => + UserId -> + ConnId -> + PropertyKey -> + RawPropertyValue -> + Sem r () +setPropertyImpl uid connId key val = do + validatePropertyKey key + checkMaxProperties uid key + parsedVal <- validatePropertyValue val + PropertyStore.insertProperty uid key val + generatePropertyEvent uid connId $ PropertySet key parsedVal + +checkMaxProperties :: + ( Member PropertyStore r, + Member (Input PropertySubsystemConfig) r, + Member (Error PropertySubsystemError) r + ) => + UserId -> + PropertyKey -> + Sem r () +checkMaxProperties uid key = do + propExists <- isJust <$> PropertyStore.lookupProperty uid key + unless propExists $ do + cfg <- input + count <- PropertyStore.countProperties uid + when (count >= cfg.maxProperties) $ + throw TooManyProperties + +validatePropertyKey :: + ( Member (Input PropertySubsystemConfig) r, + Member (Error PropertySubsystemError) r + ) => + PropertyKey -> + Sem r () +validatePropertyKey key = do + cfg <- input + let keyText = Ascii.toText $ propertyKeyName key + when (Text.compareLength keyText (fromIntegral cfg.maxKeyLength) == GT) $ + throw PropertyKeyTooLarge + +validatePropertyValue :: + ( Member (Input PropertySubsystemConfig) r, + Member (Error PropertySubsystemError) r + ) => + RawPropertyValue -> + Sem r Value +validatePropertyValue (RawPropertyValue bs) = do + cfg <- input + when (LBS.compareLength bs cfg.maxValueLength == GT) $ + throw PropertyValueTooLarge + + case Aeson.eitherDecode @Value bs of + Left e -> throw $ PropertyValueInvalid e + Right val -> pure val + +deletePropertyImpl :: (Member PropertyStore r, Member Events r) => UserId -> ConnId -> PropertyKey -> Sem r () +deletePropertyImpl uid connId key = do + PropertyStore.deleteProperty uid key + generatePropertyEvent uid connId $ PropertyDeleted key + +onUserDeletdImpl :: (Member PropertyStore r) => UserId -> Sem r () +onUserDeletdImpl uid = do + PropertyStore.clearProperties uid + +clearPropertiesImpl :: (Member PropertyStore r, Member Events r) => UserId -> ConnId -> Sem r () +clearPropertiesImpl uid connId = do + PropertyStore.clearProperties uid + generatePropertyEvent uid connId PropertiesCleared + +lookupPropertyImpl :: (Member PropertyStore r) => UserId -> PropertyKey -> Sem r (Maybe RawPropertyValue) +lookupPropertyImpl uid key = + PropertyStore.lookupProperty uid key + +getPropertyKeysImpl :: (Member PropertyStore r) => UserId -> Sem r [PropertyKey] +getPropertyKeysImpl uid = + PropertyStore.getPropertyKeys uid + +getAllPropertiesImpl :: + ( Member PropertyStore r, + Member TinyLog r, + Member (Error PropertySubsystemError) r + ) => + UserId -> + Sem r PropertyKeysAndValues +getAllPropertiesImpl uid = do + rawProps <- Map.fromList <$> PropertyStore.getAllProperties uid + PropertyKeysAndValues <$> traverse parseStoredPropertyValue rawProps + +parseStoredPropertyValue :: (Member TinyLog r, Member (Error PropertySubsystemError) r) => RawPropertyValue -> Sem r Value +parseStoredPropertyValue raw = case Aeson.eitherDecode raw.rawPropertyBytes of + Right value -> pure value + Left e -> do + Log.err $ + Log.msg (Log.val "Failed to parse a stored property value") + . Log.field "raw_value" raw.rawPropertyBytes + . Log.field "parse_error" e + throw StoredPropertyValueInvalid diff --git a/libs/wire-subsystems/src/Wire/Rpc.hs b/libs/wire-subsystems/src/Wire/Rpc.hs index 99f52727867..8a954eafddb 100644 --- a/libs/wire-subsystems/src/Wire/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/Rpc.hs @@ -43,11 +43,11 @@ runRpcWithHttp mgr reqId = interpret $ \case embed $ runHttpRpc mgr reqId $ rpcWithRetriesImpl serviceName ep req rpcImpl :: ServiceName -> Endpoint -> (Request -> Request) -> HttpRpc (Response (Maybe LByteString)) -rpcImpl serviceName ep req = +rpcImpl serviceName ep req = do rpc' serviceName empty $ req - . Bilge.host (encodeUtf8 ep._host) - . Bilge.port ep._port + . Bilge.host (encodeUtf8 ep.host) + . Bilge.port ep.port rpcWithRetriesImpl :: ServiceName -> Endpoint -> (Request -> Request) -> HttpRpc (Response (Maybe LByteString)) rpcWithRetriesImpl serviceName ep req = @@ -81,6 +81,7 @@ newtype HttpRpc a = HttpRpc {unHttpRpc :: ReaderT (Manager, RequestId) IO a} Applicative, Monad, MonadIO, + MonadUnliftIO, MonadThrow, MonadCatch, MonadMask, diff --git a/libs/wire-subsystems/src/Wire/StoredUser.hs b/libs/wire-subsystems/src/Wire/StoredUser.hs index b2ace0784cb..7d8420740af 100644 --- a/libs/wire-subsystems/src/Wire/StoredUser.hs +++ b/libs/wire-subsystems/src/Wire/StoredUser.hs @@ -19,9 +19,10 @@ import Wire.Arbitrary data StoredUser = StoredUser { id :: UserId, name :: Name, + textStatus :: Maybe TextStatus, pict :: Maybe Pict, - email :: Maybe Email, - phone :: Maybe Phone, + email :: Maybe EmailAddress, + emailUnvalidated :: Maybe EmailAddress, ssoId :: Maybe UserSSOId, accentId :: ColourId, assets :: Maybe [Asset], @@ -72,18 +73,19 @@ hasPendingInvitation u = u.status == Just PendingInvitation mkUserFromStored :: Domain -> Locale -> StoredUser -> User mkUserFromStored domain defaultLocale storedUser = - let deleted = Just Deleted == storedUser.status - expiration = if storedUser.status == Just Ephemeral then storedUser.expires else Nothing + let expiration = if storedUser.status == Just Ephemeral then storedUser.expires else Nothing loc = toLocale defaultLocale (storedUser.language, storedUser.country) svc = newServiceRef <$> storedUser.serviceId <*> storedUser.providerId in User { userQualifiedId = (Qualified storedUser.id domain), userIdentity = storedUser.identity, + userEmailUnvalidated = storedUser.emailUnvalidated, userDisplayName = storedUser.name, + userTextStatus = storedUser.textStatus, userPict = (fromMaybe noPict storedUser.pict), userAssets = (fromMaybe [] storedUser.assets), userAccentId = storedUser.accentId, - userDeleted = deleted, + userStatus = fromMaybe Active storedUser.status, userLocale = loc, userService = svc, userHandle = storedUser.handle, @@ -95,12 +97,6 @@ mkUserFromStored domain defaultLocale storedUser = Just ps -> if S.null ps then defSupportedProtocols else ps } -mkAccountFromStored :: Domain -> Locale -> StoredUser -> UserAccount -mkAccountFromStored domain defaultLocale storedUser = - UserAccount - (mkUserFromStored domain defaultLocale storedUser) - (fromMaybe Active storedUser.status) - toLocale :: Locale -> (Maybe Language, Maybe Country) -> Locale toLocale _ (Just l, c) = Locale l c toLocale l _ = l @@ -114,7 +110,7 @@ toLocale l _ = l toIdentity :: -- | Whether the user is activated Bool -> - Maybe Email -> + Maybe EmailAddress -> Maybe UserSSOId -> Maybe UserIdentity toIdentity True (Just e) Nothing = Just $! EmailIdentity e diff --git a/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem.hs b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem.hs new file mode 100644 index 00000000000..09cb54a6250 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.TeamInvitationSubsystem where + +import Data.Id +import Data.Qualified +import Imports +import Polysemy +import Wire.API.Team.Invitation +import Wire.API.Team.Role +import Wire.API.User (InvitationCode) +import Wire.API.User.EmailAddress + +data TeamInvitationSubsystem m a where + InviteUser :: Local UserId -> TeamId -> InvitationRequest -> TeamInvitationSubsystem m (Invitation, InvitationLocation) + -- | This function exists to support migration in this susbystem, after the + -- migration this would just be an internal detail of the subsystem + InternalCreateInvitation :: TeamId -> Maybe InvitationId -> Role -> Local (Maybe UserId) -> EmailAddress -> InvitationRequest -> TeamInvitationSubsystem m (Invitation, InvitationCode) + +makeSem ''TeamInvitationSubsystem diff --git a/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Error.hs b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Error.hs new file mode 100644 index 00000000000..892450e3354 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Error.hs @@ -0,0 +1,23 @@ +module Wire.TeamInvitationSubsystem.Error where + +import Imports +import Wire.API.Error +import Wire.API.Error.Brig qualified as E +import Wire.Error + +data TeamInvitationSubsystemError + = TeamInvitationNoEmail + | TeamInvitationInsufficientTeamPermissions + | TooManyTeamInvitations + | TeamInvitationBlacklistedEmail + | TeamInvitationEmailTaken + deriving (Show) + +teamInvitationErrorToHttpError :: TeamInvitationSubsystemError -> HttpError +teamInvitationErrorToHttpError = + StdError . \case + TeamInvitationNoEmail -> errorToWai @E.NoEmail + TeamInvitationInsufficientTeamPermissions -> errorToWai @E.InsufficientTeamPermissions + TooManyTeamInvitations -> errorToWai @E.TooManyTeamInvitations + TeamInvitationBlacklistedEmail -> errorToWai @E.BlacklistedEmail + TeamInvitationEmailTaken -> errorToWai @E.EmailExists diff --git a/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs new file mode 100644 index 00000000000..46445645c9d --- /dev/null +++ b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs @@ -0,0 +1,257 @@ +module Wire.TeamInvitationSubsystem.Interpreter where + +import Control.Arrow ((&&&)) +import Control.Error (MaybeT (..)) +import Data.ByteString.Conversion (toByteString') +import Data.Id +import Data.Qualified +import Data.Set qualified as Set +import Data.Text.Ascii qualified as AsciiText +import Data.Text.Encoding qualified as Text +import Imports +import Polysemy +import Polysemy.Error +import Polysemy.Input (Input, input, runInputConst) +import Polysemy.TinyLog +import System.Logger.Message as Log +import URI.ByteString +import Util.Logging +import Util.Timeout (Timeout (..)) +import Wire.API.Team.Invitation +import Wire.API.Team.Member +import Wire.API.Team.Member qualified as Teams +import Wire.API.Team.Permission +import Wire.API.Team.Role +import Wire.API.User +import Wire.Arbitrary +import Wire.EmailSubsystem +import Wire.GalleyAPIAccess hiding (AddTeamMember) +import Wire.GalleyAPIAccess qualified as GalleyAPIAccess +import Wire.InvitationStore (InvitationStore, StoredInvitation) +import Wire.InvitationStore qualified as Store +import Wire.Sem.Logger qualified as Log +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.TeamInvitationSubsystem +import Wire.TeamInvitationSubsystem.Error +import Wire.UserKeyStore +import Wire.UserSubsystem (UserSubsystem, getLocalUserAccountByUserKey, getSelfProfile, isBlocked) + +data TeamInvitationSubsystemConfig = TeamInvitationSubsystemConfig + { maxTeamSize :: Word32, + teamInvitationTimeout :: Timeout + } + deriving (Show, Generic) + deriving (Arbitrary) via GenericUniform TeamInvitationSubsystemConfig + +runTeamInvitationSubsystem :: + ( Member (Error TeamInvitationSubsystemError) r, + Member TinyLog r, + Member GalleyAPIAccess r, + Member UserSubsystem r, + Member Random r, + Member InvitationStore r, + Member Now r, + Member EmailSubsystem r + ) => + TeamInvitationSubsystemConfig -> + InterpreterFor TeamInvitationSubsystem r +runTeamInvitationSubsystem cfg = interpret $ \case + InviteUser luid tid request -> runInputConst cfg $ inviteUserImpl luid tid request + InternalCreateInvitation tid mExpectedInvId role mbInviterUid inviterEmail invRequest -> + runInputConst cfg $ createInvitation' tid mExpectedInvId role mbInviterUid inviterEmail invRequest + +inviteUserImpl :: + ( Member (Error TeamInvitationSubsystemError) r, + Member GalleyAPIAccess r, + Member UserSubsystem r, + Member TinyLog r, + Member Random r, + Member InvitationStore r, + Member (Input TeamInvitationSubsystemConfig) r, + Member Now r, + Member EmailSubsystem r + ) => + Local UserId -> + TeamId -> + InvitationRequest -> + Sem r (Invitation, InvitationLocation) +inviteUserImpl luid tid request = do + let inviteeRole = fromMaybe defaultRole request.role + + let inviteePerms = Teams.rolePermissions inviteeRole + ensurePermissionToAddUser (tUnqualified luid) tid inviteePerms + + inviterEmail <- + note TeamInvitationNoEmail =<< runMaybeT do + self <- MaybeT $ getSelfProfile luid + MaybeT . pure . userEmail $ selfUser self + + let context = + logFunction "Brig.Team.API.createInvitation" + . logTeam tid + . logEmail request.inviteeEmail + + (id &&& loc) . fst + <$> logInvitationRequest + context + (createInvitation' tid Nothing inviteeRole (Just <$> luid) inviterEmail request) + where + loc :: Invitation -> InvitationLocation + loc inv = + InvitationLocation $ "/teams/" <> toByteString' tid <> "/invitations/" <> toByteString' inv.invitationId + +createInvitation' :: + ( Member GalleyAPIAccess r, + Member UserSubsystem r, + Member InvitationStore r, + Member TinyLog r, + Member (Error TeamInvitationSubsystemError) r, + Member Random r, + Member (Input TeamInvitationSubsystemConfig) r, + Member Now r, + Member EmailSubsystem r + ) => + TeamId -> + Maybe InvitationId -> + Role -> + Local (Maybe UserId) -> + EmailAddress -> + InvitationRequest -> + Sem r (Invitation, InvitationCode) +createInvitation' tid mExpectedInvId inviteeRole mbInviterUid inviterEmail invRequest = do + let email = invRequest.inviteeEmail + let uke = qualifyAs mbInviterUid $ mkEmailKey email + blacklistedEm <- isBlocked email + when blacklistedEm $ + throw TeamInvitationBlacklistedEmail + + mEmailOwner <- getLocalUserAccountByUserKey uke + isPersonalUserMigration <- case mEmailOwner of + Nothing -> pure False + Just user -> + if (user.userStatus == Active && isNothing user.userTeam) + then pure True + else throw TeamInvitationEmailTaken + + maxSize <- maxTeamSize <$> input + pending <- Store.countInvitations tid + when (fromIntegral pending >= maxSize) $ + throw TooManyTeamInvitations + + showInvitationUrl <- GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid + + do + iid <- maybe (Id <$> Random.uuid) pure mExpectedInvId + now <- Now.get + timeout <- teamInvitationTimeout <$> input + code <- mkInvitationCode + newInv <- + let insertInv = + Store.MkInsertInvitation + { invitationId = iid, + teamId = tid, + role = inviteeRole, + createdAt = now, + createdBy = tUnqualified mbInviterUid, + inviteeEmail = email, + inviteeName = invRequest.inviteeName, + code = code + -- mUrl = mUrl + } + in Store.insertInvitation insertInv timeout + + let sendOp = + if isPersonalUserMigration + then sendTeamInvitationMailPersonalUser + else sendTeamInvitationMail + + invitationUrl <- sendOp email tid inviterEmail code invRequest.locale + inv <- toInvitation invitationUrl showInvitationUrl newInv + pure (inv, code) + +mkInvitationCode :: (Member Random r) => Sem r InvitationCode +mkInvitationCode = InvitationCode . AsciiText.encodeBase64Url <$> Random.bytes 24 + +-- | brig used to not store the role, so for migration we allow this to be empty and fill in the +-- default here. +toInvitation :: + forall r. + (Member TinyLog r) => + Text -> + ShowOrHideInvitationUrl -> + StoredInvitation -> + Sem r Invitation +toInvitation urlText showUrl storedInv = do + url <- + case showUrl of + HideInvitationUrl -> pure Nothing + ShowInvitationUrl -> parseHttpsUrl urlText + pure $ + Invitation + { team = storedInv.teamId, + role = fromMaybe defaultRole storedInv.role, + invitationId = storedInv.invitationId, + createdAt = storedInv.createdAt, + createdBy = storedInv.createdBy, + inviteeEmail = storedInv.email, + inviteeName = storedInv.name, + inviteeUrl = url + } + where + parseHttpsUrl :: Text -> Sem r (Maybe (URIRef Absolute)) + parseHttpsUrl url = + either (\e -> Nothing <$ logError url e) (pure . Just) $ + parseURI laxURIParserOptions (Text.encodeUtf8 url) + + logError url e = + Log.err $ + Log.msg @Text "Unable to create invitation url. Please check configuration." + . Log.field "url" url + . Log.field "error" (show e) + +logInvitationRequest :: + (Member TinyLog r, Member (Error TeamInvitationSubsystemError) r) => + (Msg -> Msg) -> + Sem (Error TeamInvitationSubsystemError : r) (Invitation, InvitationCode) -> + Sem r (Invitation, InvitationCode) +logInvitationRequest context action = + runError action >>= \case + Left e -> do + Log.warn $ + msg @String ("Failed to create invitation: " <> show e) + . context + throw e + Right res@(_, code) -> do + Log.info $ + msg @ByteString "Successfully created invitation" + . context + . logInvitationCode code + pure res + +-- | Privilege escalation detection (make sure no `RoleMember` user creates a `RoleOwner`). +-- +-- There is some code duplication with 'Galley.API.Teams.ensureNotElevated'. +ensurePermissionToAddUser :: + ( Member GalleyAPIAccess r, + Member (Error TeamInvitationSubsystemError) r + ) => + UserId -> + TeamId -> + Permissions -> + Sem r () +ensurePermissionToAddUser u t inviteePerms = do + minviter <- GalleyAPIAccess.getTeamMember u t + unless (check minviter) $ + throw TeamInvitationInsufficientTeamPermissions + where + check :: Maybe TeamMember -> Bool + check (Just inviter) = + hasPermission inviter AddTeamMember + && all (mayGrantPermission inviter) (Set.toList (inviteePerms.self)) + check Nothing = False + +logInvitationCode :: InvitationCode -> (Msg -> Msg) +logInvitationCode code = field "invitation_code" (AsciiText.toText $ fromInvitationCode code) diff --git a/libs/wire-subsystems/src/Wire/UserEvents.hs b/libs/wire-subsystems/src/Wire/UserEvents.hs deleted file mode 100644 index 0288dee8d92..00000000000 --- a/libs/wire-subsystems/src/Wire/UserEvents.hs +++ /dev/null @@ -1,13 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} - -module Wire.UserEvents where - -import Data.Id -import Imports -import Polysemy -import Wire.API.UserEvent - -data UserEvents m a where - GenerateUserEvent :: UserId -> Maybe ConnId -> UserEvent -> UserEvents m () - -makeSem ''UserEvents diff --git a/libs/wire-subsystems/src/Wire/UserKeyStore.hs b/libs/wire-subsystems/src/Wire/UserKeyStore.hs index 5683c25b763..d372d150450 100644 --- a/libs/wire-subsystems/src/Wire/UserKeyStore.hs +++ b/libs/wire-subsystems/src/Wire/UserKeyStore.hs @@ -4,7 +4,8 @@ module Wire.UserKeyStore where import Data.Id import Data.Text qualified as Text -import Imports +import Data.Text.Encoding (decodeUtf8) +import Imports hiding (local) import Polysemy import Test.QuickCheck import Wire.API.User @@ -12,7 +13,7 @@ import Wire.API.User -- | An 'EmailKey' is an 'Email' in a form that serves as a unique lookup key. data EmailKey = EmailKey { emailKeyUniq :: !Text, - emailKeyOrig :: !Email + emailKeyOrig :: !EmailAddress } deriving (Ord) @@ -33,14 +34,16 @@ instance Arbitrary EmailKey where -- e-mail addresses fully case-insensitive. -- * "+" suffixes on the local part are stripped unless the domain -- part is contained in a trusted whitelist. -mkEmailKey :: Email -> EmailKey -mkEmailKey orig@(Email localPart domain) = +mkEmailKey :: EmailAddress -> EmailKey +mkEmailKey orig = let uniq = Text.toLower localPart' <> "@" <> Text.toLower domain in EmailKey uniq orig where + domain = decodeUtf8 . domainPart $ orig + local = decodeUtf8 . localPart $ orig localPart' - | domain `notElem` trusted = Text.takeWhile (/= '+') localPart - | otherwise = localPart + | (domainPart orig) `notElem` trusted = Text.takeWhile (/= '+') local + | otherwise = decodeUtf8 (localPart orig) trusted = ["wearezeta.com", "wire.com", "simulator.amazonses.com"] data UserKeyStore m a where diff --git a/libs/wire-subsystems/src/Wire/UserSearch/Metrics.hs b/libs/wire-subsystems/src/Wire/UserSearch/Metrics.hs new file mode 100644 index 00000000000..656186a5f18 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/UserSearch/Metrics.hs @@ -0,0 +1,44 @@ +module Wire.UserSearch.Metrics where + +import Imports +import Prometheus qualified as Prom + +{-# NOINLINE indexUpdateCounter #-} +indexUpdateCounter :: Prom.Counter +indexUpdateCounter = + Prom.unsafeRegister $ + Prom.counter + Prom.Info + { Prom.metricName = "user_index_update_count", + Prom.metricHelp = "Number of updates on user index" + } + +{-# NOINLINE indexUpdateErrorCounter #-} +indexUpdateErrorCounter :: Prom.Counter +indexUpdateErrorCounter = + Prom.unsafeRegister $ + Prom.counter + Prom.Info + { Prom.metricName = "user_index_update_err", + Prom.metricHelp = "Number of errors during user index update" + } + +{-# NOINLINE indexUpdateSuccessCounter #-} +indexUpdateSuccessCounter :: Prom.Counter +indexUpdateSuccessCounter = + Prom.unsafeRegister $ + Prom.counter + Prom.Info + { Prom.metricName = "user_index_update_ok", + Prom.metricHelp = "Number of successful user index updates" + } + +{-# NOINLINE indexDeleteCounter #-} +indexDeleteCounter :: Prom.Counter +indexDeleteCounter = + Prom.unsafeRegister $ + Prom.counter + Prom.Info + { Prom.metricName = "user_index_delete_count", + Prom.metricHelp = "Number of deletes on user index" + } diff --git a/libs/wire-subsystems/src/Wire/UserSearch/Migration.hs b/libs/wire-subsystems/src/Wire/UserSearch/Migration.hs new file mode 100644 index 00000000000..da343e721b1 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/UserSearch/Migration.hs @@ -0,0 +1,30 @@ +module Wire.UserSearch.Migration where + +import Data.Aeson +import Database.Bloodhound.Types qualified as ES +import Imports +import Numeric.Natural +import System.Logger.Class (ToBytes (..)) + +newtype MigrationVersion = MigrationVersion {migrationVersion :: Natural} + deriving (Show, Eq, Ord) + +instance ToJSON MigrationVersion where + toJSON (MigrationVersion v) = object ["migration_version" .= v] + +instance FromJSON MigrationVersion where + parseJSON = withObject "MigrationVersion" $ \o -> MigrationVersion <$> o .: "migration_version" + +instance ToBytes MigrationVersion where + bytes = bytes . toInteger . migrationVersion + +data MigrationException + = CreateMigrationIndexFailed String + | FetchMigrationVersionsFailed String + | PersistVersionFailed MigrationVersion String + | PutMappingFailed String + | TargetIndexAbsent + | VersionSourceMissing (ES.SearchResult MigrationVersion) + deriving (Show) + +instance Exception MigrationException diff --git a/libs/wire-subsystems/src/Wire/UserSearch/Types.hs b/libs/wire-subsystems/src/Wire/UserSearch/Types.hs new file mode 100644 index 00000000000..61fab5fe704 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/UserSearch/Types.hs @@ -0,0 +1,222 @@ +{-# LANGUAGE RecordWildCards #-} + +module Wire.UserSearch.Types where + +import Cassandra qualified as C +import Cassandra.Util +import Data.Aeson +import Data.Attoparsec.ByteString +import Data.ByteString.Builder +import Data.ByteString.Conversion +import Data.ByteString.Lazy +import Data.Handle +import Data.Id +import Data.Json.Util +import Data.Text.Encoding +import Database.Bloodhound.Types +import Imports +import Test.QuickCheck +import Wire.API.Team.Feature +import Wire.API.Team.Role +import Wire.API.User +import Wire.API.User.Search +import Wire.Arbitrary + +newtype IndexVersion = IndexVersion {docVersion :: DocVersion} + +mkIndexVersion :: [Maybe (Writetime x)] -> IndexVersion +mkIndexVersion writetimes = + let maxVersion = getMax . mconcat . fmap (Max . writetimeToInt64) $ catMaybes writetimes + in -- This minBound case would only get triggered when the maxVersion is <= 0 + -- or >= 9.2e+18. First case can happen when the writetimes list is empty + -- or contains a timestamp before the unix epoch, which is unlikely. + -- Second case will happen in a few billion years. It is also not really a + -- restriction in ES, Bloodhound's authors' interpretation of the the ES + -- documentation caused this limiation, otherwise `maxBound :: Int64`, + -- would be acceptable by ES. + IndexVersion . fromMaybe minBound . mkDocVersion . fromIntegral $ maxVersion + +-- | Represents an ES *document*, ie. the subset of user attributes stored in ES. +-- See also 'IndexUser'. +-- +-- If a user is not searchable, e.g. because the account got +-- suspended, all fields except for the user id are set to 'Nothing' and +-- consequently removed from the index. +data UserDoc = UserDoc + { udId :: UserId, + udTeam :: Maybe TeamId, + udName :: Maybe Name, + udNormalized :: Maybe Text, + udHandle :: Maybe Handle, + udEmail :: Maybe EmailAddress, + udColourId :: Maybe ColourId, + udAccountStatus :: Maybe AccountStatus, + udSAMLIdP :: Maybe Text, + udManagedBy :: Maybe ManagedBy, + udCreatedAt :: Maybe UTCTimeMillis, + udRole :: Maybe Role, + udSearchVisibilityInbound :: Maybe SearchVisibilityInbound, + udScimExternalId :: Maybe Text, + udSso :: Maybe Sso, + udEmailUnvalidated :: Maybe EmailAddress + } + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform UserDoc) + +instance ToJSON UserDoc where + toJSON ud = + object + [ "id" .= udId ud, + "team" .= udTeam ud, + "name" .= udName ud, + "normalized" .= udNormalized ud, + "handle" .= udHandle ud, + "email" .= udEmail ud, + "accent_id" .= udColourId ud, + "account_status" .= udAccountStatus ud, + "saml_idp" .= udSAMLIdP ud, + "managed_by" .= udManagedBy ud, + "created_at" .= udCreatedAt ud, + "role" .= udRole ud, + searchVisibilityInboundFieldName .= udSearchVisibilityInbound ud, + "scim_external_id" .= udScimExternalId ud, + "sso" .= udSso ud, + "email_unvalidated" .= udEmailUnvalidated ud + ] + +instance FromJSON UserDoc where + parseJSON = withObject "UserDoc" $ \o -> + UserDoc + <$> o .: "id" + <*> o .:? "team" + <*> o .:? "name" + <*> o .:? "normalized" + <*> o .:? "handle" + <*> o .:? "email" + <*> o .:? "accent_id" + <*> o .:? "account_status" + <*> o .:? "saml_idp" + <*> o .:? "managed_by" + <*> o .:? "created_at" + <*> o .:? "role" + <*> o .:? searchVisibilityInboundFieldName + <*> o .:? "scim_external_id" + <*> o .:? "sso" + <*> o .:? "email_unvalidated" + +searchVisibilityInboundFieldName :: Key +searchVisibilityInboundFieldName = "search_visibility_inbound" + +userDocToTeamContact :: UserDoc -> TeamContact +userDocToTeamContact UserDoc {..} = + TeamContact + { teamContactUserId = udId, + teamContactTeam = udTeam, + teamContactSso = udSso, + teamContactScimExternalId = udScimExternalId, + teamContactSAMLIdp = udSAMLIdP, + teamContactRole = udRole, + teamContactName = maybe "" fromName udName, + teamContactManagedBy = udManagedBy, + teamContactHandle = fromHandle <$> udHandle, + teamContactEmailUnvalidated = udEmailUnvalidated, + teamContactEmail = udEmail, + teamContactCreatedAt = udCreatedAt, + teamContactColorId = fromIntegral . fromColourId <$> udColourId + } + +-- | Outbound search restrictions configured by team admin of the searcher. This +-- value restricts the set of user that are searched. +-- +-- See 'optionallySearchWithinTeam' for the effect on full-text search. +-- +-- See 'mkTeamSearchInfo' for the business logic that defines the TeamSearchInfo +-- value. +-- +-- Search results might be affected by the inbound search restriction settings of +-- the searched user. ('SearchVisibilityInbound') +data TeamSearchInfo + = -- | Only users that are not part of any team are searched + NoTeam + | -- | Only users from the same team as the searcher are searched + TeamOnly TeamId + | -- | No search restrictions, all users are searched + AllUsers + +-- | Inbound search restrictions configured by team to-be-searched. Affects only +-- full-text search (i.e. search on the display name and the handle), not exact +-- handle search. +data SearchVisibilityInbound + = -- | The user can only be found by users from the same team + SearchableByOwnTeam + | -- | The user can by found by any user of any team + SearchableByAllTeams + deriving (Eq, Show) + +instance Arbitrary SearchVisibilityInbound where + arbitrary = elements [SearchableByOwnTeam, SearchableByAllTeams] + +instance ToByteString SearchVisibilityInbound where + builder SearchableByOwnTeam = "searchable-by-own-team" + builder SearchableByAllTeams = "searchable-by-all-teams" + +instance FromByteString SearchVisibilityInbound where + parser = + SearchableByOwnTeam + <$ string "searchable-by-own-team" + <|> SearchableByAllTeams + <$ string "searchable-by-all-teams" + +instance C.Cql SearchVisibilityInbound where + ctype = C.Tagged C.IntColumn + + toCql SearchableByOwnTeam = C.CqlInt 0 + toCql SearchableByAllTeams = C.CqlInt 1 + + fromCql (C.CqlInt 0) = pure SearchableByOwnTeam + fromCql (C.CqlInt 1) = pure SearchableByAllTeams + fromCql n = Left $ "Unexpected SearchVisibilityInbound: " ++ show n + +defaultSearchVisibilityInbound :: SearchVisibilityInbound +defaultSearchVisibilityInbound = SearchableByOwnTeam + +searchVisibilityInboundFromFeatureStatus :: FeatureStatus -> SearchVisibilityInbound +searchVisibilityInboundFromFeatureStatus FeatureStatusDisabled = SearchableByOwnTeam +searchVisibilityInboundFromFeatureStatus FeatureStatusEnabled = SearchableByAllTeams + +instance ToJSON SearchVisibilityInbound where + toJSON = String . decodeUtf8 . toStrict . toLazyByteString . builder + +instance FromJSON SearchVisibilityInbound where + parseJSON = withText "SearchVisibilityInbound" $ \str -> + case runParser (parser @SearchVisibilityInbound) (encodeUtf8 str) of + Left err -> fail err + Right result -> pure result + +data IndexQuery r = IndexQuery Query Filter [DefaultSort] + +data BrowseTeamFilters = BrowseTeamFilters + { teamId :: TeamId, + mQuery :: Maybe Text, + mRoleFilter :: Maybe RoleFilter, + mSortBy :: Maybe TeamUserSearchSortBy, + mSortOrder :: Maybe TeamUserSearchSortOrder + } + +userIdToDocId :: UserId -> DocId +userIdToDocId uid = DocId (idToText uid) + +-- | We use cassandra writetimes to construct the ES index version. Since nulling fields in +-- cassandra also nulls the writetime, re-indexing does not happen when nulling a field, and +-- the old search key can still effectively be used. +-- +-- `write_time_bumper type int` is an extra field that we can update whenever we null a field +-- and want to update the write time of the table. `WriteTimeBumper` writes to 'int' fields, +-- but only cares about the field's writetime. +data WriteTimeBumper = WriteTimeBumper + deriving (Eq, Show) + +instance C.Cql WriteTimeBumper where + ctype = C.Tagged C.IntColumn + toCql WriteTimeBumper = C.CqlInt 0 + fromCql _ = pure WriteTimeBumper diff --git a/libs/wire-subsystems/src/Wire/UserStore.hs b/libs/wire-subsystems/src/Wire/UserStore.hs index fc4260a5a3d..a5189d29818 100644 --- a/libs/wire-subsystems/src/Wire/UserStore.hs +++ b/libs/wire-subsystems/src/Wire/UserStore.hs @@ -1,17 +1,21 @@ {-# LANGUAGE TemplateHaskell #-} -{-# OPTIONS_GHC -Wno-ambiguous-fields #-} module Wire.UserStore where +import Cassandra (PageWithState (..), PagingState) import Data.Default import Data.Handle import Data.Id +import Data.Time.Clock import Imports import Polysemy import Polysemy.Error +import Wire.API.Password import Wire.API.User +import Wire.API.User.RichInfo import Wire.Arbitrary import Wire.StoredUser +import Wire.UserStore.IndexUser -- | Update of any "simple" attributes (ones that do not involve locking, like handle, or -- validation protocols, like email). @@ -19,6 +23,7 @@ import Wire.StoredUser -- | see 'UserProfileUpdate'. data StoredUserUpdate = MkStoredUserUpdate { name :: Maybe Name, + textStatus :: Maybe TextStatus, pict :: Maybe Pict, assets :: Maybe [Asset], accentId :: Maybe ColourId, @@ -29,7 +34,7 @@ data StoredUserUpdate = MkStoredUserUpdate deriving (Arbitrary) via GenericUniform StoredUserUpdate instance Default StoredUserUpdate where - def = MkStoredUserUpdate Nothing Nothing Nothing Nothing Nothing Nothing + def = MkStoredUserUpdate Nothing Nothing Nothing Nothing Nothing Nothing Nothing -- | Update user handle (this involves several http requests for locking the required handle). -- The old/previous handle (for deciding idempotency). @@ -45,7 +50,9 @@ data StoredUserUpdateError = StoredUserUpdateHandleExists -- | Effect containing database logic around 'StoredUser'. (Example: claim handle lock is -- database logic; validate handle is application logic.) data UserStore m a where - GetUser :: UserId -> UserStore m (Maybe StoredUser) + GetIndexUser :: UserId -> UserStore m (Maybe IndexUser) + GetIndexUsersPaginated :: Int32 -> Maybe PagingState -> UserStore m (PageWithState IndexUser) + GetUsers :: [UserId] -> UserStore m [StoredUser] UpdateUser :: UserId -> StoredUserUpdate -> UserStore m () UpdateUserHandleEither :: UserId -> StoredUserHandleUpdate -> UserStore m (Either StoredUserUpdateError ()) DeleteUser :: User -> UserStore m () @@ -62,9 +69,16 @@ data UserStore m a where -- an email address or phone number. IsActivated :: UserId -> UserStore m Bool LookupLocale :: UserId -> UserStore m (Maybe (Maybe Language, Maybe Country)) + UpdateUserTeam :: UserId -> TeamId -> UserStore m () + GetActivityTimestamps :: UserId -> UserStore m [Maybe UTCTime] + GetRichInfo :: UserId -> UserStore m (Maybe RichInfoAssocList) + GetUserAuthenticationInfo :: UserId -> UserStore m (Maybe (Maybe Password, AccountStatus)) makeSem ''UserStore +getUser :: (Member UserStore r) => UserId -> Sem r (Maybe StoredUser) +getUser uid = listToMaybe <$> getUsers [uid] + updateUserHandle :: (Member UserStore r, Member (Error StoredUserUpdateError) r) => UserId -> diff --git a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs index cba7356f22e..96e78df99d3 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -1,36 +1,92 @@ module Wire.UserStore.Cassandra (interpretUserStoreCassandra) where import Cassandra +import Cassandra.Exec (prepared) import Data.Handle import Data.Id +import Data.Time.Clock import Database.CQL.Protocol import Imports import Polysemy import Polysemy.Embed import Polysemy.Error +import Wire.API.Password (Password) import Wire.API.User hiding (DeleteUser) +import Wire.API.User.RichInfo import Wire.StoredUser import Wire.UserStore +import Wire.UserStore.IndexUser hiding (userId) import Wire.UserStore.Unique interpretUserStoreCassandra :: (Member (Embed IO) r) => ClientState -> InterpreterFor UserStore r interpretUserStoreCassandra casClient = interpret $ - runEmbedded (runClient casClient) . \case - GetUser uid -> getUserImpl uid - UpdateUser uid update -> embed $ updateUserImpl uid update - UpdateUserHandleEither uid update -> embed $ updateUserHandleEitherImpl uid update - DeleteUser user -> embed $ deleteUserImpl user - LookupHandle hdl -> embed $ lookupHandleImpl LocalQuorum hdl - GlimpseHandle hdl -> embed $ lookupHandleImpl One hdl - LookupStatus uid -> embed $ lookupStatusImpl uid - IsActivated uid -> embed $ isActivatedImpl uid - LookupLocale uid -> embed $ lookupLocaleImpl uid - -getUserImpl :: (Member (Embed Client) r) => UserId -> Sem r (Maybe StoredUser) -getUserImpl uid = embed $ do - mUserTuple <- retry x1 $ query1 selectUser (params LocalQuorum (Identity uid)) - pure $ asRecord <$> mUserTuple + runEmbedded (runClient casClient) . embed . \case + GetUsers uids -> getUsersImpl uids + GetIndexUser uid -> getIndexUserImpl uid + GetIndexUsersPaginated pageSize mPagingState -> getIndexUserPaginatedImpl pageSize mPagingState + UpdateUser uid update -> updateUserImpl uid update + UpdateUserHandleEither uid update -> updateUserHandleEitherImpl uid update + DeleteUser user -> deleteUserImpl user + LookupHandle hdl -> lookupHandleImpl LocalQuorum hdl + GlimpseHandle hdl -> lookupHandleImpl One hdl + LookupStatus uid -> lookupStatusImpl uid + IsActivated uid -> isActivatedImpl uid + LookupLocale uid -> lookupLocaleImpl uid + UpdateUserTeam uid tid -> updateUserTeamImpl uid tid + GetActivityTimestamps uid -> getActivityTimestampsImpl uid + GetRichInfo uid -> getRichInfoImpl uid + GetUserAuthenticationInfo uid -> getUserAuthenticationInfoImpl uid + +getUserAuthenticationInfoImpl :: UserId -> Client (Maybe (Maybe Password, AccountStatus)) +getUserAuthenticationInfoImpl uid = fmap f <$> retry x1 (query1 authSelect (params LocalQuorum (Identity uid))) + where + f (pw, st) = (pw, fromMaybe Active st) + authSelect :: PrepQuery R (Identity UserId) (Maybe Password, Maybe AccountStatus) + authSelect = + [sql| + SELECT password, status FROM user WHERE id = ? + |] + +getUsersImpl :: [UserId] -> Client [StoredUser] +getUsersImpl usrs = + map asRecord + <$> retry x1 (query selectUsers (params LocalQuorum (Identity usrs))) + +getIndexUserImpl :: UserId -> Client (Maybe IndexUser) +getIndexUserImpl u = do + mIndexUserTuple <- retry x1 $ query1 cql (params LocalQuorum (Identity u)) + pure $ asRecord <$> mIndexUserTuple + where + cql :: PrepQuery R (Identity UserId) (TupleType IndexUser) + cql = prepared . QueryString $ getIndexUserBaseQuery <> " WHERE id = ?" + +getIndexUserPaginatedImpl :: Int32 -> Maybe PagingState -> Client (PageWithState IndexUser) +getIndexUserPaginatedImpl pageSize mPagingState = + asRecord <$$> paginateWithState cql (paramsPagingState LocalQuorum () pageSize mPagingState) + where + cql :: PrepQuery R () (TupleType IndexUser) + cql = prepared $ QueryString getIndexUserBaseQuery + +getIndexUserBaseQuery :: LText +getIndexUserBaseQuery = + [sql| + SELECT + id, + team, writetime(team), + name, writetime(name), + status, writetime(status), + handle, writetime(handle), + email, writetime(email), + accent_id, writetime(accent_id), + activated, writetime(activated), + service, writetime(service), + managed_by, writetime(managed_by), + sso_id, writetime(sso_id), + email_unvalidated, writetime(email_unvalidated), + writetime(write_time_bumper) + FROM user + |] updateUserImpl :: UserId -> StoredUserUpdate -> Client () updateUserImpl uid update = @@ -42,6 +98,7 @@ updateUserImpl uid update = setType BatchLogged setConsistency LocalQuorum for_ update.name \n -> addPrepQuery userDisplayNameUpdate (n, uid) + for_ update.textStatus \s -> addPrepQuery userTextStatusUpdate (s, uid) for_ update.pict \p -> addPrepQuery userPictUpdate (p, uid) for_ update.assets \a -> addPrepQuery userAssetsUpdate (a, uid) for_ update.locale \a -> addPrepQuery userLocaleUpdate (a.lLanguage, a.lCountry, uid) @@ -122,19 +179,45 @@ lookupLocaleImpl :: UserId -> Client (Maybe (Maybe Language, Maybe Country)) lookupLocaleImpl u = do retry x1 (query1 localeSelect (params LocalQuorum (Identity u))) +updateUserTeamImpl :: UserId -> TeamId -> Client () +updateUserTeamImpl u t = retry x5 $ write userTeamUpdate (params LocalQuorum (t, u)) + where + userTeamUpdate :: PrepQuery W (TeamId, UserId) () + userTeamUpdate = "UPDATE user SET team = ? WHERE id = ?" + +getActivityTimestampsImpl :: UserId -> Client [Maybe UTCTime] +getActivityTimestampsImpl uid = do + runIdentity <$$> retry x1 (query q (params LocalQuorum (Identity uid))) + where + q :: PrepQuery R (Identity UserId) (Identity (Maybe UTCTime)) + q = "SELECT last_active from clients where user = ?" + +getRichInfoImpl :: UserId -> Client (Maybe RichInfoAssocList) +getRichInfoImpl uid = + fmap runIdentity + <$> retry x1 (query1 q (params LocalQuorum (Identity uid))) + where + q :: PrepQuery R (Identity UserId) (Identity RichInfoAssocList) + q = "SELECT json FROM rich_info WHERE user = ?" + -------------------------------------------------------------------------------- -- Queries -selectUser :: PrepQuery R (Identity UserId) (TupleType StoredUser) -selectUser = - "SELECT id, name, picture, email, phone, sso_id, accent_id, assets, \ - \activated, status, expires, language, country, provider, service, \ - \handle, team, managed_by, supported_protocols \ - \FROM user where id = ?" +selectUsers :: PrepQuery R (Identity [UserId]) (TupleType StoredUser) +selectUsers = + [sql| + SELECT id, name, text_status, picture, email, email_unvalidated, sso_id, accent_id, assets, + activated, status, expires, language, country, provider, + service, handle, team, managed_by, supported_protocols + FROM user WHERE id IN ? + |] userDisplayNameUpdate :: PrepQuery W (Name, UserId) () userDisplayNameUpdate = "UPDATE user SET name = ? WHERE id = ?" +userTextStatusUpdate :: PrepQuery W (TextStatus, UserId) () +userTextStatusUpdate = "UPDATE user SET text_status = ? WHERE id = ?" + userPictUpdate :: PrepQuery W (Pict, UserId) () userPictUpdate = "UPDATE user SET picture = ? WHERE id = ?" @@ -166,7 +249,7 @@ updateUserToTombstone :: PrepQuery W (AccountStatus, Name, ColourId, Pict, [Asse updateUserToTombstone = "UPDATE user SET status = ?, name = ?,\ \ accent_id = ?, picture = ?, assets = ?, handle = null, country = null,\ - \ language = null, email = null, phone = null, sso_id = null WHERE id = ?" + \ language = null, email = null, sso_id = null WHERE id = ?" statusSelect :: PrepQuery R (Identity UserId) (Identity (Maybe AccountStatus)) statusSelect = "SELECT status FROM user WHERE id = ?" diff --git a/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs b/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs new file mode 100644 index 00000000000..b5a005036ce --- /dev/null +++ b/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs @@ -0,0 +1,208 @@ +{-# LANGUAGE RecordWildCards #-} + +module Wire.UserStore.IndexUser where + +import Cassandra.Util +import Data.ByteString.Builder +import Data.ByteString.Lazy qualified as LBS +import Data.Handle +import Data.Id +import Data.Json.Util +import Data.Text.Encoding qualified as Text +import Data.Text.Encoding.Error qualified as Text +import Data.Text.ICU.Translit +import Database.CQL.Protocol +import Imports +import SAML2.WebSSO qualified as SAML +import URI.ByteString +import Wire.API.User hiding (userId) +import Wire.API.User.Search +import Wire.UserSearch.Types + +type Activated = Bool + +data WithWritetime a = WithWriteTime {value :: a, writetime :: Writetime a} + deriving (Eq, Show) + +data IndexUser = IndexUser + { userId :: UserId, + teamId :: Maybe (WithWritetime TeamId), + name :: WithWritetime Name, + accountStatus :: Maybe (WithWritetime AccountStatus), + handle :: Maybe (WithWritetime Handle), + email :: Maybe (WithWritetime EmailAddress), + colourId :: WithWritetime ColourId, + activated :: WithWritetime Activated, + serviceId :: Maybe (WithWritetime ServiceId), + managedBy :: Maybe (WithWritetime ManagedBy), + ssoId :: Maybe (WithWritetime UserSSOId), + unverifiedEmail :: Maybe (WithWritetime EmailAddress), + writeTimeBumper :: Maybe (Writetime WriteTimeBumper) + } + deriving (Eq, Show) + +{- ORMOLU_DISABLE -} +type instance + TupleType IndexUser = + ( UserId, + Maybe TeamId, Maybe (Writetime TeamId), + Name, Writetime Name, + Maybe AccountStatus, Maybe (Writetime AccountStatus), + Maybe Handle, Maybe (Writetime Handle), + Maybe EmailAddress, Maybe (Writetime EmailAddress), + ColourId, Writetime ColourId, + Activated, Writetime Activated, + Maybe ServiceId, Maybe (Writetime ServiceId), + Maybe ManagedBy, Maybe (Writetime ManagedBy), + Maybe UserSSOId, Maybe (Writetime UserSSOId), + Maybe EmailAddress, Maybe (Writetime EmailAddress), + Maybe (Writetime WriteTimeBumper) + ) + +instance Record IndexUser where + asTuple (IndexUser {..}) = + ( userId, + value <$> teamId, writetime <$> teamId, + name.value, name.writetime, + value <$> accountStatus, writetime <$> accountStatus, + value <$> handle, writetime <$> handle, + value <$> email, writetime <$> email, + colourId.value, colourId.writetime, + activated.value, activated.writetime, + value <$> serviceId, writetime <$> serviceId, + value <$> managedBy, writetime <$> managedBy, + value <$> ssoId, writetime <$> ssoId, + value <$> unverifiedEmail, writetime <$> unverifiedEmail, + writeTimeBumper + ) + + asRecord + ( u, + mTeam, tTeam, + name, tName, + status, tStatus, + handle, tHandle, + email, tEmail, + colour, tColour, + activated, tActivated, + service, tService, + managedBy, tManagedBy, + ssoId, tSsoId, + emailUnvalidated, tEmailUnvalidated, + tWriteTimeBumper + ) = IndexUser { + userId = u, + teamId = WithWriteTime <$> mTeam <*> tTeam, + name = WithWriteTime name tName, + accountStatus = WithWriteTime <$> status <*> tStatus, + handle = WithWriteTime <$> handle <*> tHandle, + email = WithWriteTime <$> email <*> tEmail, + colourId = WithWriteTime colour tColour, + activated = WithWriteTime activated tActivated, + serviceId = WithWriteTime <$> service <*> tService, + managedBy = WithWriteTime <$> managedBy <*> tManagedBy, + ssoId = WithWriteTime <$> ssoId <*> tSsoId, + unverifiedEmail = WithWriteTime <$> emailUnvalidated <*> tEmailUnvalidated, + writeTimeBumper = tWriteTimeBumper + } +{- ORMOLU_ENABLE -} + +indexUserToVersion :: IndexUser -> IndexVersion +indexUserToVersion IndexUser {..} = + mkIndexVersion + [ const () <$$> Just name.writetime, + const () <$$> fmap writetime teamId, + const () <$$> fmap writetime accountStatus, + const () <$$> fmap writetime handle, + const () <$$> fmap writetime email, + const () <$$> Just colourId.writetime, + const () <$$> Just activated.writetime, + const () <$$> fmap writetime serviceId, + const () <$$> fmap writetime managedBy, + const () <$$> fmap writetime ssoId, + const () <$$> fmap writetime unverifiedEmail, + const () <$$> writeTimeBumper + ] + +indexUserToDoc :: SearchVisibilityInbound -> IndexUser -> UserDoc +indexUserToDoc searchVisInbound IndexUser {..} = + if shouldIndex + then + UserDoc + { udEmailUnvalidated = value <$> unverifiedEmail, + udSso = sso . value =<< ssoId, + udScimExternalId = join $ scimExternalId <$> (value <$> managedBy) <*> (value <$> ssoId), + udSearchVisibilityInbound = Just searchVisInbound, + -- FUTUREWORK: This is a bug: https://wearezeta.atlassian.net/browse/WPB-11124 + udRole = Nothing, + udCreatedAt = Just . toUTCTimeMillis $ writetimeToUTC activated.writetime, + udManagedBy = value <$> managedBy, + udSAMLIdP = idpUrl . value =<< ssoId, + udAccountStatus = value <$> accountStatus, + udColourId = Just colourId.value, + udEmail = value <$> email, + udHandle = value <$> handle, + udNormalized = Just $ normalized name.value.fromName, + udName = Just name.value, + udTeam = value <$> teamId, + udId = userId + } + else -- We insert a tombstone-style user here, as it's easier than + -- deleting the old one. It's mostly empty, but having the status here + -- might be useful in the future. + emptyUserDoc userId + where + shouldIndex = + ( case value <$> accountStatus of + Nothing -> True + Just Active -> True + Just Suspended -> True + Just Deleted -> False + Just Ephemeral -> False + Just PendingInvitation -> False + ) + && activated.value -- FUTUREWORK: how is this adding to the first case? + && isNothing serviceId + + idpUrl :: UserSSOId -> Maybe Text + idpUrl (UserSSOId (SAML.UserRef (SAML.Issuer uri) _subject)) = + Just $ fromUri uri + idpUrl (UserScimExternalId _) = Nothing + + fromUri :: URI -> Text + fromUri = + Text.decodeUtf8With Text.lenientDecode + . LBS.toStrict + . toLazyByteString + . serializeURIRef + + sso :: UserSSOId -> Maybe Sso + sso userSsoId = do + (issuer, nameid) <- ssoIssuerAndNameId userSsoId + pure $ Sso {ssoIssuer = issuer, ssoNameId = nameid} + +-- Transliteration could also be done by ElasticSearch (ICU plugin), but this would +-- require a data migration. +normalized :: Text -> Text +normalized = transliterate (trans "Any-Latin; Latin-ASCII; Lower") + +emptyUserDoc :: UserId -> UserDoc +emptyUserDoc uid = + UserDoc + { udEmailUnvalidated = Nothing, + udSso = Nothing, + udScimExternalId = Nothing, + udSearchVisibilityInbound = Nothing, + udRole = Nothing, + udCreatedAt = Nothing, + udManagedBy = Nothing, + udSAMLIdP = Nothing, + udAccountStatus = Nothing, + udColourId = Nothing, + udEmail = Nothing, + udHandle = Nothing, + udNormalized = Nothing, + udName = Nothing, + udTeam = Nothing, + udId = uid + } diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem.hs b/libs/wire-subsystems/src/Wire/UserSubsystem.hs index 16f53f23f1d..f53da756a00 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem.hs @@ -1,20 +1,40 @@ +{-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} -module Wire.UserSubsystem where +module Wire.UserSubsystem + ( module Wire.UserSubsystem, + module Data.HavePendingInvitations, + ) +where import Data.Default +import Data.Domain import Data.Handle (Handle) +import Data.HavePendingInvitations import Data.Id +import Data.Misc import Data.Qualified +import Data.Range import Imports import Polysemy +import Polysemy.Error import Wire.API.Federation.Error +import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti (TeamStatus) +import Wire.API.Team.Export (TeamExportUser) +import Wire.API.Team.Feature +import Wire.API.Team.Member (IsPerm (..), TeamMember) import Wire.API.User +import Wire.API.User.Search import Wire.Arbitrary -import Wire.UserKeyStore +import Wire.GalleyAPIAccess (GalleyAPIAccess) +import Wire.GalleyAPIAccess qualified as GalleyAPIAccess +import Wire.InvitationStore +import Wire.UserKeyStore (EmailKey, emailKeyOrig) +import Wire.UserSearch.Types +import Wire.UserSubsystem.Error (UserSubsystemError (..)) --- | Who is performing this update operation? (Single source of truth: users managed by SCIM --- can't be updated by clients and vice versa.) +-- | Who is performing this update operation / who is allowed to? (Single source of truth: +-- users managed by SCIM can't be updated by clients and vice versa.) data UpdateOriginType = -- | Call originates from the SCIM api in spar. UpdateOriginScim @@ -26,11 +46,12 @@ data UpdateOriginType -- | Simple updates (as opposed to, eg., handle, where we need to manage locks). -- -- This is isomorphic to 'StoredUserUpdate', but we keep the two types separate because they --- belong to different abstractions / levels (UserSubsystem vs. UserStore), and they may +-- belong to different abstraction levels (UserSubsystem vs. UserStore), and they may -- change independently in the future ('UserStoreUpdate' may grow more fields for other -- operations). data UserProfileUpdate = MkUserProfileUpdate { name :: Maybe Name, + textStatus :: Maybe TextStatus, pict :: Maybe Pict, -- DEPRECATED assets :: Maybe [Asset], accentId :: Maybe ColourId, @@ -44,6 +65,7 @@ instance Default UserProfileUpdate where def = MkUserProfileUpdate { name = Nothing, + textStatus = Nothing, pict = Nothing, -- DEPRECATED assets = Nothing, accentId = Nothing, @@ -51,27 +73,78 @@ instance Default UserProfileUpdate where supportedProtocols = Nothing } +-- | Parameters for `getExternalAccountsBy` operation below. +data GetBy = MkGetBy + { -- | whether or not to include pending invitations when getting users by ids. + includePendingInvitations :: HavePendingInvitations, + -- | get accounts by 'UserId'. + getByUserId :: [UserId], + -- | get accounts by their 'Handle' + getByHandle :: [Handle] + } + deriving stock (Eq, Ord, Show, Generic) + deriving (Arbitrary) via GenericUniform GetBy + +instance Default GetBy where + def = MkGetBy NoPendingInvitations [] [] + data UserSubsystem m a where -- | First arg is for authorization only. GetUserProfiles :: Local UserId -> [Qualified UserId] -> UserSubsystem m [UserProfile] + -- | These give us partial success and hide concurrency in the interpreter. + -- (Nit-pick: a better return type for this might be `([Qualified ([UserId], + -- FederationError)], [UserProfile])`, and then we'd probably need a function of type + -- `([Qualified ([UserId], FederationError)], [UserProfile]) -> ([(Qualified UserId, + -- 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] - -- | Self profile contains things not present in Profile. + -- | 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 + -- identity and accounts with status /= active included). + GetAccountsByEmailNoFilter :: Local [EmailAddress] -> UserSubsystem m [User] + -- | Get user account by local user id (accounts with missing identity and accounts with + -- status /= active included). + GetAccountNoFilter :: Local UserId -> UserSubsystem m (Maybe User) + -- | Get `SelfProfile` (it contains things not present in `UserProfile`). GetSelfProfile :: Local UserId -> UserSubsystem m (Maybe SelfProfile) - -- | These give us partial success and hide concurrency in the interpreter. - -- FUTUREWORK: it would be better to return errors as `Map Domain FederationError`, but would clients like that? - GetUserProfilesWithErrors :: Local UserId -> [Qualified UserId] -> UserSubsystem m ([(Qualified UserId, FederationError)], [UserProfile]) -- | Simple updates (as opposed to, eg., handle, where we need to manage locks). Empty fields are ignored (not deleted). UpdateUserProfile :: Local UserId -> Maybe ConnId -> UpdateOriginType -> UserProfileUpdate -> UserSubsystem m () - -- | parse and lookup a handle, return what the operation has found + -- | Parse and lookup a handle. CheckHandle :: Text {- use Handle here? -} -> UserSubsystem m CheckHandleResp - -- | checks a number of 'Handle's for availability and returns at most 'Word' amount of them + -- | Check a number of 'Handle's for availability and returns at most 'Word' amount of them CheckHandles :: [Handle] -> Word -> UserSubsystem m [Handle] - -- | parses a handle, this may fail so it's effectful + -- | Parse and update a handle. Parsing may fail so this is effectful. UpdateHandle :: Local UserId -> Maybe ConnId -> UpdateOriginType -> Text {- use Handle here? -} -> UserSubsystem m () - GetLocalUserAccountByUserKey :: Local EmailKey -> UserSubsystem m (Maybe UserAccount) - -- | returns the user's locale or the default locale if the users exists + -- | Return the user's locale (or the default locale if the users exists and has none). LookupLocaleWithDefault :: Local UserId -> UserSubsystem m (Maybe Locale) + -- | Check if an email is blocked. + IsBlocked :: EmailAddress -> UserSubsystem m Bool + -- | Remove an email from the block list. + BlockListDelete :: EmailAddress -> UserSubsystem m () + -- | Add an email to the block list. + BlockListInsert :: EmailAddress -> UserSubsystem m () + UpdateTeamSearchVisibilityInbound :: TeamStatus SearchVisibilityInboundConfig -> UserSubsystem m () + SearchUsers :: + Local UserId -> + Text -> + Maybe Domain -> + Maybe (Range 1 500 Int32) -> + UserSubsystem m (SearchResult Contact) + BrowseTeam :: + UserId -> + BrowseTeamFilters -> + Maybe (Range 1 500 Int) -> + Maybe PagingState -> + UserSubsystem m (SearchResult TeamContact) + -- | (... or does `AcceptTeamInvitation` belong into `TeamInvitationSubsystems`?) + AcceptTeamInvitation :: Local UserId -> PlainTextPassword6 -> InvitationCode -> UserSubsystem m () + -- | The following "internal" functions exists to support migration in this susbystem, after the + -- migration this would just be an internal detail of the subsystem + InternalUpdateSearchIndex :: UserId -> UserSubsystem m () + InternalFindTeamInvitation :: Maybe EmailKey -> InvitationCode -> UserSubsystem m StoredInvitation + GetUserExportData :: UserId -> UserSubsystem m (Maybe TeamExportUser) -- | the return type of 'CheckHandle' data CheckHandleResp @@ -89,5 +162,43 @@ getLocalUserProfile :: (Member UserSubsystem r) => Local UserId -> Sem r (Maybe getLocalUserProfile targetUser = listToMaybe <$> getLocalUserProfiles ((: []) <$> targetUser) -updateSupportedProtocols :: (Member UserSubsystem r) => Local UserId -> UpdateOriginType -> Set BaseProtocolTag -> Sem r () -updateSupportedProtocols uid mb prots = updateUserProfile uid Nothing mb (def {supportedProtocols = Just prots}) +getLocalAccountBy :: + (Member UserSubsystem r) => + HavePendingInvitations -> + Local UserId -> + Sem r (Maybe User) +getLocalAccountBy includePendingInvitations uid = + listToMaybe + <$> getAccountsBy + ( qualifyAs uid $ + def + { getByUserId = [tUnqualified uid], + includePendingInvitations + } + ) + +getLocalUserAccountByUserKey :: (Member UserSubsystem r) => Local EmailKey -> Sem r (Maybe User) +getLocalUserAccountByUserKey q@(tUnqualified -> ek) = + listToMaybe <$> getAccountsByEmailNoFilter (qualifyAs q [emailKeyOrig ek]) + +------------------------------------------ +-- FUTUREWORK: Pending functions for a team subsystem +------------------------------------------ + +ensurePermissions :: + ( IsPerm perm, + Member GalleyAPIAccess r, + Member (Error UserSubsystemError) r + ) => + UserId -> + TeamId -> + [perm] -> + Sem r () +ensurePermissions u t perms = do + m <- GalleyAPIAccess.getTeamMember u t + unless (check m) $ + throw UserSubsystemInsufficientTeamPermissions + where + check :: Maybe TeamMember -> Bool + check (Just m) = all (hasPermission m) perms + check Nothing = False diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs index 40006412b47..90a2d39a888 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs @@ -1,6 +1,8 @@ module Wire.UserSubsystem.Error where import Imports +import Network.HTTP.Types (status404) +import Network.Wai.Utilities qualified as Wai import Wire.API.Error import Wire.API.Error.Brig qualified as E import Wire.Error @@ -16,6 +18,16 @@ data UserSubsystemError | UserSubsystemHandleExists | UserSubsystemInvalidHandle | UserSubsystemProfileNotFound + | UserSubsystemInsufficientTeamPermissions + | UserSubsystemCannotJoinMultipleTeams + | UserSubsystemTooManyTeamMembers + | UserSubsystemMissingIdentity + | UserSubsystemInvalidActivationCodeWrongUser + | UserSubsystemInvalidActivationCodeWrongCode + | UserSubsystemInvalidInvitationCode + | UserSubsystemInvitationNotFound + | UserSubsystemUserNotAllowedToJoinTeam Wai.Error + | UserSubsystemMLSServicesNotAllowed deriving (Eq, Show) userSubsystemErrorToHttpError :: UserSubsystemError -> HttpError @@ -28,5 +40,15 @@ userSubsystemErrorToHttpError = UserSubsystemHandleExists -> errorToWai @E.HandleExists UserSubsystemInvalidHandle -> errorToWai @E.InvalidHandle UserSubsystemHandleManagedByScim -> errorToWai @E.HandleManagedByScim + UserSubsystemInsufficientTeamPermissions -> errorToWai @E.InsufficientTeamPermissions + UserSubsystemCannotJoinMultipleTeams -> errorToWai @E.CannotJoinMultipleTeams + UserSubsystemTooManyTeamMembers -> errorToWai @E.TooManyTeamMembers + UserSubsystemMissingIdentity -> errorToWai @E.MissingIdentity + UserSubsystemInvalidActivationCodeWrongUser -> errorToWai @E.InvalidActivationCodeWrongUser + UserSubsystemInvalidActivationCodeWrongCode -> errorToWai @E.InvalidActivationCodeWrongCode + UserSubsystemInvalidInvitationCode -> errorToWai @E.InvalidInvitationCode + UserSubsystemInvitationNotFound -> Wai.mkError status404 "not-found" "Something went wrong, while looking up the invitation" + UserSubsystemUserNotAllowedToJoinTeam e -> e + UserSubsystemMLSServicesNotAllowed -> errorToWai @E.MLSServicesNotAllowed instance Exception UserSubsystemError diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index 945f128e700..8f9ba2566e1 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -1,4 +1,5 @@ {-# LANGUAGE RecordWildCards #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} module Wire.UserSubsystem.Interpreter ( runUserSubsystem, @@ -6,98 +7,214 @@ module Wire.UserSubsystem.Interpreter ) where -import Control.Lens (view) +import Control.Error.Util (hush) +import Control.Lens (view, (^.)) import Control.Monad.Trans.Maybe -import Data.Either.Extra +import Data.CaseInsensitive qualified as CI +import Data.Domain import Data.Handle (Handle) import Data.Handle qualified as Handle import Data.Id import Data.Json.Util import Data.LegalHold +import Data.List.Extra (nubOrd) +import Data.Misc (HttpsUrl, PlainTextPassword6, mkHttpsUrl) import Data.Qualified +import Data.Range import Data.Time.Clock -import Imports hiding (local) +import Database.Bloodhound qualified as ES +import Imports import Polysemy -import Polysemy.Error hiding (try) +import Polysemy.Error import Polysemy.Input +import Polysemy.TinyLog (TinyLog) +import Polysemy.TinyLog qualified as Log +import SAML2.WebSSO qualified as SAML import Servant.Client.Core +import System.Logger.Message qualified as Log import Wire.API.Federation.API +import Wire.API.Federation.API.Brig qualified as FedBrig import Wire.API.Federation.Error +import Wire.API.Routes.FederationDomainConfig +import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti (TeamStatus (..)) +import Wire.API.Team.Export import Wire.API.Team.Feature -import Wire.API.Team.Member hiding (userId) -import Wire.API.User +import Wire.API.Team.Member +import Wire.API.Team.Permission qualified as Permission +import Wire.API.Team.Role (defaultRole) +import Wire.API.Team.SearchVisibility +import Wire.API.Team.Size (TeamSize (TeamSize)) +import Wire.API.User as User +import Wire.API.User.RichInfo +import Wire.API.User.Search import Wire.API.UserEvent import Wire.Arbitrary +import Wire.AuthenticationSubsystem +import Wire.BlockListStore as BlockList import Wire.DeleteQueue +import Wire.Events import Wire.FederationAPIAccess +import Wire.FederationConfigStore import Wire.GalleyAPIAccess +import Wire.GalleyAPIAccess qualified as GalleyAPIAccess +import Wire.IndexedUserStore (IndexedUserStore) +import Wire.IndexedUserStore qualified as IndexedUserStore +import Wire.IndexedUserStore.Bulk.ElasticSearch (teamSearchVisibilityInbound) +import Wire.InvitationStore import Wire.Sem.Concurrency +import Wire.Sem.Metrics +import Wire.Sem.Metrics qualified as Metrics import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now import Wire.StoredUser -import Wire.UserEvents import Wire.UserKeyStore +import Wire.UserSearch.Metrics +import Wire.UserSearch.Types import Wire.UserStore as UserStore +import Wire.UserStore.IndexUser import Wire.UserSubsystem import Wire.UserSubsystem.Error import Wire.UserSubsystem.HandleBlacklist +import Witherable (wither) data UserSubsystemConfig = UserSubsystemConfig { emailVisibilityConfig :: EmailVisibilityConfig, - defaultLocale :: Locale + defaultLocale :: Locale, + searchSameTeamOnly :: Bool, + maxTeamSize :: Word32 } - deriving (Show) - -instance Arbitrary UserSubsystemConfig where - arbitrary = UserSubsystemConfig <$> arbitrary <*> arbitrary + deriving (Show, Generic) + deriving (Arbitrary) via (GenericUniform UserSubsystemConfig) runUserSubsystem :: - ( Member GalleyAPIAccess r, - Member UserStore r, + ( Member UserStore r, Member UserKeyStore r, - Member (Concurrency 'Unsafe) r, -- FUTUREWORK: subsystems should implement concurrency inside interpreters, not depend on this dangerous effect. + Member GalleyAPIAccess r, + Member BlockListStore r, + Member (Concurrency 'Unsafe) r, Member (Error FederationError) r, Member (Error UserSubsystemError) r, Member (FederationAPIAccess fedM) r, Member DeleteQueue r, - Member UserEvents r, + Member Events r, Member Now r, RunClient (fedM 'Brig), FederationMonad fedM, - Typeable fedM + Typeable fedM, + Member IndexedUserStore r, + Member FederationConfigStore r, + Member Metrics r, + Member InvitationStore r, + Member TinyLog r, + Member (Input UserSubsystemConfig) r ) => - UserSubsystemConfig -> - InterpreterFor UserSubsystem r -runUserSubsystem cfg = runInputConst cfg . interpretUserSubsystem . raiseUnder + InterpreterFor AuthenticationSubsystem 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 + GetAccountNoFilter luid -> + getAccountNoFilterImpl luid + 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 + IsBlocked email -> + isBlockedImpl email + BlockListDelete email -> + blockListDeleteImpl email + BlockListInsert email -> + blockListInsertImpl email + UpdateTeamSearchVisibilityInbound status -> + updateTeamSearchVisibilityInboundImpl status + SearchUsers luid query mDomain mMaxResults -> + searchUsersImpl luid query mDomain mMaxResults + BrowseTeam uid browseTeamFilters mMaxResults mPagingState -> + browseTeamImpl uid browseTeamFilters mMaxResults mPagingState + InternalUpdateSearchIndex uid -> + syncUserIndex uid + AcceptTeamInvitation luid pwd code -> + authInterpreter $ + acceptTeamInvitationImpl luid pwd code + InternalFindTeamInvitation mEmailKey code -> + internalFindTeamInvitationImpl mEmailKey code + GetUserExportData uid -> getUserExportDataImpl uid -interpretUserSubsystem :: - ( Member GalleyAPIAccess r, - Member UserStore r, - Member UserKeyStore r, - Member (Concurrency 'Unsafe) r, - Member (Error FederationError) r, +scimExtId :: StoredUser -> Maybe Text +scimExtId su = do + m <- su.managedBy + i <- su.identity + sso <- ssoIdentity i + scimExternalId m sso + +userToIdPIssuer :: StoredUser -> Maybe HttpsUrl +userToIdPIssuer su = case su.identity >>= ssoIdentity of + Just (UserSSOId (SAML.UserRef issuer _)) -> + either (const Nothing) Just . mkHttpsUrl $ issuer ^. SAML.fromIssuer + Just _ -> Nothing + Nothing -> Nothing + +samlNamedId :: StoredUser -> Maybe Text +samlNamedId su = + su.identity >>= ssoIdentity >>= \case + (UserSSOId (SAML.UserRef _idp nameId)) -> Just . CI.original . SAML.unsafeShowNameID $ nameId + (UserScimExternalId _) -> Nothing + +internalFindTeamInvitationImpl :: + ( Member InvitationStore r, Member (Error UserSubsystemError) r, - Member (FederationAPIAccess fedM) r, Member (Input UserSubsystemConfig) r, - Member DeleteQueue r, - Member UserEvents r, - Member Now r, - RunClient (fedM 'Brig), - FederationMonad fedM, - Typeable fedM + Member (GalleyAPIAccess) r, + Member IndexedUserStore r ) => - InterpreterFor UserSubsystem r -interpretUserSubsystem = interpret \case - GetUserProfiles self others -> getUserProfilesImpl self others - GetLocalUserProfiles others -> getLocalUserProfilesImpl others - 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 - GetLocalUserAccountByUserKey userKey -> getLocalUserAccountByUserKeyImpl userKey - LookupLocaleWithDefault luid -> lookupLocaleOrDefaultImpl luid + Maybe EmailKey -> + InvitationCode -> + Sem r StoredInvitation +internalFindTeamInvitationImpl Nothing _ = throw UserSubsystemMissingIdentity +internalFindTeamInvitationImpl (Just e) c = + lookupInvitationByCode c >>= \case + Just inv -> do + if e == mkEmailKey (inv.email) + then ensureMemberCanJoin inv.teamId $> inv + else throw UserSubsystemInvalidInvitationCode + Nothing -> throw UserSubsystemInvalidInvitationCode + where + ensureMemberCanJoin tid = do + maxSize <- maxTeamSize <$> input + (TeamSize teamSize) <- IndexedUserStore.getTeamSize tid + when (teamSize >= fromIntegral maxSize) $ + throw UserSubsystemTooManyTeamMembers + -- FUTUREWORK: The above can easily be done/tested in the intra call. + -- Remove after the next release. + mAddUserError <- checkUserCanJoinTeam tid + maybe (pure ()) (throw . UserSubsystemUserNotAllowedToJoinTeam) mAddUserError + +isBlockedImpl :: (Member BlockListStore r) => EmailAddress -> Sem r Bool +isBlockedImpl = BlockList.exists . mkEmailKey + +blockListDeleteImpl :: (Member BlockListStore r) => EmailAddress -> Sem r () +blockListDeleteImpl = BlockList.delete . mkEmailKey + +blockListInsertImpl :: (Member BlockListStore r) => EmailAddress -> Sem r () +blockListInsertImpl = BlockList.insert . mkEmailKey lookupLocaleOrDefaultImpl :: (Member UserStore r, Member (Input UserSubsystemConfig) r) => Local UserId -> Sem r (Maybe Locale) lookupLocaleOrDefaultImpl luid = do @@ -138,7 +255,8 @@ getLocalUserProfilesImpl :: Member (Input UserSubsystemConfig) r, Member DeleteQueue r, Member Now r, - Member GalleyAPIAccess r + Member GalleyAPIAccess r, + Member (Concurrency Unsafe) r ) => Local [UserId] -> Sem r [UserProfile] @@ -154,7 +272,8 @@ getUserProfilesFromDomain :: Member UserStore r, RunClient (fedM 'Brig), FederationMonad fedM, - Typeable fedM + Typeable fedM, + Member (Concurrency Unsafe) r ) => Local UserId -> Qualified [UserId] -> @@ -183,7 +302,8 @@ getUserProfilesLocalPart :: Member (Input UserSubsystemConfig) r, Member DeleteQueue r, Member Now r, - Member GalleyAPIAccess r + Member GalleyAPIAccess r, + Member (Concurrency Unsafe) r ) => Maybe (Local UserId) -> Local [UserId] -> @@ -199,7 +319,7 @@ getUserProfilesLocalPart requestingUser luids = do <$> traverse getRequestingUserInfo requestingUser -- FUTUREWORK: (in the interpreters where it makes sense) pull paginated lists from the DB, -- not just single rows. - catMaybes <$> traverse (getLocalUserProfileImpl emailVisibilityConfigWithViewer) (sequence luids) + catMaybes <$> unsafePooledForConcurrentlyN 8 (sequence luids) (getLocalUserProfileImpl emailVisibilityConfigWithViewer) where getRequestingUserInfo :: Local UserId -> Sem r (Maybe (TeamId, TeamMember)) getRequestingUserInfo self = do @@ -313,10 +433,10 @@ getUserProfilesWithErrorsImpl self others = do (outp -> inp -> outp) aggregate acc [] = acc aggregate (accL, accR) (Right prof : buckets) = aggregate (accL, prof <> accR) buckets - aggregate (accL, accR) (Left err : buckets) = aggregate (renderBucketError err <> accL, accR) buckets + aggregate (accL, accR) (Left e : buckets) = aggregate (renderBucketError e <> accL, accR) buckets renderBucketError :: (FederationError, Qualified [UserId]) -> [(Qualified UserId, FederationError)] - renderBucketError (err, qlist) = (,err) . (flip Qualified (qDomain qlist)) <$> qUnqualified qlist + renderBucketError (e, qlist) = (,e) . (flip Qualified (qDomain qlist)) <$> qUnqualified qlist -- | 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`. @@ -357,8 +477,10 @@ guardLockedHandleField user updateOrigin handle = do updateUserProfileImpl :: ( Member UserStore r, Member (Error UserSubsystemError) r, - Member UserEvents r, - Member GalleyAPIAccess r + Member Events r, + Member GalleyAPIAccess r, + Member IndexedUserStore r, + Member Metrics r ) => Local UserId -> Maybe ConnId -> @@ -370,12 +492,15 @@ updateUserProfileImpl (tUnqualified -> uid) mconn updateOrigin update = do guardLockedFields user updateOrigin update mapError (\StoredUserUpdateHandleExists -> UserSubsystemHandleExists) $ updateUser uid (storedUserUpdate update) + let interestingToUpdateIndex = isJust update.name || isJust update.accentId + when interestingToUpdateIndex $ syncUserIndex uid generateUserEvent uid mconn (mkProfileUpdateEvent uid update) storedUserUpdate :: UserProfileUpdate -> StoredUserUpdate storedUserUpdate update = MkStoredUserUpdate { name = update.name, + textStatus = update.textStatus, pict = update.pict, assets = update.assets, accentId = update.accentId, @@ -388,6 +513,7 @@ mkProfileUpdateEvent uid update = UserUpdated $ (emptyUserUpdatedData uid) { eupName = update.name, + eupTextStatus = update.textStatus, eupPict = update.pict, eupAccentId = update.accentId, eupAssets = update.assets, @@ -399,27 +525,16 @@ mkProfileUpdateHandleEvent :: UserId -> Handle -> UserEvent mkProfileUpdateHandleEvent uid handle = UserUpdated $ (emptyUserUpdatedData uid) {eupHandle = Just handle} -getLocalUserAccountByUserKeyImpl :: - ( Member UserStore r, - Member UserKeyStore r, - Member (Input UserSubsystemConfig) r - ) => - Local EmailKey -> - Sem r (Maybe UserAccount) -getLocalUserAccountByUserKeyImpl target = runMaybeT $ do - config <- lift input - uid <- MaybeT $ lookupKey (tUnqualified target) - user <- MaybeT $ getUser uid - pure $ mkAccountFromStored (tDomain target) config.defaultLocale user - -------------------------------------------------------------------------------- -- Update Handle updateHandleImpl :: ( Member (Error UserSubsystemError) r, Member GalleyAPIAccess r, - Member UserEvents r, - Member UserStore r + Member Events r, + Member UserStore r, + Member IndexedUserStore r, + Member Metrics r ) => Local UserId -> Maybe ConnId -> @@ -436,6 +551,7 @@ updateHandleImpl (tUnqualified -> uid) mconn updateOrigin uhandle = do throw UserSubsystemNoIdentity mapError (\StoredUserUpdateHandleExists -> UserSubsystemHandleExists) $ UserStore.updateUserHandle uid (MkStoredUserHandleUpdate user.handle newHandle) + syncUserIndex uid generateUserEvent uid mconn (mkProfileUpdateHandleEvent uid newHandle) checkHandleImpl :: (Member (Error UserSubsystemError) r, Member UserStore r) => Text -> Sem r CheckHandleResp @@ -452,8 +568,9 @@ checkHandleImpl uhandle = do hasE2EId :: (Member GalleyAPIAccess r) => StoredUser -> Sem r Bool hasE2EId user = - wsStatus . afcMlsE2EId - <$> getAllFeatureConfigsForUser (Just user.id) <&> \case + -- FUTUREWORK(mangoiv): we should use a function 'getSingleFeatureForUser' + (.status) . npProject @MlsE2EIdConfig + <$> getAllTeamFeaturesForUser (Just user.id) <&> \case FeatureStatusEnabled -> True FeatureStatusDisabled -> False @@ -475,3 +592,385 @@ checkHandlesImpl check num = reverse <$> collectFree [] check num case owner of Nothing -> collectFree (h : free) hs (n - 1) Just _ -> collectFree free hs n + +------------------------------------------------------------------------------- +-- Search + +syncUserIndex :: + forall r. + ( Member UserStore r, + Member GalleyAPIAccess r, + Member IndexedUserStore r, + Member Metrics r + ) => + UserId -> + Sem r () +syncUserIndex uid = do + getIndexUser uid + >>= maybe deleteFromIndex upsert + where + deleteFromIndex :: Sem r () + deleteFromIndex = do + Metrics.incCounter indexDeleteCounter + IndexedUserStore.upsert (userIdToDocId uid) (emptyUserDoc uid) ES.NoVersionControl + + upsert :: IndexUser -> Sem r () + upsert indexUser = do + vis <- + maybe + (pure defaultSearchVisibilityInbound) + (teamSearchVisibilityInbound . value) + indexUser.teamId + let userDoc = indexUserToDoc vis indexUser + version = ES.ExternalGT . ES.ExternalDocVersion . docVersion $ indexUserToVersion indexUser + Metrics.incCounter indexUpdateCounter + IndexedUserStore.upsert (userIdToDocId uid) userDoc version + +updateTeamSearchVisibilityInboundImpl :: (Member IndexedUserStore r) => TeamStatus SearchVisibilityInboundConfig -> Sem r () +updateTeamSearchVisibilityInboundImpl teamStatus = + IndexedUserStore.updateTeamSearchVisibilityInbound teamStatus.team $ + searchVisibilityInboundFromFeatureStatus teamStatus.status + +searchUsersImpl :: + forall r fedM. + ( Member UserStore r, + Member GalleyAPIAccess r, + Member (Error UserSubsystemError) r, + Member IndexedUserStore r, + Member FederationConfigStore r, + RunClient (fedM 'Brig), + Member (FederationAPIAccess fedM) r, + FederationMonad fedM, + Typeable fedM, + Member TinyLog r, + Member (Error FederationError) r, + Member (Input UserSubsystemConfig) r + ) => + Local UserId -> + Text -> + Maybe Domain -> + Maybe (Range 1 500 Int32) -> + Sem r (SearchResult Contact) +searchUsersImpl searcherId searchTerm maybeDomain maybeMaxResults = do + let searcher = tUnqualified searcherId + mSearcherTeamId <- + UserStore.getUser searcher >>= \mTeam -> pure (mTeam >>= (.teamId)) + + for_ mSearcherTeamId $ \tid -> + ensurePermissions searcher tid [SearchContacts] + let qDomain = Qualified () (fromMaybe (tDomain searcherId) maybeDomain) + foldQualified + searcherId + (\_ -> searchLocally ((,mSearcherTeamId) <$> searcherId) searchTerm maybeMaxResults) + (\rdom -> searchRemotely rdom mSearcherTeamId searchTerm) + qDomain + +searchLocally :: + forall r. + ( Member GalleyAPIAccess r, + Member UserStore r, + Member IndexedUserStore r, + Member (Input UserSubsystemConfig) r + ) => + Local (UserId, Maybe TeamId) -> + Text -> + Maybe (Range 1 500 Int32) -> + Sem r (SearchResult Contact) +searchLocally searcher searchTerm maybeMaxResults = do + let maxResults = maybe 15 (fromIntegral . fromRange) maybeMaxResults + let (searcherId, searcherTeamId) = (fst <$> searcher, snd <$> searcher) + teamSearchInfo <- mkTeamSearchInfo (tUnqualified searcherTeamId) + + maybeExactHandleMatch <- exactHandleSearch teamSearchInfo + + let exactHandleMatchCount = length maybeExactHandleMatch + esMaxResults = maxResults - exactHandleMatchCount + + esResult <- + if esMaxResults > 0 + then + IndexedUserStore.searchUsers + (tUnqualified searcherId) + (tUnqualified searcherTeamId) + teamSearchInfo + searchTerm + esMaxResults + else pure $ SearchResult 0 0 0 [] FullSearch Nothing Nothing + + -- Prepend results matching exact handle and results from ES. + pure $ + esResult + { searchResults = maybeToList maybeExactHandleMatch <> map userDocToContact (searchResults esResult), + searchFound = exactHandleMatchCount + searchFound esResult, + searchReturned = exactHandleMatchCount + searchReturned esResult + } + where + handleTeamVisibility :: TeamId -> TeamSearchVisibility -> TeamSearchInfo + handleTeamVisibility _ SearchVisibilityStandard = AllUsers + handleTeamVisibility t SearchVisibilityNoNameOutsideTeam = TeamOnly t + + userDocToContact :: UserDoc -> Contact + userDocToContact userDoc = + Contact + { contactQualifiedId = tUntagged $ qualifyAs searcher userDoc.udId, + contactName = maybe "" fromName userDoc.udName, + contactColorId = fromIntegral . fromColourId <$> userDoc.udColourId, + contactHandle = Handle.fromHandle <$> userDoc.udHandle, + contactTeam = userDoc.udTeam + } + + mkTeamSearchInfo :: Maybe TeamId -> Sem r TeamSearchInfo + mkTeamSearchInfo searcherTeamId = do + config <- input + case searcherTeamId of + Nothing -> pure NoTeam + Just t -> + -- This flag in brig overrules any flag on galley - it is system wide + if config.searchSameTeamOnly + then pure (TeamOnly t) + else do + -- For team users, we need to check the visibility flag + handleTeamVisibility t <$> GalleyAPIAccess.getTeamSearchVisibility t + + exactHandleSearch :: TeamSearchInfo -> Sem r (Maybe Contact) + exactHandleSearch _teamSerachInfo = runMaybeT $ do + handle <- MaybeT . pure $ Handle.parseHandle searchTerm + owner <- MaybeT $ UserStore.lookupHandle handle + storedUser <- MaybeT $ UserStore.getUser owner + config <- lift input + let contact = contactFromStoredUser (tDomain searcher) storedUser + isContactVisible = + (config.searchSameTeamOnly && (snd . tUnqualified $ searcher) == storedUser.teamId) + || (not config.searchSameTeamOnly) + if isContactVisible + then pure contact + else MaybeT $ pure Nothing + + contactFromStoredUser :: Domain -> StoredUser -> Contact + contactFromStoredUser domain storedUser = + Contact + { contactQualifiedId = Qualified storedUser.id domain, + contactName = fromName storedUser.name, + contactHandle = Handle.fromHandle <$> storedUser.handle, + contactColorId = Just . fromIntegral . fromColourId $ storedUser.accentId, + contactTeam = storedUser.teamId + } + +searchRemotely :: + ( Member FederationConfigStore r, + RunClient (fedM 'Brig), + Member (FederationAPIAccess fedM) r, + FederationMonad fedM, + Typeable fedM, + Member TinyLog r, + Member (Error FederationError) r + ) => + Remote x -> + Maybe TeamId -> + Text -> + Sem r (SearchResult Contact) +searchRemotely rDom mTid searchTerm = do + let domain = tDomain rDom + Log.info $ + Log.msg (Log.val "searchRemotely") + . Log.field "domain" (show domain) + . Log.field "searchTerm" searchTerm + mFedCnf <- getFederationConfig domain + let onlyInTeams = case restriction <$> mFedCnf of + Just FederationRestrictionAllowAll -> Nothing + Just (FederationRestrictionByTeam teams) -> Just teams + -- if we are not federating at all, we also do not allow to search any remote teams + Nothing -> Just [] + + searchResponse <- + runFederated rDom $ + fedClient @'Brig @"search-users" (FedBrig.SearchRequest searchTerm mTid onlyInTeams) + let contacts = searchResponse.contacts + let count = length contacts + pure + SearchResult + { searchResults = contacts, + searchFound = count, + searchReturned = count, + searchTook = 0, + searchPolicy = searchResponse.searchPolicy, + searchPagingState = Nothing, + searchHasMore = Nothing + } + +browseTeamImpl :: + ( Member GalleyAPIAccess r, + Member (Error UserSubsystemError) r, + Member IndexedUserStore r + ) => + UserId -> + BrowseTeamFilters -> + Maybe (Range 1 500 Int) -> + Maybe PagingState -> + Sem r (SearchResult TeamContact) +browseTeamImpl uid filters mMaxResults mPagingState = do + -- limit this to team admins to reduce risk of involuntary DOS attacks. (also, + -- this way we don't need to worry about revealing confidential user data to + -- other team members.) + ensurePermissions uid filters.teamId [Permission.AddTeamMember] + + let maxResults = maybe 15 fromRange mMaxResults + userDocToTeamContact <$$> IndexedUserStore.paginateTeamMembers filters maxResults mPagingState + +getAccountNoFilterImpl :: + forall r. + ( Member UserStore r, + Member (Input UserSubsystemConfig) r + ) => + Local UserId -> + Sem r (Maybe User) +getAccountNoFilterImpl (tSplit -> (domain, uid)) = do + cfg <- input + muser <- getUser uid + pure $ (mkUserFromStored domain cfg.defaultLocale) <$> muser + +getAccountsByEmailNoFilterImpl :: + forall r. + ( Member UserStore r, + Member UserKeyStore r, + Member (Input UserSubsystemConfig) r + ) => + Local [EmailAddress] -> + Sem r [User] +getAccountsByEmailNoFilterImpl (tSplit -> (domain, emails)) = do + config <- input + nubOrd <$> flip foldMap emails \ek -> do + mactiveUid <- lookupKey (mkEmailKey ek) + getUsers (nubOrd . catMaybes $ [mactiveUid]) + <&> map (mkUserFromStored domain config.defaultLocale) + +-------------------------------------------------------------------------------- +-- getting user accounts by different criteria + +getAccountsByImpl :: + forall r. + ( Member UserStore r, + Member DeleteQueue r, + Member (Input UserSubsystemConfig) r, + Member InvitationStore r + ) => + Local GetBy -> + Sem r [User] +getAccountsByImpl (tSplit -> (domain, MkGetBy {includePendingInvitations, getByHandle, getByUserId})) = do + storedToExtAcc <- do + config <- input + pure $ mkUserFromStored domain config.defaultLocale + + handleUserIds :: [UserId] <- + wither lookupHandle getByHandle + + accsByIds :: [User] <- + getUsers (nubOrd $ handleUserIds <> getByUserId) <&> map storedToExtAcc + + filterM want (nubOrd $ accsByIds) + where + -- not wanted: + -- . users without identity + -- . pending users without matching invitation (those are garbage-collected) + -- . TODO: deleted users? + want :: User -> Sem r Bool + want user = + case user.userIdentity of + Nothing -> pure False + Just ident -> case user.userStatus of + PendingInvitation -> + case includePendingInvitations of + WithPendingInvitations -> case emailIdentity ident of + -- TODO(fisx): emailIdentity does not return an unvalidated address in case a + -- validated one cannot be found. that's probably wrong? split up into + -- validEmailIdentity, anyEmailIdentity? + Just email -> do + hasInvitation <- isJust . listToMaybe <$> lookupInvitationsByEmail email + gcHack hasInvitation (User.userId user) + pure hasInvitation + Nothing -> error "getExtendedAccountsByImpl: should never happen, user invited via scim always has an email" + NoPendingInvitations -> pure False + Active -> pure True + Suspended -> pure True + Deleted -> pure True -- TODO(mangoiv): previous comment said "We explicitly filter out deleted users now." Why? + Ephemeral -> pure True + + -- user invited via scim expires together with its invitation. the UserSubsystem interface + -- semantics hides the fact that pending users have no TTL field. we chose to emulate this + -- in this convoluted way (by making the invitation expire and then checking if it's still + -- there when looking up pending users), because adding TTLs would have been a much bigger + -- change in the database schema (`enqueueUserDeletion` would need to happen purely based + -- on TTL values in cassandra, and there is too much application logic involved there). + -- + -- we could also delete these users here and run a background process that scans for + -- pending users without invitation. we chose not to because enqueuing the user deletion + -- here is very cheap, and avoids database traffic if the user is looked up again. if the + -- background job is reliably taking care of this, there is no strong reason to keep this + -- function. + -- + -- there are certainly other ways to improve this, but they probably involve a non-trivial + -- database schema re-design. + gcHack :: Bool -> UserId -> Sem r () + gcHack hasInvitation uid = unless hasInvitation (enqueueUserDeletion uid) + +acceptTeamInvitationImpl :: + ( Member (Input UserSubsystemConfig) r, + Member UserStore r, + Member GalleyAPIAccess r, + Member (Error UserSubsystemError) r, + Member InvitationStore r, + Member IndexedUserStore r, + Member Metrics r, + Member Events r, + Member AuthenticationSubsystem r + ) => + Local UserId -> + PlainTextPassword6 -> + InvitationCode -> + Sem r () +acceptTeamInvitationImpl luid pw code = do + mSelfProfile <- getSelfProfileImpl luid + let mEmailKey = mkEmailKey <$> (userEmail . selfUser =<< mSelfProfile) + mTid = mSelfProfile >>= userTeam . selfUser + verifyUserPasswordError luid pw + inv <- internalFindTeamInvitationImpl mEmailKey code + let tid = inv.teamId + let minvmeta = (,inv.createdAt) <$> inv.createdBy + uid = tUnqualified luid + for_ mTid $ \userTid -> + unless (tid == userTid) $ + throw UserSubsystemCannotJoinMultipleTeams + added <- GalleyAPIAccess.addTeamMember uid tid minvmeta (fromMaybe defaultRole inv.role) + unless added $ throw UserSubsystemTooManyTeamMembers + updateUserTeam uid tid + deleteInvitation inv.teamId inv.invitationId + syncUserIndex uid + generateUserEvent uid Nothing (teamUpdated uid tid) + +getUserExportDataImpl :: (Member UserStore r) => UserId -> Sem r (Maybe TeamExportUser) +getUserExportDataImpl uid = fmap hush . runError @() $ do + su <- UserStore.getUser uid >>= note () + mRichInfo <- UserStore.getRichInfo uid + timestamps <- UserStore.getActivityTimestamps uid + -- Make sure the list of timestamps is non-empty so that 'maximum' is + -- well-defined and returns 'Nothing' when no valid timestamps are present. + let lastActive = maximum (Nothing : timestamps) + let numClients = length timestamps + pure $ + TeamExportUser + { tExportDisplayName = su.name, + tExportHandle = su.handle, + tExportEmail = su.email, + tExportRole = Nothing, + tExportCreatedOn = Nothing, + tExportInvitedBy = Nothing, + tExportIdpIssuer = userToIdPIssuer su, + tExportManagedBy = fromMaybe ManagedByWire su.managedBy, + tExportSAMLNamedId = fromMaybe "" (samlNamedId su), + tExportSCIMExternalId = fromMaybe "" (scimExtId su), + tExportSCIMRichInfo = fmap RichInfo mRichInfo, + tExportUserId = uid, + tExportNumDevices = numClients, + tExportLastActive = lastActive, + tExportStatus = su.status + } diff --git a/libs/wire-subsystems/src/Wire/VerificationCode.hs b/libs/wire-subsystems/src/Wire/VerificationCode.hs index 1caea31049d..4dd32c5e799 100644 --- a/libs/wire-subsystems/src/Wire/VerificationCode.hs +++ b/libs/wire-subsystems/src/Wire/VerificationCode.hs @@ -59,7 +59,7 @@ data Code = Code -- once, and it cannot actually be "re"-tried after that. codeRetries :: !Retries, codeTTL :: !Timeout, - codeFor :: !Email, + codeFor :: !EmailAddress, codeAccount :: !(Maybe UUID) } deriving (Eq, Show) diff --git a/libs/wire-subsystems/src/Wire/VerificationCodeGen.hs b/libs/wire-subsystems/src/Wire/VerificationCodeGen.hs index 7290a0fbae4..8f9bef9985f 100644 --- a/libs/wire-subsystems/src/Wire/VerificationCodeGen.hs +++ b/libs/wire-subsystems/src/Wire/VerificationCodeGen.hs @@ -39,7 +39,7 @@ data RandomValueType -- different contexts for the same email address. -- TODO: newtype KeyContext = KeyContext ByteString data VerificationCodeGen = VerificationCodeGen - { genFor :: !Email, + { genFor :: !EmailAddress, genKey :: !Key, -- Note [Unique keys] genValueType :: !RandomValueType } @@ -49,17 +49,17 @@ data VerificationCodeGen = VerificationCodeGen -- | Initialise a 'Code' 'VerificationCodeGen'erator for a given natural key. -- This generates a link for emails and a 6-digit code for phone. See also: -- `mk6DigitVerificationCodeGen`. -mkVerificationCodeGen :: Email -> VerificationCodeGen +mkVerificationCodeGen :: EmailAddress -> VerificationCodeGen mkVerificationCodeGen email = VerificationCodeGen email (mkKey email) Random15Bytes -- | Initialise a 'Code' 'VerificationCodeGen'erator for a given natural key. -- This generates a 6-digit code, matter whether it is sent to a phone or to an -- email address. See also: `mkVerificationCodeGen`. -mk6DigitVerificationCodeGen :: Email -> VerificationCodeGen +mk6DigitVerificationCodeGen :: EmailAddress -> VerificationCodeGen mk6DigitVerificationCodeGen email = VerificationCodeGen email (mkKey email) Random6DigitNumber -mkKey :: Email -> Key +mkKey :: EmailAddress -> Key mkKey email = Key . unsafeRange diff --git a/libs/wire-subsystems/src/Wire/VerificationCodeStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/VerificationCodeStore/Cassandra.hs index e2e013ec62d..72671edec74 100644 --- a/libs/wire-subsystems/src/Wire/VerificationCodeStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/VerificationCodeStore/Cassandra.hs @@ -31,7 +31,7 @@ insertCodeImpl c = do let t = round (codeTTL c) retry x5 (write cql (params LocalQuorum (k, s, v, r, e, a, t))) where - cql :: PrepQuery W (Key, Scope, Value, Retries, Email, Maybe UUID, Int32) () + cql :: PrepQuery W (Key, Scope, Value, Retries, EmailAddress, Maybe UUID, Int32) () cql = "INSERT INTO vcodes (key, scope, value, retries, email, account) \ \VALUES (?, ?, ?, ?, ?, ?) USING TTL ?" @@ -40,12 +40,12 @@ insertCodeImpl c = do lookupCodeImpl :: (MonadClient m) => Key -> Scope -> m (Maybe Code) lookupCodeImpl k s = toCode <$$> retry x1 (query1 cql (params LocalQuorum (k, s))) where - cql :: PrepQuery R (Key, Scope) (Value, Int32, Retries, Email, Maybe UUID) + cql :: PrepQuery R (Key, Scope) (Value, Int32, Retries, EmailAddress, Maybe UUID) cql = "SELECT value, ttl(value), retries, email, account \ \FROM vcodes WHERE key = ? AND scope = ?" - toCode :: (Value, Int32, Retries, Email, Maybe UUID) -> Code + toCode :: (Value, Int32, Retries, EmailAddress, Maybe UUID) -> Code toCode (val, ttl, retries, email, account) = Code { codeKey = k, diff --git a/libs/wire-subsystems/src/Wire/VerificationCodeSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/VerificationCodeSubsystem/Interpreter.hs index 156be1cbd90..408be66fc38 100644 --- a/libs/wire-subsystems/src/Wire/VerificationCodeSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/VerificationCodeSubsystem/Interpreter.hs @@ -65,7 +65,12 @@ createCodeOverwritePreviousImpl gen scope retries timeout mId = do code <- generateVerificationCode gen scope retries timeout mId maybe (pure code) (throw . VerificationCodeThrottled) =<< insert code -insert :: (Member VerificationCodeStore r, Member (Input VerificationCodeThrottleTTL) r) => Code -> Sem r (Maybe RetryAfter) +insert :: + ( Member VerificationCodeStore r, + Member (Input VerificationCodeThrottleTTL) r + ) => + Code -> + Sem r (Maybe RetryAfter) insert code = do VerificationCodeThrottleTTL ttl <- input mRetryAfter <- lookupThrottle (codeKey code) (codeScope code) diff --git a/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs index 39dda77c340..87509b688de 100644 --- a/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs @@ -6,6 +6,7 @@ import Data.Domain import Data.Id import Data.Misc (PlainTextPassword8) import Data.Qualified +import Data.Text.Encoding (decodeUtf8) import Data.Time import Imports import Polysemy @@ -17,7 +18,7 @@ import Test.Hspec import Test.Hspec.QuickCheck import Test.QuickCheck import Wire.API.Allowlists (AllowlistEmailDomains (AllowlistEmailDomains)) -import Wire.API.Password +import Wire.API.Password as Password import Wire.API.User import Wire.API.User qualified as User import Wire.API.User.Auth @@ -32,11 +33,13 @@ import Wire.PasswordStore import Wire.Sem.Logger.TinyLog import Wire.Sem.Now (Now) import Wire.SessionStore +import Wire.StoredUser import Wire.UserKeyStore -import Wire.UserSubsystem +import Wire.UserStore type AllEffects = - [ Error AuthenticationSubsystemError, + [ AuthenticationSubsystem, + Error AuthenticationSubsystemError, HashPassword, Now, State UTCTime, @@ -45,26 +48,26 @@ type AllEffects = SessionStore, State (Map UserId [Cookie ()]), PasswordStore, - State (Map UserId Password), PasswordResetCodeStore, State (Map PasswordResetKey (PRQueryData Identity)), TinyLog, EmailSubsystem, - State (Map Email [SentMail]), - UserSubsystem + UserStore, + State [StoredUser], + State (Map EmailAddress [SentMail]) ] -interpretDependencies :: Domain -> [UserAccount] -> Map UserId Password -> Maybe [Text] -> Sem AllEffects a -> Either AuthenticationSubsystemError a -interpretDependencies localDomain preexistingUsers preexistingPasswords mAllowedEmailDomains = +runAllEffects :: Domain -> [User] -> Maybe [Text] -> Sem AllEffects a -> Either AuthenticationSubsystemError a +runAllEffects localDomain preexistingUsers mAllowedEmailDomains = run - . userSubsystemTestInterpreter preexistingUsers . evalState mempty + . evalState mempty + . inMemoryUserStoreInterpreter . emailSubsystemInterpreter . discardTinyLogs . evalState mempty . inMemoryPasswordResetCodeStore - . evalState preexistingPasswords - . inMemoryPasswordStoreInterpreter + . runInMemoryPasswordStoreInterpreter . evalState mempty . inMemorySessionStoreInterpreter . runInputConst (AllowlistEmailDomains <$> mAllowedEmailDomains) @@ -73,186 +76,213 @@ interpretDependencies localDomain preexistingUsers preexistingPasswords mAllowed . interpretNowAsState . staticHashPasswordInterpreter . runError + . interpretAuthenticationSubsystem (userSubsystemTestInterpreter preexistingUsers) spec :: Spec spec = describe "AuthenticationSubsystem.Interpreter" do describe "password reset" do prop "password reset should work with the email being used as password reset key" $ \email userNoEmail (cookiesWithTTL :: [(Cookie (), Maybe TTL)]) mPreviousPassword newPassword -> - let user = userNoEmail {userIdentity = Just $ EmailIdentity email} + let user = + userNoEmail + { userIdentity = Just $ EmailIdentity email, + userEmailUnvalidated = Nothing, + userStatus = Active + } uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (newPasswordHash, cookiesAfterReset) = - interpretDependencies localDomain [UserAccount user Active] mempty Nothing - . interpretAuthenticationSubsystem - $ do - forM_ mPreviousPassword (hashPassword >=> upsertHashedPassword uid) - mapM_ (uncurry (insertCookie uid)) cookiesWithTTL + runAllEffects localDomain [user] Nothing $ do + forM_ mPreviousPassword (hashPassword >=> upsertHashedPassword uid) + mapM_ (uncurry (insertCookie uid)) cookiesWithTTL - createPasswordResetCode (mkEmailKey email) - (_, code) <- expect1ResetPasswordEmail email - resetPassword (PasswordResetEmailIdentity email) code newPassword + createPasswordResetCode (mkEmailKey email) + (_, code) <- expect1ResetPasswordEmail email + resetPassword (PasswordResetEmailIdentity email) code newPassword - (,) <$> lookupHashedPassword uid <*> listCookies uid + (,) <$> lookupHashedPassword uid <*> listCookies uid in mPreviousPassword /= Just newPassword ==> - (fmap (verifyPassword newPassword) newPasswordHash === Just True) + (fmap (Password.verifyPassword newPassword) newPasswordHash === Just 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} + let user = + userNoEmail + { userIdentity = Just $ EmailIdentity email, + userEmailUnvalidated = Nothing, + userStatus = Active + } uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (newPasswordHash, cookiesAfterReset) = - interpretDependencies localDomain [UserAccount user Active] mempty Nothing - . interpretAuthenticationSubsystem - $ do - forM_ mPreviousPassword (hashPassword >=> upsertHashedPassword uid) - mapM_ (uncurry (insertCookie uid)) cookiesWithTTL + runAllEffects localDomain [user] Nothing $ do + forM_ mPreviousPassword (hashPassword >=> upsertHashedPassword uid) + mapM_ (uncurry (insertCookie uid)) cookiesWithTTL - createPasswordResetCode (mkEmailKey email) - (passwordResetKey, code) <- expect1ResetPasswordEmail email - resetPassword (PasswordResetIdentityKey passwordResetKey) code newPassword + createPasswordResetCode (mkEmailKey email) + (passwordResetKey, code) <- expect1ResetPasswordEmail email + resetPassword (PasswordResetIdentityKey passwordResetKey) code newPassword - (,) <$> lookupHashedPassword uid <*> listCookies uid + (,) <$> lookupHashedPassword uid <*> listCookies uid in mPreviousPassword /= Just newPassword ==> - (fmap (verifyPassword newPassword) newPasswordHash === Just True) + (fmap (Password.verifyPassword newPassword) newPasswordHash === Just True) .&&. (cookiesAfterReset === []) prop "reset code is not generated when email is not in allow list" $ \email localDomain -> let createPasswordResetCodeResult = - interpretDependencies localDomain [] mempty (Just ["example.com"]) - . interpretAuthenticationSubsystem - $ createPasswordResetCode (mkEmailKey email) + runAllEffects localDomain [] (Just ["example.com"]) $ + createPasswordResetCode (mkEmailKey email) <* expectNoEmailSent - in emailDomain email /= "example.com" ==> + in domainPart email /= "example.com" ==> createPasswordResetCodeResult === Right () prop "reset code is generated when email is in allow list" $ \email userNoEmail -> - let user = userNoEmail {userIdentity = Just $ EmailIdentity email} + let user = + userNoEmail + { userIdentity = Just $ EmailIdentity email, + userEmailUnvalidated = Nothing, + userStatus = Active + } localDomain = userNoEmail.userQualifiedId.qDomain createPasswordResetCodeResult = - interpretDependencies localDomain [UserAccount user Active] mempty (Just [emailDomain email]) - . interpretAuthenticationSubsystem - $ createPasswordResetCode (mkEmailKey email) + runAllEffects localDomain [user] (Just [decodeUtf8 $ domainPart email]) $ + createPasswordResetCode (mkEmailKey email) in counterexample ("expected Right, got: " <> show createPasswordResetCodeResult) $ isRight createPasswordResetCodeResult prop "reset code is not generated for when user's status is not Active" $ - \email userNoEmail status -> - let user = userNoEmail {userIdentity = Just $ EmailIdentity email} + \email userNoEmail -> + let user = + userNoEmail + { userIdentity = Just $ EmailIdentity email, + userEmailUnvalidated = Nothing + } localDomain = userNoEmail.userQualifiedId.qDomain createPasswordResetCodeResult = - interpretDependencies localDomain [UserAccount user status] mempty Nothing - . interpretAuthenticationSubsystem - $ createPasswordResetCode (mkEmailKey email) + runAllEffects localDomain [user] Nothing $ + createPasswordResetCode (mkEmailKey email) <* expectNoEmailSent - in status /= Active ==> + in userStatus user /= Active ==> createPasswordResetCodeResult === Right () prop "reset code is not generated for when there is no user for the email" $ \email localDomain -> let createPasswordResetCodeResult = - interpretDependencies localDomain [] mempty Nothing - . interpretAuthenticationSubsystem - $ createPasswordResetCode (mkEmailKey email) + runAllEffects localDomain [] Nothing $ + createPasswordResetCode (mkEmailKey email) <* expectNoEmailSent in createPasswordResetCodeResult === Right () prop "reset code is only generated once" $ \email userNoEmail newPassword -> - let user = userNoEmail {userIdentity = Just $ EmailIdentity email} + let user = + userNoEmail + { userIdentity = Just $ EmailIdentity email, + userEmailUnvalidated = Nothing, + userStatus = Active + } uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (newPasswordHash, mCaughtException) = - interpretDependencies localDomain [UserAccount user Active] mempty Nothing - . interpretAuthenticationSubsystem - $ do - createPasswordResetCode (mkEmailKey email) - (_, code) <- expect1ResetPasswordEmail email + runAllEffects localDomain [user] Nothing $ do + createPasswordResetCode (mkEmailKey email) + (_, code) <- expect1ResetPasswordEmail email - mCaughtExc <- catchExpectedError $ createPasswordResetCode (mkEmailKey email) + mCaughtExc <- catchExpectedError $ createPasswordResetCode (mkEmailKey email) - -- Reset password still works with previously generated reset code - resetPassword (PasswordResetEmailIdentity email) code newPassword + -- Reset password still works with previously generated reset code + resetPassword (PasswordResetEmailIdentity email) code newPassword - (,mCaughtExc) <$> lookupHashedPassword uid - in (fmap (verifyPassword newPassword) newPasswordHash === Just True) + (,mCaughtExc) <$> lookupHashedPassword uid + in (fmap (Password.verifyPassword newPassword) newPasswordHash === Just True) .&&. (mCaughtException === Nothing) prop "reset code is not accepted after expiry" $ \email userNoEmail oldPassword newPassword -> - let user = userNoEmail {userIdentity = Just $ EmailIdentity email} + let user = + userNoEmail + { userIdentity = Just $ EmailIdentity email, + userEmailUnvalidated = Nothing, + userStatus = Active + } uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (passwordInDB, resetPasswordResult) = - interpretDependencies localDomain [UserAccount user Active] mempty Nothing - . interpretAuthenticationSubsystem - $ do - upsertHashedPassword uid =<< hashPassword oldPassword - createPasswordResetCode (mkEmailKey email) - (_, code) <- expect1ResetPasswordEmail email + runAllEffects localDomain [user] Nothing $ do + upsertHashedPassword uid =<< hashPassword oldPassword + createPasswordResetCode (mkEmailKey email) + (_, code) <- expect1ResetPasswordEmail email - passTime (passwordResetCodeTtl + 1) + passTime (passwordResetCodeTtl + 1) - mCaughtExc <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity email) code newPassword - (,mCaughtExc) <$> lookupHashedPassword uid + mCaughtExc <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity email) code newPassword + (,mCaughtExc) <$> lookupHashedPassword uid in resetPasswordResult === Just AuthenticationSubsystemInvalidPasswordResetCode .&&. verifyPasswordProp oldPassword passwordInDB 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} + let user = + userNoEmail + { userIdentity = Just $ EmailIdentity email, + userEmailUnvalidated = Nothing, + userStatus = Active + } uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (passwordInDB, resetPasswordResult) = - interpretDependencies localDomain [UserAccount user Active] mempty Nothing - . interpretAuthenticationSubsystem - $ do - upsertHashedPassword uid =<< hashPassword oldPassword - mCaughtExc <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity email) resetCode newPassword - (,mCaughtExc) <$> lookupHashedPassword uid + runAllEffects localDomain [user] Nothing $ do + upsertHashedPassword uid =<< hashPassword oldPassword + mCaughtExc <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity email) resetCode newPassword + (,mCaughtExc) <$> lookupHashedPassword uid in resetPasswordResult === Just AuthenticationSubsystemInvalidPasswordResetCode .&&. verifyPasswordProp oldPassword passwordInDB prop "password reset doesn't work if email is wrong" $ \email wrongEmail userNoEmail resetCode oldPassword newPassword -> - let user = userNoEmail {userIdentity = Just $ EmailIdentity email} + let user = + userNoEmail + { userIdentity = Just $ EmailIdentity email, + userEmailUnvalidated = Nothing, + userStatus = Active + } uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (passwordInDB, resetPasswordResult) = - interpretDependencies localDomain [UserAccount user Active] mempty Nothing - . interpretAuthenticationSubsystem - $ do - hashAndUpsertPassword uid oldPassword - mCaughtExc <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity wrongEmail) resetCode newPassword - (,mCaughtExc) <$> lookupHashedPassword uid + runAllEffects localDomain [user] Nothing $ do + hashAndUpsertPassword uid oldPassword + mCaughtExc <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity wrongEmail) resetCode newPassword + (,mCaughtExc) <$> lookupHashedPassword uid in email /= wrongEmail ==> resetPasswordResult === Just AuthenticationSubsystemInvalidPasswordResetKey .&&. verifyPasswordProp oldPassword passwordInDB prop "only 3 wrong password reset attempts are allowed" $ \email userNoEmail arbitraryResetCode oldPassword newPassword (Upto4 wrongResetAttempts) -> - let user = userNoEmail {userIdentity = Just $ EmailIdentity email} + let user = + userNoEmail + { userIdentity = Just $ EmailIdentity email, + userEmailUnvalidated = Nothing, + userStatus = Active + } uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (passwordHashInDB, correctResetCode, wrongResetErrors, resetPassworedWithCorectCodeResult) = - interpretDependencies localDomain [UserAccount user Active] mempty Nothing - . interpretAuthenticationSubsystem - $ do - upsertHashedPassword uid =<< hashPassword oldPassword - createPasswordResetCode (mkEmailKey email) - (_, generatedResetCode) <- expect1ResetPasswordEmail email - - wrongResetErrs <- - replicateM wrongResetAttempts $ - catchExpectedError $ - resetPassword (PasswordResetEmailIdentity email) arbitraryResetCode newPassword - - mFinalResetErr <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity email) generatedResetCode newPassword - (,generatedResetCode,wrongResetErrs,mFinalResetErr) <$> lookupHashedPassword uid + runAllEffects localDomain [user] Nothing $ do + upsertHashedPassword uid =<< hashPassword oldPassword + createPasswordResetCode (mkEmailKey email) + (_, generatedResetCode) <- expect1ResetPasswordEmail email + + wrongResetErrs <- + replicateM wrongResetAttempts $ + catchExpectedError $ + resetPassword (PasswordResetEmailIdentity email) arbitraryResetCode newPassword + + mFinalResetErr <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity email) generatedResetCode newPassword + (,generatedResetCode,wrongResetErrs,mFinalResetErr) <$> lookupHashedPassword uid expectedFinalResetResult = if wrongResetAttempts >= 3 then Just AuthenticationSubsystemInvalidPasswordResetCode @@ -269,17 +299,20 @@ spec = describe "AuthenticationSubsystem.Interpreter" do describe "internalLookupPasswordResetCode" do prop "should find password reset code by email" $ \email userNoEmail newPassword -> - let user = userNoEmail {userIdentity = Just $ EmailIdentity email} + let user = + userNoEmail + { userIdentity = Just $ EmailIdentity email, + userEmailUnvalidated = Nothing, + userStatus = Active + } uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right passwordHashInDB = - interpretDependencies localDomain [UserAccount user Active] mempty Nothing - . interpretAuthenticationSubsystem - $ do - void $ createPasswordResetCode (mkEmailKey email) - mLookupRes <- internalLookupPasswordResetCode (mkEmailKey email) - for_ mLookupRes $ \(_, code) -> resetPassword (PasswordResetEmailIdentity email) code newPassword - lookupHashedPassword uid + runAllEffects localDomain [user] Nothing $ do + void $ createPasswordResetCode (mkEmailKey email) + mLookupRes <- internalLookupPasswordResetCode (mkEmailKey email) + for_ mLookupRes $ \(_, code) -> resetPassword (PasswordResetEmailIdentity email) code newPassword + lookupHashedPassword uid in verifyPasswordProp newPassword passwordHashInDB newtype Upto4 = Upto4 Int @@ -291,13 +324,13 @@ instance Arbitrary Upto4 where verifyPasswordProp :: PlainTextPassword8 -> Maybe Password -> Property verifyPasswordProp plainTextPassword passwordHash = counterexample ("Password doesn't match, plainText=" <> show plainTextPassword <> ", passwordHash=" <> show passwordHash) $ - fmap (verifyPassword plainTextPassword) passwordHash == Just True + fmap (Password.verifyPassword plainTextPassword) passwordHash == Just True -hashAndUpsertPassword :: (Member PasswordStore r, Member HashPassword r) => UserId -> PlainTextPassword8 -> Sem r () +hashAndUpsertPassword :: (Member PasswordStore r) => UserId -> PlainTextPassword8 -> Sem r () hashAndUpsertPassword uid password = upsertHashedPassword uid =<< hashPassword password -expect1ResetPasswordEmail :: (Member (State (Map Email [SentMail])) r) => Email -> Sem r PasswordResetPair +expect1ResetPasswordEmail :: (Member (State (Map EmailAddress [SentMail])) r) => EmailAddress -> Sem r PasswordResetPair expect1ResetPasswordEmail email = getEmailsSentTo email <&> \case @@ -305,7 +338,7 @@ expect1ResetPasswordEmail email = [SentMail _ (PasswordResetMail resetPair)] -> resetPair wrongEmails -> error $ "Wrong emails sent: " <> show wrongEmails -expectNoEmailSent :: (Member (State (Map Email [SentMail])) r) => Sem r () +expectNoEmailSent :: (Member (State (Map EmailAddress [SentMail])) r) => Sem r () expectNoEmailSent = do emails <- get if null emails diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index d1fea2a4012..ee951963bbc 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} + module Wire.MiniBackend ( -- * Mini backends MiniBackend (..), @@ -6,7 +8,6 @@ module Wire.MiniBackend interpretFederationStack, runFederationStack, interpretNoFederationStack, - runNoFederationStackState, interpretNoFederationStackState, runNoFederationStack, runAllErrorsUnsafe, @@ -18,6 +19,8 @@ module Wire.MiniBackend -- * Quickcheck helpers NotPendingStoredUser (..), + NotPendingEmptyIdentityStoredUser (..), + PendingNotEmptyIdentityStoredUser (..), PendingStoredUser (..), ) where @@ -33,6 +36,7 @@ import Data.Proxy import Data.Qualified import Data.Time import Data.Type.Equality +import GHC.Generics import Imports import Polysemy import Polysemy.Error @@ -44,32 +48,67 @@ import Servant.Client.Core import System.Logger qualified as Log import Test.QuickCheck import Type.Reflection +import Wire.API.Allowlists (AllowlistEmailDomains) import Wire.API.Federation.API import Wire.API.Federation.Component import Wire.API.Federation.Error import Wire.API.Team.Feature import Wire.API.Team.Member hiding (userId) import Wire.API.User as User hiding (DeleteUser) +import Wire.API.User.Activation (ActivationCode) import Wire.API.User.Password +import Wire.ActivationCodeStore +import Wire.AuthenticationSubsystem +import Wire.AuthenticationSubsystem.Interpreter +import Wire.BlockListStore import Wire.DeleteQueue import Wire.DeleteQueue.InMemory +import Wire.EmailSubsystem (EmailSubsystem) +import Wire.Events import Wire.FederationAPIAccess import Wire.FederationAPIAccess.Interpreter as FI +import Wire.FederationConfigStore import Wire.GalleyAPIAccess +import Wire.HashPassword (HashPassword) +import Wire.IndexedUserStore import Wire.InternalEvent hiding (DeleteUser) +import Wire.InvitationStore import Wire.MockInterpreters +import Wire.MockInterpreters.ActivationCodeStore (inMemoryActivationCodeStoreInterpreter) +import Wire.MockInterpreters.InvitationStore (inMemoryInvitationStoreInterpreter) import Wire.PasswordResetCodeStore +import Wire.PasswordStore import Wire.Sem.Concurrency import Wire.Sem.Concurrency.Sequential +import Wire.Sem.Metrics +import Wire.Sem.Metrics.IO (ignoreMetrics) import Wire.Sem.Now hiding (get) +import Wire.SessionStore (SessionStore) import Wire.StoredUser -import Wire.UserEvents import Wire.UserKeyStore import Wire.UserStore import Wire.UserSubsystem import Wire.UserSubsystem.Error import Wire.UserSubsystem.Interpreter +newtype PendingNotEmptyIdentityStoredUser = PendingNotEmptyIdentityStoredUser StoredUser + deriving (Show, Eq) + +instance Arbitrary PendingNotEmptyIdentityStoredUser where + arbitrary = do + user <- arbitrary `suchThat` \user -> isJust user.identity + pure $ PendingNotEmptyIdentityStoredUser (user {status = Just PendingInvitation}) + +newtype NotPendingEmptyIdentityStoredUser = NotPendingEmptyIdentityStoredUser StoredUser + deriving (Show, Eq) + +-- TODO: make sure this is a valid state +instance Arbitrary NotPendingEmptyIdentityStoredUser where + arbitrary = do + user <- arbitrary `suchThat` \user -> isNothing user.identity + notPendingStatus <- elements (Nothing : map Just [Active, Suspended, Ephemeral]) + pure $ NotPendingEmptyIdentityStoredUser (user {status = notPendingStatus}) + newtype PendingStoredUser = PendingStoredUser StoredUser deriving (Show, Eq) @@ -84,29 +123,47 @@ newtype NotPendingStoredUser = NotPendingStoredUser StoredUser instance Arbitrary NotPendingStoredUser where arbitrary = do user <- arbitrary `suchThat` \user -> isJust user.identity - notPendingStatus <- elements (Nothing : map Just [Active, Suspended, Deleted, Ephemeral]) + notPendingStatus <- elements (Nothing : map Just [Active, Suspended, Ephemeral]) pure $ NotPendingStoredUser (user {status = notPendingStatus}) type AllErrors = [ Error UserSubsystemError, - Error FederationError + Error FederationError, + Error AuthenticationSubsystemError ] -type MiniBackendEffects = - [ UserSubsystem, +type MiniBackendEffects = UserSubsystem ': MiniBackendLowerEffects + +type MiniBackendLowerEffects = + [ EmailSubsystem, GalleyAPIAccess, + InvitationStore, + PasswordStore, + State (Map (TeamId, InvitationId) StoredInvitation), + State (Map InvitationCode StoredInvitation), + ActivationCodeStore, + State (Map EmailKey (Maybe UserId, ActivationCode)), + BlockListStore, + State [EmailKey], UserStore, State [StoredUser], UserKeyStore, State (Map EmailKey UserId), + IndexedUserStore, + FederationConfigStore, + PasswordResetCodeStore, + SessionStore, + HashPassword, DeleteQueue, - UserEvents, + Events, State [InternalNotification], State MiniBackend, State [MiniEvent], Now, Input UserSubsystemConfig, Input (Local ()), + Input (Maybe AllowlistEmailDomains), + Metrics, FederationAPIAccess MiniFederationMonad, TinyLog, Concurrency 'Unsafe @@ -118,15 +175,24 @@ data MiniBackend = MkMiniBackend -- invariant: for each key, the user.id and the key are the same users :: [StoredUser], userKeys :: Map EmailKey UserId, - passwordResetCodes :: Map PasswordResetKey (PRQueryData Identity) + passwordResetCodes :: Map PasswordResetKey (PRQueryData Identity), + blockList :: [EmailKey], + activationCodes :: Map EmailKey (Maybe UserId, ActivationCode), + invitationInfos :: Map InvitationCode StoredInvitation, + invitations :: Map (TeamId, InvitationId) StoredInvitation } + deriving stock (Eq, Show, Generic) instance Default MiniBackend where def = MkMiniBackend { users = mempty, userKeys = mempty, - passwordResetCodes = mempty + passwordResetCodes = mempty, + blockList = mempty, + activationCodes = mempty, + invitationInfos = mempty, + invitations = mempty } -- | represents an entire federated, stateful world of backends @@ -298,21 +364,11 @@ runNoFederationStack localBackend teamMember cfg = -- want to do errors?) runAllErrorsUnsafe . interpretNoFederationStack localBackend teamMember def cfg -runNoFederationStackState :: - (HasCallStack) => - MiniBackend -> - Maybe TeamMember -> - UserSubsystemConfig -> - Sem (MiniBackendEffects `Append` AllErrors) a -> - (MiniBackend, a) -runNoFederationStackState localBackend teamMember cfg = - runAllErrorsUnsafe . interpretNoFederationStackState localBackend teamMember def cfg - interpretNoFederationStack :: (Members AllErrors r) => MiniBackend -> Maybe TeamMember -> - AllFeatureConfigs -> + AllTeamFeatures -> UserSubsystemConfig -> Sem (MiniBackendEffects `Append` r) a -> Sem r a @@ -323,39 +379,81 @@ interpretNoFederationStackState :: (Members AllErrors r) => MiniBackend -> Maybe TeamMember -> - AllFeatureConfigs -> + AllTeamFeatures -> UserSubsystemConfig -> Sem (MiniBackendEffects `Append` r) a -> Sem r (MiniBackend, a) interpretNoFederationStackState = interpretMaybeFederationStackState emptyFederationAPIAcesss interpretMaybeFederationStackState :: + forall r a. (Members AllErrors r) => InterpreterFor (FederationAPIAccess MiniFederationMonad) (Logger (Log.Msg -> Log.Msg) : Concurrency 'Unsafe : r) -> MiniBackend -> Maybe TeamMember -> - AllFeatureConfigs -> + AllTeamFeatures -> UserSubsystemConfig -> Sem (MiniBackendEffects `Append` r) a -> Sem r (MiniBackend, a) interpretMaybeFederationStackState maybeFederationAPIAccess localBackend teamMember galleyConfigs cfg = - sequentiallyPerformConcurrency - . noOpLogger - . maybeFederationAPIAccess - . runInputConst (toLocalUnsafe (Domain "localdomain") ()) - . runInputConst cfg - . interpretNowConst (UTCTime (ModifiedJulianDay 0) 0) - . evalState [] - . runState localBackend - . evalState [] - . miniEventInterpreter - . inMemoryDeleteQueueInterpreter - . liftUserKeyStoreState - . inMemoryUserKeyStoreInterpreter - . liftUserStoreState - . inMemoryUserStoreInterpreter - . miniGalleyAPIAccess teamMember galleyConfigs - . runUserSubsystem cfg + let authSubsystemInterpreter :: InterpreterFor AuthenticationSubsystem (MiniBackendLowerEffects `Append` r) + authSubsystemInterpreter = interpretAuthenticationSubsystem userSubsystemInterpreter + + userSubsystemInterpreter :: InterpreterFor UserSubsystem (MiniBackendLowerEffects `Append` r) + userSubsystemInterpreter = runUserSubsystem authSubsystemInterpreter + in sequentiallyPerformConcurrency + . noOpLogger + . maybeFederationAPIAccess + . ignoreMetrics + . runInputConst Nothing + . runInputConst (toLocalUnsafe (Domain "localdomain") ()) + . runInputConst cfg + . interpretNowConst (UTCTime (ModifiedJulianDay 0) 0) + . evalState [] + . runState localBackend + . evalState [] + . miniEventInterpreter + . inMemoryDeleteQueueInterpreter + . staticHashPasswordInterpreter + . runInMemorySessionStore + . runInMemoryPasswordResetCodeStore + . runFederationConfigStoreInMemory + . inMemoryIndexedUserStoreInterpreter + . liftUserKeyStoreState + . inMemoryUserKeyStoreInterpreter + . liftUserStoreState + . inMemoryUserStoreInterpreter + . liftBlockListStoreState + . inMemoryBlockListStoreInterpreter + . liftActivationCodeStoreState + . inMemoryActivationCodeStoreInterpreter + . liftInvitationInfoStoreState + . liftInvitationStoreState + . runInMemoryPasswordStoreInterpreter + . inMemoryInvitationStoreInterpreter + . miniGalleyAPIAccess teamMember galleyConfigs + . noopEmailSubsystemInterpreter + . userSubsystemInterpreter + +liftInvitationInfoStoreState :: (Member (State MiniBackend) r) => Sem (State (Map InvitationCode StoredInvitation) : r) a -> Sem r a +liftInvitationInfoStoreState = interpret \case + Polysemy.State.Get -> gets (.invitationInfos) + Put newAcs -> modify $ \b -> b {invitationInfos = newAcs} + +liftInvitationStoreState :: (Member (State MiniBackend) r) => Sem (State (Map (TeamId, InvitationId) StoredInvitation) : r) a -> Sem r a +liftInvitationStoreState = interpret \case + Polysemy.State.Get -> gets (.invitations) + Put newInvs -> modify $ \b -> b {invitations = newInvs} + +liftActivationCodeStoreState :: (Member (State MiniBackend) r) => Sem (State (Map EmailKey (Maybe UserId, ActivationCode)) : r) a -> Sem r a +liftActivationCodeStoreState = interpret \case + Polysemy.State.Get -> gets (.activationCodes) + Put newAcs -> modify $ \b -> b {activationCodes = newAcs} + +liftBlockListStoreState :: (Member (State MiniBackend) r) => Sem (State [EmailKey] : r) a -> Sem r a +liftBlockListStoreState = interpret $ \case + Polysemy.State.Get -> gets (.blockList) + Put newBlockList -> modify $ \b -> b {blockList = newBlockList} liftUserKeyStoreState :: (Member (State MiniBackend) r) => Sem (State (Map EmailKey UserId) : r) a -> Sem r a liftUserKeyStoreState = interpret $ \case @@ -368,7 +466,7 @@ liftUserStoreState = interpret $ \case Put newUsers -> modify $ \b -> b {users = newUsers} runAllErrorsUnsafe :: forall a. (HasCallStack) => Sem AllErrors a -> a -runAllErrorsUnsafe = run . runErrorUnsafe . runErrorUnsafe +runAllErrorsUnsafe = run . runErrorUnsafe . runErrorUnsafe . runErrorUnsafe emptyFederationAPIAcesss :: InterpreterFor (FederationAPIAccess MiniFederationMonad) r emptyFederationAPIAcesss = interpret $ \case diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs index 9145369b703..e975ac6a06c 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs @@ -3,16 +3,20 @@ module Wire.MockInterpreters (module MockInterpreters) where -- Run this from project root to generate the imports: -- ls libs/wire-subsystems/test/unit/Wire/MockInterpreters | sed 's|\(.*\)\.hs|import Wire.MockInterpreters.\1 as MockInterpreters|' +import Wire.MockInterpreters.BlockListStore as MockInterpreters import Wire.MockInterpreters.EmailSubsystem as MockInterpreters import Wire.MockInterpreters.Error as MockInterpreters +import Wire.MockInterpreters.Events as MockInterpreters +import Wire.MockInterpreters.FederationConfigStore as MockInterpreters import Wire.MockInterpreters.GalleyAPIAccess as MockInterpreters import Wire.MockInterpreters.HashPassword as MockInterpreters +import Wire.MockInterpreters.IndexedUserStore as MockInterpreters import Wire.MockInterpreters.Now as MockInterpreters import Wire.MockInterpreters.PasswordResetCodeStore as MockInterpreters import Wire.MockInterpreters.PasswordStore as MockInterpreters +import Wire.MockInterpreters.PropertyStore as MockInterpreters import Wire.MockInterpreters.Random as MockInterpreters import Wire.MockInterpreters.SessionStore as MockInterpreters -import Wire.MockInterpreters.UserEvents as MockInterpreters import Wire.MockInterpreters.UserKeyStore as MockInterpreters import Wire.MockInterpreters.UserStore as MockInterpreters import Wire.MockInterpreters.UserSubsystem as MockInterpreters diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs new file mode 100644 index 00000000000..0265c8d07fe --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs @@ -0,0 +1,13 @@ +module Wire.MockInterpreters.ActivationCodeStore where + +import Data.Id +import Data.Map +import Imports +import Polysemy +import Polysemy.State +import Wire.API.User.Activation +import Wire.ActivationCodeStore (ActivationCodeStore (..)) +import Wire.UserKeyStore + +inMemoryActivationCodeStoreInterpreter :: (Member (State (Map EmailKey (Maybe UserId, ActivationCode))) r) => InterpreterFor ActivationCodeStore r +inMemoryActivationCodeStoreInterpreter = interpret \case LookupActivationCode ek -> gets (!? ek) diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/BlockListStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/BlockListStore.hs new file mode 100644 index 00000000000..2ed63f4e081 --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/BlockListStore.hs @@ -0,0 +1,13 @@ +module Wire.MockInterpreters.BlockListStore where + +import Imports +import Polysemy +import Polysemy.State +import Wire.BlockListStore +import Wire.UserKeyStore + +inMemoryBlockListStoreInterpreter :: (Member (State [EmailKey]) r) => InterpreterFor BlockListStore r +inMemoryBlockListStoreInterpreter = interpret $ \case + Insert uk -> modify (uk :) + Exists uk -> gets (elem uk) + Delete uk -> modify (filter (/= uk)) diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/EmailSubsystem.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/EmailSubsystem.hs index 57c9fac0c9e..ee8125d758e 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/EmailSubsystem.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/EmailSubsystem.hs @@ -16,10 +16,25 @@ data SentMail = SentMail data SentMailContent = PasswordResetMail PasswordResetPair deriving (Show, Eq) -emailSubsystemInterpreter :: (Member (State (Map Email [SentMail])) r) => InterpreterFor EmailSubsystem r +emailSubsystemInterpreter :: (Member (State (Map EmailAddress [SentMail])) r) => InterpreterFor EmailSubsystem r emailSubsystemInterpreter = interpret \case SendPasswordResetMail email keyCodePair mLocale -> modify $ Map.insertWith (<>) email [SentMail mLocale $ PasswordResetMail keyCodePair] _ -> error "emailSubsystemInterpreter: implement on demand" -getEmailsSentTo :: (Member (State (Map Email [SentMail])) r) => Email -> Sem r [SentMail] +getEmailsSentTo :: (Member (State (Map EmailAddress [SentMail])) r) => EmailAddress -> Sem r [SentMail] getEmailsSentTo email = gets $ Map.findWithDefault [] email + +noopEmailSubsystemInterpreter :: InterpreterFor EmailSubsystem r +noopEmailSubsystemInterpreter = interpret \case + SendPasswordResetMail {} -> pure () + SendVerificationMail {} -> pure () + SendCreateScimTokenVerificationMail {} -> pure () + SendLoginVerificationMail {} -> pure () + SendActivationMail {} -> pure () + SendEmailAddressUpdateMail {} -> pure () + SendNewClientEmail {} -> pure () + SendAccountDeletionEmail {} -> pure () + SendTeamActivationMail {} -> pure () + SendTeamDeletionVerificationMail {} -> pure () + SendTeamInvitationMail {} -> pure "" + SendTeamInvitationMailPersonalUser {} -> pure "" diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/Events.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/Events.hs new file mode 100644 index 00000000000..a80ec590088 --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/Events.hs @@ -0,0 +1,22 @@ +module Wire.MockInterpreters.Events where + +import Data.Id +import Imports +import Polysemy +import Polysemy.State +import Wire.API.UserEvent +import Wire.Events + +data MiniEvent = MkMiniEvent + { userId :: UserId, + mConnId :: Maybe ConnId, + event :: Event + } + deriving stock (Eq, Show) + +miniEventInterpreter :: + (Member (State [MiniEvent]) r) => + InterpreterFor Events r +miniEventInterpreter = interpret \case + GenerateUserEvent uid mconn e -> modify (MkMiniEvent uid mconn (UserEvent e) :) + GeneratePropertyEvent uid mconn e -> modify (MkMiniEvent uid (Just mconn) (PropertyEvent e) :) diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/FederationConfigStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/FederationConfigStore.hs new file mode 100644 index 00000000000..57a9bf5566e --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/FederationConfigStore.hs @@ -0,0 +1,36 @@ +module Wire.MockInterpreters.FederationConfigStore where + +import Imports +import Polysemy +import Polysemy.State +import Wire.API.Routes.FederationDomainConfig +import Wire.FederationConfigStore + +inMemoryFederationConfigStoreInterpreter :: + (Member (State [FederationDomainConfig]) r) => + InterpreterFor FederationConfigStore r +inMemoryFederationConfigStoreInterpreter = + interpret $ \case + GetFederationConfig domain -> gets $ find (\cfg -> cfg.domain == domain) + GetFederationConfigs -> do + remoteConfigs <- get + pure $ FederationDomainConfigs AllowDynamic remoteConfigs 1 + AddFederationConfig newCfg -> do + modify $ (newCfg :) . deleteBy (\a b -> a.domain == b.domain) newCfg + pure AddFederationRemoteSuccess + UpdateFederationConfig _ -> + error "UpdateFederationConfig not implemented in inMemoryFederationConfigStoreInterpreter" + AddFederationRemoteTeam _ _ -> + error "AddFederationRemoteTeam not implemented in inMemoryFederationConfigStoreInterpreter" + RemoveFederationRemoteTeam _ _ -> + error "RemoveFederationRemoteTeam not implemented in inMemoryFederationConfigStoreInterpreter" + GetFederationRemoteTeams _ -> + error "GetFederationRemoteTeams not implemented in inMemoryFederationConfigStoreInterpreter" + BackendFederatesWith _ -> + error "BackendFederatesWith not implemented in inMemoryFederationConfigStoreInterpreter" + +runFederationConfigStoreInMemory :: InterpreterFor FederationConfigStore r +runFederationConfigStoreInMemory = + evalState [] + . inMemoryFederationConfigStoreInterpreter + . raiseUnder diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs index 1e8a81e9f51..9f37e501b4a 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs @@ -1,5 +1,7 @@ module Wire.MockInterpreters.GalleyAPIAccess where +import Data.Id +import Data.Proxy import Imports import Polysemy import Wire.API.Team.Feature @@ -10,10 +12,14 @@ import Wire.GalleyAPIAccess miniGalleyAPIAccess :: -- | what to return when calling GetTeamMember Maybe TeamMember -> - -- | what to return when calling GetAllFeatureConfigsForUser - AllFeatureConfigs -> + -- | what to return when calling GetAllTeamFeaturesForUser + AllTeamFeatures -> InterpreterFor GalleyAPIAccess r miniGalleyAPIAccess member configs = interpret $ \case GetTeamMember _ _ -> pure member - GetAllFeatureConfigsForUser _ -> pure configs + GetAllTeamFeaturesForUser _ -> pure configs + GetFeatureConfigForTeam tid -> pure $ getFeatureConfigForTeamImpl configs tid _ -> error "uninterpreted effect: GalleyAPIAccess" + +getFeatureConfigForTeamImpl :: forall feature. (IsFeatureConfig feature) => AllTeamFeatures -> TeamId -> LockableFeature feature +getFeatureConfigForTeamImpl allfeatures _ = npProject' (Proxy @(feature)) allfeatures diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs index 05c15259bec..6684ca34c47 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs @@ -5,22 +5,29 @@ import Data.Misc import Data.Text.Encoding qualified as Text import Imports import Polysemy -import Wire.API.Password +import Wire.API.Password as Password import Wire.HashPassword staticHashPasswordInterpreter :: InterpreterFor HashPassword r staticHashPasswordInterpreter = interpret $ \case - HashPassword password -> go (hashPasswordArgon2idWithOptions fastArgon2IdOptions) "9bytesalt" password - where - go alg salt password = do - let passwordBS = Text.encodeUtf8 (fromPlainTextPassword password) - pure $ unsafeMkPassword $ alg salt passwordBS + HashPassword6 password -> hashPassword password + HashPassword8 password -> hashPassword password + +hashPassword :: (Monad m) => PlainTextPassword' t -> m Password +hashPassword password = + pure . Argon2Password $ + hashPasswordArgon2idWithSalt + fastArgon2IdOptions + "9bytesalt" + (Text.encodeUtf8 (fromPlainTextPassword password)) fastArgon2IdOptions :: Argon2.Options fastArgon2IdOptions = let hashParallelism = 4 - in defaultOptions - { iterations = 1, + in Argon2.Options + { variant = Argon2.Argon2id, + version = Argon2.Version13, + iterations = 1, parallelism = hashParallelism, -- This needs to be min 8 * hashParallelism, otherewise we get an -- unsafe error diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/IndexedUserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/IndexedUserStore.hs new file mode 100644 index 00000000000..06d78cfd24b --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/IndexedUserStore.hs @@ -0,0 +1,16 @@ +module Wire.MockInterpreters.IndexedUserStore where + +import Imports +import Polysemy +import Wire.IndexedUserStore + +inMemoryIndexedUserStoreInterpreter :: InterpreterFor IndexedUserStore r +inMemoryIndexedUserStoreInterpreter = + interpret $ \case + Upsert {} -> pure () + UpdateTeamSearchVisibilityInbound {} -> pure () + BulkUpsert {} -> pure () + DoesIndexExist -> pure True + SearchUsers {} -> error "IndexedUserStore: unimplemented in memory interpreter" + PaginateTeamMembers {} -> error "IndexedUserStore: unimplemented in memory interpreter" + GetTeamSize {} -> error "IndexedUserStore: unimplemented in memory interpreter" diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/InvitationStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/InvitationStore.hs new file mode 100644 index 00000000000..3de35c3da6a --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/InvitationStore.hs @@ -0,0 +1,28 @@ +module Wire.MockInterpreters.InvitationStore where + +import Data.Id (InvitationId, TeamId) +import Data.Map (elems, (!?)) +import Data.Map qualified as M +import Imports +import Polysemy +import Polysemy.State (State, get, gets) +import Wire.API.User (InvitationCode (..)) +import Wire.InvitationStore + +inMemoryInvitationStoreInterpreter :: + forall r. + ( Member (State (Map (TeamId, InvitationId) StoredInvitation)) r, + Member (State (Map (InvitationCode) StoredInvitation)) r + ) => + InterpreterFor InvitationStore r +inMemoryInvitationStoreInterpreter = interpret \case + InsertInvitation _a _timeout -> error "InsertInvitation" + LookupInvitation tid iid -> gets (!? (tid, iid)) + LookupInvitationByCode iid -> gets (!? iid) + LookupInvitationsByEmail em -> + let c i = guard (i.email == em) $> i + in mapMaybe c . elems <$> get @(Map (TeamId, InvitationId) _) + LookupInvitationsPaginated {} -> error "LookupInvitationsPaginated" + CountInvitations tid -> gets (fromIntegral . M.size . M.filterWithKey (\(tid', _) _v -> tid == tid')) + DeleteInvitation _tid _invId -> error "DeleteInvitation" + DeleteAllTeamInvitations _tid -> error "DeleteAllTeamInvitations" diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordResetCodeStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordResetCodeStore.hs index 25d6ab11d89..98bb17286bc 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordResetCodeStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordResetCodeStore.hs @@ -8,6 +8,12 @@ import Polysemy.State import Wire.API.User.Password import Wire.PasswordResetCodeStore +runInMemoryPasswordResetCodeStore :: forall r. InterpreterFor PasswordResetCodeStore r +runInMemoryPasswordResetCodeStore = + evalState (mempty :: Map PasswordResetKey (PRQueryData Identity)) + . inMemoryPasswordResetCodeStore + . raiseUnder + inMemoryPasswordResetCodeStore :: forall r. (Member (State (Map PasswordResetKey (PRQueryData Identity))) r) => diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordStore.hs index be4f1a140d3..a0eb7fc845c 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordStore.hs @@ -8,7 +8,11 @@ import Polysemy.State import Wire.API.Password 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 = 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/PropertyStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PropertyStore.hs new file mode 100644 index 00000000000..6cbc980f1ff --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PropertyStore.hs @@ -0,0 +1,19 @@ +module Wire.MockInterpreters.PropertyStore where + +import Data.Id +import Data.Map qualified as Map +import Imports +import Polysemy +import Polysemy.State +import Wire.API.Properties +import Wire.PropertyStore + +inMemoryPropertyStoreInterpreter :: (Member (State (Map UserId (Map PropertyKey RawPropertyValue))) r) => InterpreterFor PropertyStore r +inMemoryPropertyStoreInterpreter = interpret $ \case + InsertProperty u k v -> modify $ Map.insertWith (Map.union) u (Map.singleton k v) + LookupProperty u k -> gets $ Map.lookup k <=< Map.lookup u + CountProperties u -> gets $ Map.size . Map.findWithDefault mempty u + DeleteProperty u k -> modify $ Map.adjust (Map.delete k) u + ClearProperties u -> modify $ Map.delete u + GetPropertyKeys u -> gets $ Map.keys . Map.findWithDefault mempty u + GetAllProperties u -> gets $ Map.toAscList . Map.findWithDefault mempty u diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SessionStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SessionStore.hs index 43e2736ba2e..fcfc136d1ba 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SessionStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SessionStore.hs @@ -8,6 +8,12 @@ import Polysemy.State import Wire.API.User.Auth import Wire.SessionStore +runInMemorySessionStore :: InterpreterFor SessionStore r +runInMemorySessionStore = + evalState (mempty :: Map UserId [Cookie ()]) + . inMemorySessionStoreInterpreter + . raiseUnder + inMemorySessionStoreInterpreter :: (Member (State (Map UserId [Cookie ()])) r) => InterpreterFor SessionStore r inMemorySessionStoreInterpreter = interpret $ \case InsertCookie uid cookie _ttl -> modify $ Map.insertWith (<>) uid [cookie] diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserEvents.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserEvents.hs deleted file mode 100644 index 4bcd7319418..00000000000 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserEvents.hs +++ /dev/null @@ -1,20 +0,0 @@ -module Wire.MockInterpreters.UserEvents where - -import Data.Id -import Imports -import Polysemy -import Polysemy.State -import Wire.API.UserEvent -import Wire.UserEvents - -data MiniEvent = MkMiniEvent - { userId :: UserId, - event :: UserEvent - } - deriving stock (Eq, Show) - -miniEventInterpreter :: - (Member (State [MiniEvent]) r) => - InterpreterFor UserEvents r -miniEventInterpreter = interpret \case - GenerateUserEvent uid _mconn e -> modify (MkMiniEvent uid e :) diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs index 563b91f4bd1..133365cf986 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs @@ -1,7 +1,12 @@ +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} + module Wire.MockInterpreters.UserStore where +import Cassandra.Util import Data.Handle import Data.Id +import Data.Time +import Data.Time.Calendar.OrdinalDate import Imports import Polysemy import Polysemy.Error @@ -10,13 +15,14 @@ import Wire.API.User hiding (DeleteUser) import Wire.API.User qualified as User import Wire.StoredUser import Wire.UserStore +import Wire.UserStore.IndexUser inMemoryUserStoreInterpreter :: forall r. (Member (State [StoredUser]) r) => InterpreterFor UserStore r inMemoryUserStoreInterpreter = interpret $ \case - GetUser uid -> gets $ find (\user -> user.id == uid) + GetUsers uids -> gets $ filter (\user -> user.id `elem` uids) UpdateUser uid update -> modify (map doUpdate) where doUpdate :: StoredUser -> StoredUser @@ -31,6 +37,10 @@ inMemoryUserStoreInterpreter = interpret $ \case . maybe Imports.id setStoredUserSupportedProtocols update.supportedProtocols $ u else u + GetIndexUser uid -> + gets $ fmap storedUserToIndexUser . find (\user -> user.id == uid) + GetIndexUsersPaginated _pageSize _pagingState -> + error "GetIndexUsersPaginated not implemented in inMemoryUserStoreInterpreter" UpdateUserHandleEither uid hUpdate -> runError $ modifyLocalUsers (traverse doUpdate) where doUpdate :: StoredUser -> Sem (Error StoredUserUpdateError : r) StoredUser @@ -57,6 +67,34 @@ inMemoryUserStoreInterpreter = interpret $ \case LookupStatus uid -> lookupStatusImpl uid IsActivated uid -> isActivatedImpl uid LookupLocale uid -> lookupLocaleImpl uid + UpdateUserTeam uid tid -> + modify $ + map + (\u -> if u.id == uid then u {teamId = Just tid} :: StoredUser else u) + GetActivityTimestamps _ -> pure [] + GetRichInfo _ -> error "rich info not implemented" + GetUserAuthenticationInfo _uid -> error "Not implemented" + +storedUserToIndexUser :: StoredUser -> IndexUser +storedUserToIndexUser storedUser = + -- If we really care about this, we could start storing the writetimes, but we + -- don't need it right now + let withDefaultTime x = WithWriteTime x $ Writetime $ UTCTime (YearDay 0 1) 0 + in IndexUser + { userId = storedUser.id, + teamId = withDefaultTime <$> storedUser.teamId, + name = withDefaultTime storedUser.name, + accountStatus = withDefaultTime <$> storedUser.status, + handle = withDefaultTime <$> storedUser.handle, + email = withDefaultTime <$> storedUser.email, + colourId = withDefaultTime storedUser.accentId, + activated = withDefaultTime storedUser.activated, + serviceId = withDefaultTime <$> storedUser.serviceId, + managedBy = withDefaultTime <$> storedUser.managedBy, + ssoId = withDefaultTime <$> storedUser.ssoId, + unverifiedEmail = Nothing, + writeTimeBumper = Nothing + } lookupLocaleImpl :: (Member (State [StoredUser]) r) => UserId -> Sem r (Maybe ((Maybe Language, Maybe Country))) lookupLocaleImpl uid = do diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs index b47bfbd7d25..839ced4e5ad 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs @@ -4,12 +4,15 @@ import Data.Qualified import Imports import Polysemy import Wire.API.User -import Wire.UserKeyStore import Wire.UserSubsystem -userSubsystemTestInterpreter :: [UserAccount] -> InterpreterFor UserSubsystem r +-- HINT: This is used to test AuthenticationSubsystem, not to test itself! +userSubsystemTestInterpreter :: [User] -> InterpreterFor UserSubsystem r userSubsystemTestInterpreter initialUsers = interpret \case - GetLocalUserAccountByUserKey localUserKey -> case (tUnqualified localUserKey) of - EmailKey _ email -> pure $ find (\u -> userEmail u.accountUser == Just email) initialUsers + GetAccountsByEmailNoFilter (tUnqualified -> emails) -> + pure $ + filter + (\u -> userEmail u `elem` (Just <$> emails)) + initialUsers _ -> error $ "userSubsystemTestInterpreter: implement on demand" diff --git a/libs/wire-subsystems/test/unit/Wire/NotificationSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/NotificationSubsystem/InterpreterSpec.hs index 1c633c201ab..9fbd3babe89 100644 --- a/libs/wire-subsystems/test/unit/Wire/NotificationSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/NotificationSubsystem/InterpreterSpec.hs @@ -1,16 +1,15 @@ module Wire.NotificationSubsystem.InterpreterSpec (spec) where -import Bilge (RequestId (..)) import Control.Concurrent.Async (async, wait) import Control.Exception (throwIO) import Data.Data (Proxy (Proxy)) +import Data.Id import Data.List.NonEmpty (NonEmpty ((:|)), fromList) import Data.List1 qualified as List1 import Data.Range (fromRange, toRange) import Data.Set qualified as Set import Data.String.Conversions import Data.Time.Clock.DiffTime -import Gundeck.Types.Push.V2 qualified as V2 import Imports import Numeric.Natural (Natural) import Polysemy @@ -21,6 +20,7 @@ import System.Timeout (timeout) import Test.Hspec import Test.QuickCheck import Test.QuickCheck.Instances () +import Wire.API.Push.V2 qualified as V2 import Wire.GundeckAPIAccess import Wire.GundeckAPIAccess qualified as GundeckAPIAccess import Wire.NotificationSubsystem @@ -37,7 +37,7 @@ spec = describe "NotificationSubsystem.Interpreter" do { fanoutLimit = toRange $ Proxy @30, chunkSize = 12, slowPushDelay = 0, - requestId = RequestId "N/A" + requestId = RequestId defRequestId } connId2 <- generate arbitrary @@ -98,7 +98,7 @@ spec = describe "NotificationSubsystem.Interpreter" do { fanoutLimit = toRange $ Proxy @30, chunkSize = 12, slowPushDelay = 0, - requestId = RequestId "N/A" + requestId = RequestId defRequestId } connId2 <- generate arbitrary @@ -153,7 +153,7 @@ spec = describe "NotificationSubsystem.Interpreter" do { fanoutLimit = toRange $ Proxy @30, chunkSize = 12, slowPushDelay = 1, - requestId = RequestId "N/A" + requestId = RequestId defRequestId } connId2 <- generate arbitrary @@ -211,7 +211,7 @@ spec = describe "NotificationSubsystem.Interpreter" do { fanoutLimit = toRange $ Proxy @30, chunkSize = 12, slowPushDelay = 1, - requestId = RequestId "N/A" + requestId = RequestId defRequestId } user1 <- generate arbitrary @@ -228,9 +228,8 @@ spec = describe "NotificationSubsystem.Interpreter" do pushJson = payload1, _pushApsData = Nothing } - pushes = [push1] (_, attemptedPushes, logs) <- runMiniStackAsync mockConfig $ do - thread <- pushAsyncImpl pushes + thread <- pushAsyncImpl push1 await thread attemptedPushes `shouldBe` [[toV2Push push1]] diff --git a/libs/wire-subsystems/test/unit/Wire/PropertySubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/PropertySubsystem/InterpreterSpec.hs new file mode 100644 index 00000000000..7065e5b8b1d --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/PropertySubsystem/InterpreterSpec.hs @@ -0,0 +1,265 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + +module Wire.PropertySubsystem.InterpreterSpec where + +import Data.Aeson (FromJSON, ToJSON, Value (..), (.=)) +import Data.Aeson qualified as Aeson +import Data.Aeson.Key qualified as Key +import Data.Aeson.Text qualified as Aeson +import Data.Bifunctor (second) +import Data.ByteString.Lazy qualified as LBS +import Data.Map qualified as Map +import Data.Range +import Data.Scientific (scientific) +import Data.Set qualified as Set +import Data.Text qualified as Text +import Data.Text.Ascii (AsciiPrintable, AsciiText (..), validatePrintable) +import Data.Text.Lazy qualified as LText +import Data.Text.Lazy.Encoding qualified as LText +import GHC.IsList (fromList) +import Imports +import Polysemy +import Polysemy.Error +import Polysemy.State +import Polysemy.TinyLog +import Test.Hspec +import Test.Hspec.QuickCheck +import Test.QuickCheck +import Wire.API.Properties +import Wire.API.UserEvent +import Wire.Events +import Wire.MockInterpreters +import Wire.PropertyStore (PropertyStore) +import Wire.PropertySubsystem +import Wire.PropertySubsystem.Interpreter +import Wire.Sem.Logger.TinyLog (discardTinyLogs) + +defaultConfig :: PropertySubsystemConfig +defaultConfig = + PropertySubsystemConfig + { maxKeyLength = 1024, + maxValueLength = 1024, + maxProperties = 16 + } + +interpretDependencies :: Sem '[PropertyStore, Events, State [MiniEvent], TinyLog, Error e] a -> Either e a +interpretDependencies = + run + . runError + . discardTinyLogs + . evalState mempty + . miniEventInterpreter + . evalState mempty + . inMemoryPropertyStoreInterpreter + . raiseUnder + +spec :: Spec +spec = do + describe "Wire.PropertySubsystem.Interpreter" $ do + prop "set/lookup property" $ + \uid connId key (SmallJSON val) -> + let valBS = Aeson.encode val + rawVal = RawPropertyValue valBS + retrievedVal = + interpretDependencies + . interpretPropertySubsystem defaultConfig + $ do + setProperty uid connId key rawVal + lookupProperty uid key + in retrievedVal === Right (Just rawVal) + + prop "events" $ do + \uid connId key (SmallJSON val) -> + let valBS = Aeson.encode val + rawVal = RawPropertyValue valBS + assertion = + interpretDependencies + . interpretPropertySubsystem defaultConfig + $ do + setProperty uid connId key rawVal + eventsAfterSet <- get + + -- clear events + put [] + deleteProperty uid connId key + eventsAfterDelete <- get + + put [] + clearProperties uid connId + eventsAfterClear <- get + + -- assertions + pure $ + eventsAfterSet === [MkMiniEvent uid (Just connId) $ PropertyEvent $ PropertySet key val] + .&&. eventsAfterDelete === [MkMiniEvent uid (Just connId) $ PropertyEvent $ PropertyDeleted key] + .&&. eventsAfterClear === [MkMiniEvent uid (Just connId) $ PropertyEvent PropertiesCleared] + in either + (\e -> counterexample ("UnexpectedError: " <> show e) False) + id + assertion + + prop "set/delete/lookup property" $ + \uid connId key (SmallJSON val) -> + let valBS = Aeson.encode val + rawVal = RawPropertyValue valBS + retrievedVal = interpretDependencies . interpretPropertySubsystem defaultConfig $ do + setProperty uid connId key rawVal + deleteProperty uid connId key + lookupProperty uid key + in retrievedVal === Right Nothing + + prop "getAllProperties" $ + -- 16 is the default maxProperties + \uid connId (fromRange @0 @16 @[(PropertyKey, SmallJSON)] -> keySmallVal) -> + let keyVal = unwrapSmallJSON <$> Map.fromList keySmallVal + keyValRaw = RawPropertyValue . Aeson.encode <$> keyVal + retrievedVal = + interpretDependencies + . interpretPropertySubsystem defaultConfig + $ do + forM_ (Map.toAscList keyValRaw) (uncurry (setProperty uid connId)) + getAllProperties uid + in retrievedVal === Right (PropertyKeysAndValues keyVal) + + prop "getPropertyKeys" $ + -- 16 is the default maxProperties + \uid connId (fromRange @0 @16 @[(PropertyKey, SmallJSON)] -> keyVals) -> + let keyValRaw = Map.fromList $ map (second (RawPropertyValue . Aeson.encode)) keyVals + retrievedVal = + interpretDependencies + . interpretPropertySubsystem defaultConfig + $ do + forM_ (Map.toAscList keyValRaw) (uncurry (setProperty uid connId)) + getPropertyKeys uid + in second Set.fromList retrievedVal === Right (Map.keysSet keyValRaw) + + prop "clearProperties" $ + -- 16 is the default maxProperties + \uid connId (fromRange @0 @16 @[(PropertyKey, SmallJSON)] -> keyVals) -> + let keyValRaw = Map.fromList $ map (second (RawPropertyValue . Aeson.encode)) keyVals + retrievedVal = + interpretDependencies + . interpretPropertySubsystem defaultConfig + $ do + forM_ (Map.toAscList keyValRaw) (uncurry (setProperty uid connId)) + clearProperties uid connId + getAllProperties uid + in retrievedVal === Right (PropertyKeysAndValues mempty) + + prop "setting non JSON values should result in an error" $ + -- 1024 is the default max value length + \uid connId key (fromRange @0 @1024 @[Word8] -> nonJSONBytes) -> + let nonJSONBS = LBS.pack nonJSONBytes + setPropertyResult = interpretDependencies . interpretPropertySubsystem defaultConfig $ do + setProperty uid connId key (RawPropertyValue nonJSONBS) + in isNothing (Aeson.decode @Value nonJSONBS) ==> + case setPropertyResult of + Left (PropertyValueInvalid _) -> property True + Left x -> counterexample ("Expected PropertyValueInvalid, got: " <> show x) False + Right () -> counterexample ("Expected PropertyValueInvalid, but there was no error") False + + prop "setting very big JSON values should result in an error" $ + -- Capping default max value length to 1024 to make tests faster, bigger + -- number => slower tests. + \uid connId key (val :: Value) (fromIntegral . fromRange @0 @1024 @Int32 -> maxValueLength) -> + let cfg = defaultConfig {maxValueLength = maxValueLength} + -- Adding spaces to the end shouldn't change the meaning of a JSON, + -- maybe there are better ways of generating a big JSON + valBS = + LText.encodeUtf8 + . LText.justifyLeft (fromIntegral $ maxValueLength + 1) ' ' + $ Aeson.encodeToLazyText val + rawVal = RawPropertyValue valBS + setPropertyResult = interpretDependencies . interpretPropertySubsystem cfg $ do + setProperty uid connId key rawVal + in setPropertyResult === Left PropertyValueTooLarge + + prop "setting very big key names should result in an error" $ + \uid connId (fromRange @1 @1024 @AsciiPrintable -> unwrappedKey) (val :: SmallJSON) -> + let cfg = defaultConfig {maxKeyLength = (fromIntegral . Text.length $ toText unwrappedKey) - 1} + valBS = Aeson.encode val + rawVal = RawPropertyValue valBS + setPropertyResult = interpretDependencies . interpretPropertySubsystem cfg $ do + setProperty uid connId (PropertyKey unwrappedKey) rawVal + in setPropertyResult === Left PropertyKeyTooLarge + + prop "setProperty should respect maxProperties config" $ + \uid connId keyPrefix (SmallJSON val) (fromIntegral . fromRange @1 @20 @Int32 -> maxProperties) -> + let cfg = defaultConfig {maxProperties = maxProperties} + mkKey n = + let Right suffix = validatePrintable $ Text.pack $ show n + in PropertyKey $ keyPrefix <> suffix + keys = map mkKey [1 .. maxProperties] + extraKey = mkKey (maxProperties + 1) + valBS = Aeson.encode val + rawVal = RawPropertyValue valBS + assertion = + interpretDependencies + . interpretPropertySubsystem cfg + $ do + forM_ keys $ \key -> setProperty uid connId key rawVal + setPropErr <- catchExpectedError $ setProperty uid connId extraKey rawVal + allProps <- getAllProperties uid + pure $ + LBS.length valBS <= defaultConfig.maxValueLength ==> + setPropErr === Just TooManyProperties + .&&. allProps === PropertyKeysAndValues (Map.fromList (map (,val) keys)) + in either + (\e -> counterexample ("UnexpectedError: " <> show e) False) + id + assertion + + prop "setProperty should work for pre-existing properties even when maxProperties is reached" $ + \uid connId keyPrefix (SmallJSON val) (SmallJSON newVal) (fromIntegral . fromRange @1 @20 @Int32 -> maxProperties) -> + let cfg = defaultConfig {maxProperties = maxProperties} + mkKey n = + let Right suffix = validatePrintable $ Text.pack $ show n + in PropertyKey $ keyPrefix <> suffix + keys = map mkKey [1 .. maxProperties] + rawVal = RawPropertyValue (Aeson.encode val) + newRawVal = RawPropertyValue (Aeson.encode newVal) + retrievedVal = + interpretDependencies + . interpretPropertySubsystem cfg + $ do + forM_ keys $ \key -> setProperty uid connId key rawVal + setProperty uid connId (head keys) newRawVal + lookupProperty uid (head keys) + in retrievedVal === Right (Just newRawVal) + + describe "arbitrary @SmallJSON" $ + -- Please run this at least a million times when something about it changes + prop "Always produces JSON <= 1024 bytes" $ + \(smallJSON :: SmallJSON) -> + let jsonStr = LText.unpack $ Aeson.encodeToLazyText smallJSON + jsonBS = Aeson.encode smallJSON + in counterexample ("length = " <> show (LBS.length jsonBS) <> "\n" <> jsonStr) $ LBS.length jsonBS <= 1024 + +newtype SmallJSON = SmallJSON {unwrapSmallJSON :: Value} + deriving stock (Show, Eq) + deriving newtype (FromJSON, ToJSON) + +-- | generates small-ish JSON values +instance Arbitrary SmallJSON where + arbitrary = SmallJSON <$> go 0 + where + maxThings = 5 + -- ASCII chars take less space in the JSON + genText = toText . fromRange <$> arbitrary @(Range 0 5 AsciiPrintable) + go depth + | depth >= maxThings = pure Null + | otherwise = do + chooseInt (0, 5) >>= \case + 0 -> String <$> genText + 1 -> Number <$> (scientific <$> chooseInteger (0, 1000) <*> chooseInt (-1, 2)) + 2 -> Bool <$> arbitrary + 3 -> pure $ Null + 4 -> do + n <- chooseInt (0, maxThings) + Array . fromList <$> replicateM n (go (depth + 1)) + _ -> do + n <- chooseInt (0, maxThings) + keys <- Key.fromText <$$> replicateM n genText + vals <- replicateM n $ go (depth + 1) + pure . Aeson.object $ zipWith (.=) keys vals diff --git a/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs new file mode 100644 index 00000000000..5e82d9a569e --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs @@ -0,0 +1,54 @@ +module Wire.UserSearch.TypesSpec where + +import Control.Error (hush) +import Data.Aeson as Aeson +import Data.Fixed +import Data.Handle +import Data.Id +import Data.Json.Util +import Data.Time +import Data.Time.Clock.POSIX +import Imports +import Test.Hspec +import Test.Hspec.QuickCheck +import Test.QuickCheck +import Wire.API.Team.Role +import Wire.API.User +import Wire.UserSearch.Types + +spec :: Spec +spec = describe "UserDoc" $ do + describe "JSON" $ do + prop "roundrip to/fromJSON" $ \(userDoc :: UserDoc) -> + fromJSON (toJSON userDoc) === Aeson.Success userDoc + + it "should be backwards comptibile" $ do + eitherDecode (userDoc1ByteString) `shouldBe` Right userDoc1 + +mkTime :: Int -> UTCTime +mkTime = posixSecondsToUTCTime . secondsToNominalDiffTime . MkFixed . (* 1000000000) . fromIntegral + +userDoc1 :: UserDoc +userDoc1 = + UserDoc + { udId = fromJust . hush . parseIdFromText $ "0a96b396-57d6-11ea-a04b-7b93d1a5c19c", + udTeam = hush . parseIdFromText $ "17c59b18-57d6-11ea-9220-8bbf5eee961a", + udName = Just . Name $ "Carl Phoomp", + udNormalized = Just $ "carl phoomp", + udHandle = Just . fromJust . parseHandle $ "phoompy", + udEmail = Just $ unsafeEmailAddress "phoompy" "example.com", + udColourId = Just . ColourId $ 32, + udAccountStatus = Just Active, + udSAMLIdP = Just "https://issuer.net/214234", + udManagedBy = Just ManagedByScim, + udCreatedAt = Just (toUTCTimeMillis (mkTime 1598737800000)), + udRole = Just RoleAdmin, + udSearchVisibilityInbound = Nothing, + udScimExternalId = Nothing, + udSso = Nothing, + udEmailUnvalidated = Nothing + } + +-- Dont touch this. This represents serialized legacy data. +userDoc1ByteString :: LByteString +userDoc1ByteString = "{\"email\":\"phoompy@example.com\",\"account_status\":\"active\",\"handle\":\"phoompy\",\"managed_by\":\"scim\",\"role\":\"admin\",\"accent_id\":32,\"name\":\"Carl Phoomp\",\"created_at\":\"2020-08-29T21:50:00.000Z\",\"team\":\"17c59b18-57d6-11ea-9220-8bbf5eee961a\",\"id\":\"0a96b396-57d6-11ea-a04b-7b93d1a5c19c\",\"normalized\":\"carl phoomp\",\"saml_idp\":\"https://issuer.net/214234\"}" diff --git a/libs/wire-subsystems/test/unit/Wire/UserStoreSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserStoreSpec.hs index b1cbf972f98..5f9192c469b 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserStoreSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserStoreSpec.hs @@ -20,7 +20,7 @@ spec = do prop "user deleted" $ \domain defaultLocale storedUser -> let user = mkUserFromStored domain defaultLocale storedUser - in user.userDeleted === (storedUser.status == Just Deleted) + in userDeleted user === (storedUser.status == Just Deleted) prop "user expires" $ \domain defaultLocale storedUser -> let user = mkUserFromStored domain defaultLocale storedUser diff --git a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs index 4abd27efd0f..c573d4709c5 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs @@ -28,6 +28,9 @@ import Wire.API.Team.Member import Wire.API.Team.Permission import Wire.API.User hiding (DeleteUser) import Wire.API.UserEvent +import Wire.AuthenticationSubsystem.Error +import Wire.InvitationStore (StoredInvitation) +import Wire.InvitationStore qualified as InvitationStore import Wire.MiniBackend import Wire.StoredUser import Wire.UserKeyStore @@ -54,8 +57,9 @@ spec = describe "UserSubsystem.Interpreter" do target1 = mkUserIds remoteDomain1 targetUsers1 target2 = mkUserIds remoteDomain2 targetUsers2 localBackend = def {users = [viewer] <> localTargetUsers} + config = UserSubsystemConfig visibility miniLocale False 100 retrievedProfiles = - runFederationStack localBackend federation Nothing (UserSubsystemConfig visibility miniLocale) $ + runFederationStack localBackend federation Nothing config $ getUserProfiles (toLocalUnsafe localDomain viewer.id) (localTargets <> target1 <> target2) @@ -81,11 +85,12 @@ spec = describe "UserSubsystem.Interpreter" do mkUserIds domain users = map (flip Qualified domain . (.id)) users onlineUsers = mkUserIds onlineDomain onlineTargetUsers offlineUsers = mkUserIds offlineDomain offlineTargetUsers - config = UserSubsystemConfig visibility miniLocale + config = UserSubsystemConfig visibility miniLocale False 100 localBackend = def {users = [viewer]} result = run . runErrorUnsafe @UserSubsystemError + . runErrorUnsafe @AuthenticationSubsystemError . runError @FederationError . interpretFederationStack localBackend online Nothing config $ getUserProfiles @@ -100,49 +105,45 @@ spec = describe "UserSubsystem.Interpreter" do describe "[without federation]" do prop "returns nothing when none of the users exist" $ - \viewer targetUserIds visibility domain locale -> - let config = UserSubsystemConfig visibility locale - retrievedProfiles = + \viewer targetUserIds config domain -> + let retrievedProfiles = runNoFederationStack def Nothing config $ getUserProfiles (toLocalUnsafe domain viewer) (map (`Qualified` domain) targetUserIds) in retrievedProfiles === [] prop "gets a local user profile when the user exists and both user and viewer have accepted their invitations" $ - \(NotPendingStoredUser viewer) (NotPendingStoredUser targetUserNoTeam) visibility domain locale sameTeam -> + \(NotPendingStoredUser viewer) (NotPendingStoredUser targetUserNoTeam) config domain sameTeam -> let teamMember = mkTeamMember viewer.id fullPermissions Nothing defUserLegalHoldStatus targetUser = if sameTeam then targetUserNoTeam {teamId = viewer.teamId} else targetUserNoTeam - config = UserSubsystemConfig visibility locale localBackend = def {users = [targetUser, viewer]} retrievedProfiles = runNoFederationStack localBackend (Just teamMember) config $ getUserProfiles (toLocalUnsafe domain viewer.id) [Qualified targetUser.id domain] in retrievedProfiles === [ mkUserProfile - (fmap (const $ (,) <$> viewer.teamId <*> Just teamMember) visibility) - (mkUserFromStored domain locale targetUser) + (fmap (const $ (,) <$> viewer.teamId <*> Just teamMember) config.emailVisibilityConfig) + (mkUserFromStored domain config.defaultLocale targetUser) defUserLegalHoldStatus ] prop "gets a local user profile when the target user exists and has accepted their invitation but the viewer has not accepted their invitation" $ - \(PendingStoredUser viewer) (NotPendingStoredUser targetUserNoTeam) visibility domain locale sameTeam -> + \(PendingStoredUser viewer) (NotPendingStoredUser targetUserNoTeam) config domain sameTeam -> let teamMember = mkTeamMember viewer.id fullPermissions Nothing defUserLegalHoldStatus targetUser = if sameTeam then targetUserNoTeam {teamId = viewer.teamId} else targetUserNoTeam - config = UserSubsystemConfig visibility locale localBackend = def {users = [targetUser, viewer]} retrievedProfile = runNoFederationStack localBackend (Just teamMember) config $ getUserProfiles (toLocalUnsafe domain viewer.id) [Qualified targetUser.id domain] in retrievedProfile === [ mkUserProfile - (fmap (const Nothing) visibility) - (mkUserFromStored domain locale targetUser) + (fmap (const Nothing) config.emailVisibilityConfig) + (mkUserFromStored domain config.defaultLocale targetUser) defUserLegalHoldStatus ] prop "returns Nothing if the target user has not accepted their invitation yet" $ - \viewer (PendingStoredUser targetUser) visibility domain locale -> + \viewer (PendingStoredUser targetUser) config domain -> let teamMember = mkTeamMember viewer.id fullPermissions Nothing defUserLegalHoldStatus - config = UserSubsystemConfig visibility locale localBackend = def {users = [targetUser, viewer]} retrievedProfile = runNoFederationStack localBackend (Just teamMember) config $ @@ -154,7 +155,7 @@ spec = describe "UserSubsystem.Interpreter" do \viewer targetUsers visibility domain remoteDomain -> do let remoteBackend = def {users = targetUsers} federation = [(remoteDomain, remoteBackend)] - config = UserSubsystemConfig visibility miniLocale + config = UserSubsystemConfig visibility miniLocale False 100 localBackend = def {users = [viewer]} retrievedProfilesWithErrors :: ([(Qualified UserId, FederationError)], [UserProfile]) = runFederationStack localBackend federation Nothing config $ @@ -175,7 +176,7 @@ spec = describe "UserSubsystem.Interpreter" do prop "Remote users on offline backend always fail to return" $ \viewer (targetUsers :: Set StoredUser) visibility domain remoteDomain -> do let online = mempty - config = UserSubsystemConfig visibility miniLocale + config = UserSubsystemConfig visibility miniLocale False 100 localBackend = def {users = [viewer]} retrievedProfilesWithErrors :: ([(Qualified UserId, FederationError)], [UserProfile]) = runFederationStack localBackend online Nothing config $ @@ -195,7 +196,7 @@ spec = describe "UserSubsystem.Interpreter" do allDomains = [domain, remoteDomainA, remoteDomainB] remoteAUsers = map (flip Qualified remoteDomainA . (.id)) targetUsers remoteBUsers = map (flip Qualified remoteDomainB . (.id)) targetUsers - config = UserSubsystemConfig visibility miniLocale + config = UserSubsystemConfig visibility miniLocale False 100 localBackend = def {users = [viewer]} retrievedProfilesWithErrors :: ([(Qualified UserId, FederationError)], [UserProfile]) = runFederationStack localBackend online Nothing config $ @@ -221,9 +222,12 @@ spec = describe "UserSubsystem.Interpreter" do getSelfProfile (toLocalUnsafe domain selfId) in retrievedProfile === Nothing - prop "should mark user as managed by scim if E2EId is enabled for the user and they have a handle" \storedSelf domain susbsystemConfig mlsE2EIdConfig -> + prop "should mark user as managed by scim if E2EId is enabled for the user and they have a handle" \storedSelf domain susbsystemConfig (mlsE2EIdConfig :: MlsE2EIdConfig) -> let localBackend = def {users = [storedSelf]} - allFeatureConfigs = def {afcMlsE2EId = withStatus FeatureStatusEnabled LockStatusUnlocked mlsE2EIdConfig FeatureTTLUnlimited} + allFeatureConfigs = + npUpdate + (LockableFeature FeatureStatusEnabled LockStatusUnlocked mlsE2EIdConfig) + def SelfProfile retrievedUser = fromJust . runAllErrorsUnsafe @@ -251,18 +255,20 @@ spec = describe "UserSubsystem.Interpreter" do .&&. userAfterUpdate.userLocale === fromMaybe userBeforeUpdate.userLocale update.locale prop "Update user events" $ - \(NotPendingStoredUser alice) localDomain update config -> do + \(NotPendingStoredUser alice) connId localDomain update config -> do let lusr = toLocalUnsafe localDomain alice.id localBackend = def {users = [alice {managedBy = Just ManagedByWire}]} events = runNoFederationStack localBackend Nothing config do - updateUserProfile lusr Nothing UpdateOriginScim update + updateUserProfile lusr connId UpdateOriginScim update get @[MiniEvent] in events === [ MkMiniEvent alice.id - ( UserUpdated $ + connId + ( UserEvent . UserUpdated $ (emptyUserUpdatedData alice.id) { eupName = update.name, + eupTextStatus = update.textStatus, eupPict = update.pict, eupAccentId = update.accentId, eupAssets = update.assets, @@ -272,6 +278,302 @@ spec = describe "UserSubsystem.Interpreter" do ) ] + describe "getAccountsBy" do + prop "GetBy userId when pending fails if not explicitly allowed" $ + \(PendingNotEmptyIdentityStoredUser alice') email teamId invitationInfo localDomain visibility locale -> + let config = UserSubsystemConfig visibility locale False 100 + alice = + alice' + { email = Just email, + teamId = Just teamId + -- For simplicity, so we don't have to match the email with invitation + } + getBy = + toLocalUnsafe localDomain $ + def + { getByUserId = [alice.id], + includePendingInvitations = NoPendingInvitations + } + localBackend = + def + { users = [alice], + -- We need valid invitations or the user gets deleted by + -- our drive-by cleanup job in the interprter. + -- FUTUREWORK: Remove this if we remove the enqueueDeletion from getAccountsByImpl + invitations = + Map.singleton + (teamId, invitationInfo.invitationId) + ( invitationInfo + { InvitationStore.email = email, + InvitationStore.teamId = teamId + } + ) + } + result = + runNoFederationStack localBackend Nothing config $ + getAccountsBy getBy + in result === [] + + prop "GetBy userId works for pending if explicitly queried" $ + \(PendingNotEmptyIdentityStoredUser alice') email teamId invitationInfo localDomain visibility locale -> + let config = UserSubsystemConfig visibility locale True 100 + alice = + alice' + { email = Just email, + teamId = Just teamId + -- For simplicity, so we don't have to match the email with invitation + } + getBy = + toLocalUnsafe localDomain $ + def + { getByUserId = [alice.id], + includePendingInvitations = WithPendingInvitations + } + localBackend = + def + { users = [alice], + -- We need valid invitations or the user gets deleted by + -- our drive-by cleanup job in the interprter. + -- FUTUREWORK: Remove this if we remove the enqueueDeletion from getAccountsByImpl + invitations = + Map.singleton + (teamId, invitationInfo.invitationId) + ( invitationInfo + { InvitationStore.email = email, + InvitationStore.teamId = teamId + } + ) + } + result = + runNoFederationStack localBackend Nothing config $ + getAccountsBy getBy + in result === [mkUserFromStored localDomain locale alice] + prop "GetBy handle when pending fails if not explicitly allowed" $ + \(PendingNotEmptyIdentityStoredUser alice') handl email teamId invitationInfo localDomain visibility locale -> + let config = UserSubsystemConfig visibility locale True 100 + alice = + alice' + { email = Just email, + teamId = Just teamId, + handle = Just handl + -- For simplicity, so we don't have to match the email with invitation + } + getBy = + toLocalUnsafe localDomain $ + def + { getByHandle = [handl], + includePendingInvitations = NoPendingInvitations + } + localBackend = + def + { users = [alice], + -- We need valid invitations or the user gets deleted by + -- our drive-by cleanup job in the interprter. + -- FUTUREWORK: Remove this if we remove the enqueueDeletion from getAccountsByImpl + invitations = + Map.singleton + (teamId, invitationInfo.invitationId) + ( invitationInfo + { InvitationStore.email = email, + InvitationStore.teamId = teamId + } + ) + } + result = + runNoFederationStack localBackend Nothing config $ + getAccountsBy getBy + in result === [] + + prop "GetBy handle works for pending if explicitly queried" $ + \(PendingNotEmptyIdentityStoredUser alice') handl email teamId invitationInfo localDomain visibility locale -> + let config = UserSubsystemConfig visibility locale True 100 + alice = + alice' + { email = Just email, + teamId = Just teamId, + handle = Just handl + -- For simplicity, so we don't have to match the email with invitation + } + getBy = + toLocalUnsafe localDomain $ + def + { getByHandle = [handl], + includePendingInvitations = WithPendingInvitations + } + localBackend = + def + { users = [alice], + -- We need valid invitations or the user gets deleted by + -- our drive-by cleanup job in the interprter. + -- FUTUREWORK: Remove this if we remove the enqueueDeletion from getAccountsByImpl + invitations = + Map.singleton + (teamId, invitationInfo.invitationId) + ( invitationInfo + { InvitationStore.email = email, + InvitationStore.teamId = teamId + } + ) + } + result = + runNoFederationStack localBackend Nothing config $ + getAccountsBy getBy + in result === [mkUserFromStored localDomain locale alice] + + prop "GetBy email does not filter by pending, missing identity or expired invitations" $ + \(alice' :: StoredUser) email localDomain visibility locale -> + let config = UserSubsystemConfig visibility locale True 100 + alice = alice' {email = Just email} + localBackend = + def + { users = [alice], + userKeys = Map.singleton (mkEmailKey email) alice.id + } + result = + runNoFederationStack localBackend Nothing config $ + getAccountsByEmailNoFilter (toLocalUnsafe localDomain [email]) + in result === [mkUserFromStored localDomain locale alice] + + prop "GetBy userId does not return missing identity users, pending invitation off" $ + \(NotPendingEmptyIdentityStoredUser alice) localDomain visibility locale -> + let config = UserSubsystemConfig visibility locale True 100 + getBy = + toLocalUnsafe localDomain $ + def + { getByUserId = [alice.id], + includePendingInvitations = NoPendingInvitations + } + localBackend = def {users = [alice]} + result = + runNoFederationStack localBackend Nothing config $ + getAccountsBy getBy + in result === [] + + prop "GetBy userId does not return missing identity users, pending invtation on" $ + \(NotPendingEmptyIdentityStoredUser alice) localDomain visibility locale -> + let config = UserSubsystemConfig visibility locale True 100 + getBy = + toLocalUnsafe localDomain $ + def + { getByUserId = [alice.id], + includePendingInvitations = WithPendingInvitations + } + localBackend = def {users = [alice]} + result = + runNoFederationStack localBackend Nothing config $ + getAccountsBy getBy + in result === [] + + prop "GetBy pending user by id works if there is a valid invitation" $ + \(PendingNotEmptyIdentityStoredUser alice') (email :: EmailAddress) teamId (invitationInfo :: StoredInvitation) localDomain visibility locale -> + let config = UserSubsystemConfig visibility locale True 100 + emailKey = mkEmailKey email + getBy = + toLocalUnsafe localDomain $ + def + { getByUserId = [alice.id], + includePendingInvitations = WithPendingInvitations + } + localBackend = + def + { users = [alice], + userKeys = Map.singleton emailKey alice.id, + invitations = + Map.singleton + (teamId, invitationInfo.invitationId) + ( invitationInfo + { InvitationStore.email = email, + InvitationStore.teamId = teamId + } + ) + } + alice = alice' {email = Just email, teamId = Just teamId} + result = + runNoFederationStack localBackend Nothing config $ + getAccountsBy getBy + in result === [mkUserFromStored localDomain locale alice] + + prop "GetBy pending user by id fails if there is no valid invitation" $ + \(PendingNotEmptyIdentityStoredUser alice') (email :: EmailAddress) teamId localDomain visibility locale -> + let config = UserSubsystemConfig visibility locale True 100 + emailKey = mkEmailKey email + getBy = + toLocalUnsafe localDomain $ + def + { getByUserId = [alice.id], + includePendingInvitations = WithPendingInvitations + } + localBackend = + def + { users = [alice], + userKeys = Map.singleton emailKey alice.id + } + alice = alice' {email = Just email, teamId = Just teamId} + result = + runNoFederationStack localBackend Nothing config $ + getAccountsBy getBy + in result === [] + + prop "GetBy pending user handle id works if there is a valid invitation" $ + \(PendingNotEmptyIdentityStoredUser alice') (email :: EmailAddress) handl teamId (invitationInfo :: StoredInvitation) localDomain visibility locale -> + let config = UserSubsystemConfig visibility locale True 100 + emailKey = mkEmailKey email + getBy = + toLocalUnsafe localDomain $ + def + { getByHandle = [handl], + includePendingInvitations = WithPendingInvitations + } + localBackend = + def + { users = [alice], + userKeys = Map.singleton emailKey alice.id, + invitations = + Map.singleton + (teamId, invitationInfo.invitationId) + ( invitationInfo + { InvitationStore.email = email, + InvitationStore.teamId = teamId + } + ) + } + alice = + alice' + { email = Just email, + teamId = Just teamId, + handle = Just handl + } + result = + runNoFederationStack localBackend Nothing config $ + getAccountsBy getBy + in result === [mkUserFromStored localDomain locale alice] + + prop "GetBy pending user by handle fails if there is no valid invitation" $ + \(PendingNotEmptyIdentityStoredUser alice') (email :: EmailAddress) handl teamId localDomain visibility locale -> + let config = UserSubsystemConfig visibility locale True 100 + emailKey = mkEmailKey email + getBy = + toLocalUnsafe localDomain $ + def + { getByHandle = [handl], + includePendingInvitations = WithPendingInvitations + } + localBackend = + def + { users = [alice], + userKeys = Map.singleton emailKey alice.id + } + alice = + alice' + { email = Just email, + teamId = Just teamId, + handle = Just handl + } + result = + runNoFederationStack localBackend Nothing config $ + getAccountsBy getBy + in result === [] + describe "user managed by scim doesn't allow certain update operations, but allows others" $ do prop "happy" $ \(NotPendingStoredUser alice) localDomain update config -> @@ -280,6 +582,7 @@ spec = describe "UserSubsystem.Interpreter" do profileErr :: Either UserSubsystemError (Maybe UserProfile) = run . runErrorUnsafe + . runErrorUnsafe @AuthenticationSubsystemError . runError $ interpretNoFederationStack localBackend Nothing def config do updateUserProfile lusr Nothing UpdateOriginWireClient update {name = Nothing, locale = Nothing} @@ -294,6 +597,7 @@ spec = describe "UserSubsystem.Interpreter" do profileErr :: Either UserSubsystemError (Maybe UserProfile) = run . runErrorUnsafe + . runErrorUnsafe @AuthenticationSubsystemError . runError $ interpretNoFederationStack localBackend Nothing def config do updateUserProfile lusr Nothing UpdateOriginWireClient def {name = Just name} @@ -308,6 +612,7 @@ spec = describe "UserSubsystem.Interpreter" do profileErr :: Either UserSubsystemError (Maybe UserProfile) = run . runErrorUnsafe + . runErrorUnsafe @AuthenticationSubsystemError . runError $ interpretNoFederationStack localBackend Nothing def config do updateUserProfile lusr Nothing UpdateOriginWireClient def {locale = Just locale} @@ -323,10 +628,23 @@ spec = describe "UserSubsystem.Interpreter" do profileErr :: Either UserSubsystemError (Maybe UserProfile) = run . runErrorUnsafe + . runErrorUnsafe @AuthenticationSubsystemError . runError - $ interpretNoFederationStack localBackend Nothing def {afcMlsE2EId = setStatus FeatureStatusEnabled defFeatureStatus} config do - updateUserProfile lusr Nothing UpdateOriginScim (def {name = Just newName}) - getUserProfile lusr (tUntagged lusr) + $ interpretNoFederationStack + localBackend + Nothing + ( npUpdate + ( def + { status = FeatureStatusEnabled + } :: + LockableFeature MlsE2EIdConfig + ) + def + ) + config + do + updateUserProfile lusr Nothing UpdateOriginScim (def {name = Just newName}) + getUserProfile lusr (tUntagged lusr) in profileErr === Left UserSubsystemDisplayNameManagedByScim prop @@ -373,6 +691,7 @@ spec = describe "UserSubsystem.Interpreter" do let res :: Either UserSubsystemError () res = run . runErrorUnsafe + . runErrorUnsafe @AuthenticationSubsystemError . runError $ interpretNoFederationStack localBackend Nothing def config do updateHandle (toLocalUnsafe domain alice.id) Nothing UpdateOriginWireClient (fromHandle newHandle) @@ -382,10 +701,11 @@ spec = describe "UserSubsystem.Interpreter" do prop "Updating handles succeeds when UpdateOriginScim" - \(alice, ssoId, email :: Maybe Email, fromHandle -> newHandle, domain, config) -> + \(alice, ssoId, email :: Maybe EmailAddress, fromHandle -> newHandle, domain, config) -> not (isBlacklistedHandle (fromJust (parseHandle newHandle))) ==> let res :: Either UserSubsystemError () = run . runErrorUnsafe + . runErrorUnsafe @AuthenticationSubsystemError . runError $ interpretNoFederationStack localBackend Nothing def config do updateHandle (toLocalUnsafe domain alice.id) Nothing UpdateOriginScim newHandle @@ -408,6 +728,7 @@ spec = describe "UserSubsystem.Interpreter" do (isJust storedUser.identity && not (isBlacklistedHandle newHandle)) ==> let updateResult :: Either UserSubsystemError () = run . runErrorUnsafe + . runErrorUnsafe @AuthenticationSubsystemError . runError $ interpretNoFederationStack (def {users = [storedUser]}) Nothing def config do let luid = toLocalUnsafe dom storedUser.id @@ -421,6 +742,7 @@ spec = describe "UserSubsystem.Interpreter" do isJust storedUser.identity ==> let updateResult :: Either UserSubsystemError () = run . runErrorUnsafe + . runErrorUnsafe @AuthenticationSubsystemError . runError $ interpretNoFederationStack localBackend Nothing def config do let luid = toLocalUnsafe dom storedUser.id @@ -461,15 +783,13 @@ spec = describe "UserSubsystem.Interpreter" do userKeys = Map.singleton userKey storedUser.id } retrievedUser = - run - . runErrorUnsafe - . runErrorUnsafe @UserSubsystemError + runAllErrorsUnsafe . interpretNoFederationStack localBackend Nothing def config $ getLocalUserAccountByUserKey (toLocalUnsafe localDomain userKey) - in retrievedUser === Just (mkAccountFromStored localDomain config.defaultLocale storedUser) + in retrievedUser === Just (mkUserFromStored localDomain config.defaultLocale storedUser) prop "doesn't get users if they are not indexed by the UserKeyStore" $ - \(config :: UserSubsystemConfig) (localDomain :: Domain) (storedUserNoEmail :: StoredUser) (email :: Email) -> + \(config :: UserSubsystemConfig) (localDomain :: Domain) (storedUserNoEmail :: StoredUser) (email :: EmailAddress) -> let localBackend = def { users = [storedUser], @@ -477,9 +797,7 @@ spec = describe "UserSubsystem.Interpreter" do } storedUser = storedUserNoEmail {email = Just email} retrievedUser = - run - . runErrorUnsafe - . runErrorUnsafe @UserSubsystemError + runAllErrorsUnsafe . interpretNoFederationStack localBackend Nothing def config $ getLocalUserAccountByUserKey (toLocalUnsafe localDomain (mkEmailKey email)) in retrievedUser === Nothing @@ -492,9 +810,7 @@ spec = describe "UserSubsystem.Interpreter" do userKeys = Map.singleton userKey nonExistentUserId } retrievedUser = - run - . runErrorUnsafe - . runErrorUnsafe @UserSubsystemError + runAllErrorsUnsafe . interpretNoFederationStack localBackend Nothing def config $ getLocalUserAccountByUserKey (toLocalUnsafe localDomain userKey) in retrievedUser === Nothing diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index a603df5d43f..d9000793018 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -65,14 +65,18 @@ common common-all library import: common-all - ghc-options: -fplugin=Polysemy.Plugin -fplugin=TransitiveAnns.Plugin + ghc-options: -fplugin=Polysemy.Plugin -- cabal-fmt: expand src exposed-modules: + Wire.ActivationCodeStore + Wire.ActivationCodeStore.Cassandra Wire.AuthenticationSubsystem Wire.AuthenticationSubsystem.Error Wire.AuthenticationSubsystem.Interpreter Wire.AWS + Wire.BlockListStore + Wire.BlockListStore.Cassandra Wire.DeleteQueue Wire.DeleteQueue.InMemory Wire.EmailSending @@ -82,13 +86,24 @@ library Wire.EmailSubsystem.Interpreter Wire.EmailSubsystem.Template Wire.Error + Wire.Events Wire.FederationAPIAccess Wire.FederationAPIAccess.Interpreter + Wire.FederationConfigStore + Wire.FederationConfigStore.Cassandra Wire.GalleyAPIAccess Wire.GalleyAPIAccess.Rpc Wire.GundeckAPIAccess Wire.HashPassword + Wire.IndexedUserStore + Wire.IndexedUserStore.Bulk + Wire.IndexedUserStore.Bulk.ElasticSearch + Wire.IndexedUserStore.ElasticSearch + Wire.IndexedUserStore.MigrationStore + Wire.IndexedUserStore.MigrationStore.ElasticSearch Wire.InternalEvent + Wire.InvitationStore + Wire.InvitationStore.Cassandra Wire.NotificationSubsystem Wire.NotificationSubsystem.Interpreter Wire.ParseException @@ -96,15 +111,25 @@ library Wire.PasswordResetCodeStore.Cassandra Wire.PasswordStore Wire.PasswordStore.Cassandra + Wire.PropertyStore + Wire.PropertyStore.Cassandra + Wire.PropertySubsystem + Wire.PropertySubsystem.Interpreter Wire.Rpc Wire.SessionStore Wire.SessionStore.Cassandra Wire.StoredUser - Wire.UserEvents + Wire.TeamInvitationSubsystem + Wire.TeamInvitationSubsystem.Error + Wire.TeamInvitationSubsystem.Interpreter Wire.UserKeyStore Wire.UserKeyStore.Cassandra + Wire.UserSearch.Metrics + Wire.UserSearch.Migration + Wire.UserSearch.Types Wire.UserStore Wire.UserStore.Cassandra + Wire.UserStore.IndexUser Wire.UserStore.Unique Wire.UserSubsystem Wire.UserSubsystem.Error @@ -124,12 +149,16 @@ library , amazonka-core , amazonka-ses , async + , attoparsec , base , base16-bytestring , bilge + , bloodhound , bytestring , bytestring-conversion + , case-insensitive , cassandra-util + , conduit , containers , cql , crypton @@ -140,7 +169,6 @@ library , exceptions , extended , extra - , gundeck-types , HaskellNet , HaskellNet-SSL , HsOpenSSL @@ -161,28 +189,34 @@ library , polysemy-plugin , polysemy-time , polysemy-wire-zoo + , prometheus-client , QuickCheck , resource-pool , resourcet , retry + , saml2-web-sso + , schema-profunctor , servant , servant-client-core , stomp-queue , template , text + , text-icu-translit , time , time-out , time-units , tinylog , transformers - , transitive-anns , types-common , unliftio , unordered-containers + , uri-bytestring , uuid , wai-utilities , wire-api , wire-api-federation + , wire-otel + , witherable default-language: GHC2021 @@ -200,21 +234,29 @@ test-suite wire-subsystems-tests Wire.AuthenticationSubsystem.InterpreterSpec Wire.MiniBackend Wire.MockInterpreters + Wire.MockInterpreters.ActivationCodeStore + Wire.MockInterpreters.BlockListStore Wire.MockInterpreters.EmailSubsystem Wire.MockInterpreters.Error + Wire.MockInterpreters.Events + Wire.MockInterpreters.FederationConfigStore Wire.MockInterpreters.GalleyAPIAccess Wire.MockInterpreters.HashPassword + Wire.MockInterpreters.IndexedUserStore + Wire.MockInterpreters.InvitationStore Wire.MockInterpreters.Now Wire.MockInterpreters.PasswordResetCodeStore Wire.MockInterpreters.PasswordStore + Wire.MockInterpreters.PropertyStore Wire.MockInterpreters.Random Wire.MockInterpreters.SessionStore - Wire.MockInterpreters.UserEvents Wire.MockInterpreters.UserKeyStore Wire.MockInterpreters.UserStore Wire.MockInterpreters.UserSubsystem Wire.MockInterpreters.VerificationCodeStore Wire.NotificationSubsystem.InterpreterSpec + Wire.PropertySubsystem.InterpreterSpec + Wire.UserSearch.TypesSpec Wire.UserStoreSpec Wire.UserSubsystem.InterpreterSpec Wire.VerificationCodeSubsystem.InterpreterSpec @@ -226,12 +268,12 @@ test-suite wire-subsystems-tests , base , bilge , bytestring + , cassandra-util , containers , crypton , data-default , errors , extended - , gundeck-types , hspec , imports , iso639 @@ -247,6 +289,7 @@ test-suite wire-subsystems-tests , QuickCheck , quickcheck-instances , random + , scientific , servant-client-core , streaming-commons , string-conversions diff --git a/libs/zauth/src/Data/ZAuth/Creation.hs b/libs/zauth/src/Data/ZAuth/Creation.hs index f7dfda93d17..54fec4f357d 100644 --- a/libs/zauth/src/Data/ZAuth/Creation.hs +++ b/libs/zauth/src/Data/ZAuth/Creation.hs @@ -39,13 +39,12 @@ module Data.ZAuth.Creation legalHoldUserToken, -- * Generic - withIndex, newToken, renewToken, ) where -import Control.Lens hiding (withIndex) +import Control.Lens import Control.Monad.Catch (MonadCatch, MonadThrow) import Data.ByteString qualified as Strict import Data.ByteString.Builder (toLazyByteString) @@ -90,13 +89,6 @@ runCreate z k m = do error "runCreate: Key index out of range." runReaderT (zauth m) (z {keyIdx = k}) -withIndex :: Int -> Create a -> Create a -withIndex k m = Create $ do - e <- ask - when (k < 1 || k > Vec.length (zSign e)) $ - error "withIndex: Key index out of range." - local (const (e {keyIdx = k})) (zauth m) - userToken :: Integer -> UUID -> Maybe Text -> Word32 -> Create (Token User) userToken dur usr cli rnd = do d <- expiry dur diff --git a/libs/zauth/test/ZAuth.hs b/libs/zauth/test/ZAuth.hs index db94845d04c..80545f884ae 100644 --- a/libs/zauth/test/ZAuth.hs +++ b/libs/zauth/test/ZAuth.hs @@ -56,7 +56,7 @@ tests = do ], testGroup "Signing and Verifying" - [ testCase "testExpired - expired" (runCreate z 1 $ testExpired v), + [ testCase "expired" (runCreate z 1 $ testExpired v), testCase "not expired" (runCreate z 2 $ testNotExpired v), testCase "signed access-token is valid" (runCreate z 3 $ testSignAndVerify v) ], @@ -94,6 +94,7 @@ testNotExpired p = do liftIO $ assertBool "testNotExpired: validation failed" (isRight x) -- The testExpired test conforms to the following testing standards: +-- @SF.Channel @TSFI.RESTfulAPI @TSFI.NTP @S2 @S3 -- -- Using an expired access token should fail testExpired :: V.Env -> Create () @@ -104,6 +105,8 @@ testExpired p = do x <- liftIO $ runValidate p $ check t liftIO $ Left Expired @=? x +-- @END + testSignAndVerify :: V.Env -> Create () testSignAndVerify p = do u <- liftIO nextRandom diff --git a/nix/default.nix b/nix/default.nix index 02c0d9e01a7..f0631506f6a 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -28,7 +28,7 @@ let docsPkgs = [ pkgs.plantuml pkgs.texlive.combined.scheme-full - (pkgs.python3.withPackages + (pkgs.python310.withPackages (ps: with ps; [ myst-parser rst2pdf diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 3cc68c3effc..fbf3d3ca699 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -63,14 +63,6 @@ let # maintained by us # ---------------- - transitive-anns = { - src = fetchgit { - url = "https://github.com/wireapp/transitive-anns"; - rev = "5e0cad1f411a0c92e6445404c205ddd4a0229c4d"; - hash = "sha256-/P4KJ4yZgqhZhzmg1GcE+Ti4kdsWUQX8q++RhgCUDKI="; - }; - }; - cryptobox-haskell = { src = fetchgit { url = "https://github.com/wireapp/cryptobox-haskell"; @@ -270,6 +262,35 @@ let }; }; + # this contains an important fix to the initialization of the window size + # and should be switched to upstream as soon as we can + # version = "5.2.5"; + # This patch also includes suppressing ConnectionIsClosed + http2 = { + src = fetchgit { + url = "https://github.com/wireapp/http2"; + rev = "45653e3caab0642e539fab2681cb09402aae29ca"; + hash = "sha256-L90PQtDw/JFwyltSVFvmfjTAb0ZLhFt9Hl0jbzn+cFQ="; + }; + }; + + # hs-opentelemetry-* has not been released for a while on hackage + hs-opentelemetry = { + src = fetchgit { + url = "https://github.com/iand675/hs-opentelemetry"; + rev = "0b3c854a88113fc18df8561202a76357e593a294"; + hash = "sha256-N5FzKz6T1sE9xffGCeWa+iTW8a1GCLsy2TlAjzIed34="; + }; + packages = { + hs-opentelemetry-sdk = "sdk"; + hs-opentelemetry-api = "api"; + hs-opentelemetry-propagator-datadog = "propagators/datadog"; + hs-opentelemetry-instrumentation-http-client = "instrumentation/http-client"; + hs-opentelemetry-instrumentation-wai = "instrumentation/wai"; + hs-opentelemetry-exporter-otlp = "exporters/otlp"; + }; + }; + }; hackagePins = { @@ -281,14 +302,6 @@ let }; # start pinned dependencies for http2 - - # this contains an important fix to the initialization of the window size - # and should be switched to upstream as soon as we can - http2 = { - version = "5.2.5"; - sha256 = "sha256-FCd4lPydwWqm2lrhgYtPW+BuXGqmmA8KFrB87SYEowY="; - }; - http-semantics = { version = "0.1.2"; sha256 = "sha256-S4rGBCIKVPpLPumLcVzrPONrbWm8VBizqxI3dXNIfr0="; @@ -313,13 +326,6 @@ let }; # end pinned dependencies for http2 - # pinned for warp - warp-tls = { - version = "3.4.5"; - sha256 = "sha256-3cDi/+n7wHfcWT/iFWAsGdLYXtKYXmvzolDt+ACJnaM="; - }; - # end pinned for warp - # PR: https://github.com/wireapp/wire-server/pull/4027 HsOpenSSL = { version = "0.11.7.7"; diff --git a/nix/local-haskell-packages.nix b/nix/local-haskell-packages.nix index 133fcd9afae..414a5443410 100644 --- a/nix/local-haskell-packages.nix +++ b/nix/local-haskell-packages.nix @@ -12,7 +12,6 @@ dns-util = hself.callPackage ../libs/dns-util/default.nix { inherit gitignoreSource; }; extended = hself.callPackage ../libs/extended/default.nix { inherit gitignoreSource; }; galley-types = hself.callPackage ../libs/galley-types/default.nix { inherit gitignoreSource; }; - gundeck-types = hself.callPackage ../libs/gundeck-types/default.nix { inherit gitignoreSource; }; hscim = hself.callPackage ../libs/hscim/default.nix { inherit gitignoreSource; }; http2-manager = hself.callPackage ../libs/http2-manager/default.nix { inherit gitignoreSource; }; imports = hself.callPackage ../libs/imports/default.nix { inherit gitignoreSource; }; @@ -31,6 +30,7 @@ wire-api-federation = hself.callPackage ../libs/wire-api-federation/default.nix { inherit gitignoreSource; }; wire-api = hself.callPackage ../libs/wire-api/default.nix { inherit gitignoreSource; }; wire-message-proto-lens = hself.callPackage ../libs/wire-message-proto-lens/default.nix { inherit gitignoreSource; }; + wire-otel = hself.callPackage ../libs/wire-otel/default.nix { inherit gitignoreSource; }; wire-subsystems = hself.callPackage ../libs/wire-subsystems/default.nix { inherit gitignoreSource; }; zauth = hself.callPackage ../libs/zauth/default.nix { inherit gitignoreSource; }; background-worker = hself.callPackage ../services/background-worker/default.nix { inherit gitignoreSource; }; @@ -52,7 +52,7 @@ repair-brig-clients-table = hself.callPackage ../tools/db/repair-brig-clients-table/default.nix { inherit gitignoreSource; }; repair-handles = hself.callPackage ../tools/db/repair-handles/default.nix { inherit gitignoreSource; }; service-backfill = hself.callPackage ../tools/db/service-backfill/default.nix { inherit gitignoreSource; }; - fedcalls = hself.callPackage ../tools/fedcalls/default.nix { inherit gitignoreSource; }; + team-info = hself.callPackage ../tools/db/team-info/default.nix { inherit gitignoreSource; }; mlsstats = hself.callPackage ../tools/mlsstats/default.nix { inherit gitignoreSource; }; rabbitmq-consumer = hself.callPackage ../tools/rabbitmq-consumer/default.nix { inherit gitignoreSource; }; rex = hself.callPackage ../tools/rex/default.nix { inherit gitignoreSource; }; diff --git a/nix/manual-overrides.nix b/nix/manual-overrides.nix index d2d6a1baef8..54a5ce23fbf 100644 --- a/nix/manual-overrides.nix +++ b/nix/manual-overrides.nix @@ -1,4 +1,4 @@ -{ libsodium, protobuf, hlib, mls-test-cli, fetchurl, curl, fetchpatch, ... }: +{ libsodium, protobuf, hlib, mls-test-cli, fetchurl, curl, ... }: # FUTUREWORK: Figure out a way to detect if some of these packages are not # actually marked broken, so we can cleanup this file on every nixpkgs bump. hself: hsuper: { @@ -23,8 +23,6 @@ hself: hsuper: { # these are okay, the only issue is that the compiler underlines # errors differently than before singletons-base = hlib.markUnbroken (hlib.dontCheck hsuper.singletons-base); - # one of the tests is flaky - transitive-anns = hlib.dontCheck hsuper.transitive-anns; # Tests require a running redis hedis = hlib.dontCheck hsuper.hedis; @@ -38,7 +36,7 @@ hself: hsuper: { bytestring-arbitrary = hlib.markUnbroken (hlib.doJailbreak hsuper.bytestring-arbitrary); lens-datetime = hlib.markUnbroken (hlib.doJailbreak hsuper.lens-datetime); - # the libsodium haskell library is incompatible with the new version of the libsodium c library + # the libsodium haskell library is incompatible with the new version of the libsodium c library # that nixpkgs has - this downgrades libsodium from 1.0.19 to 1.0.18 libsodium = hlib.markUnbroken (hlib.addPkgconfigDepend hsuper.libsodium ( libsodium.overrideAttrs (old: @@ -55,7 +53,6 @@ hself: hsuper: { # depend on an old version of hedgehog polysemy-test = hlib.markUnbroken (hlib.doJailbreak hsuper.polysemy-test); - polysemy-conc = hlib.markUnbroken (hlib.doJailbreak hsuper.polysemy-conc); # ------------------------------------ # okay but marked broken (nixpkgs bug) @@ -67,20 +64,26 @@ hself: hsuper: { # version overrides # (these are fine but will probably need to be adjusted in a future nixpkgs update) # ----------------- - tls = hsuper.tls_2_0_5; - tls-session-manager = hsuper.tls-session-manager_0_0_5; + tls = hsuper.tls_2_1_0; + tls-session-manager = hsuper.tls-session-manager_0_0_6; + crypton-connection = hsuper.crypton-connection_0_4_1; # older version doesn't allow tls 2.1 + amqp = hlib.dontCheck hsuper.amqp_0_23_0; # older version doesn't allow cryton-connection 0.4.1, this one has broken tests # warp requires curl in its testsuite warp = hlib.addTestToolDepends hsuper.warp [ curl ]; + # cabal multirepl requires Cabal 3.12 + Cabal = hsuper.Cabal_3_12_1_0; + Cabal-syntax = hsuper.Cabal-syntax_3_12_1_0; + # ----------------- # flags and patches # (these are fine) # ----------------- cryptostore = hlib.addBuildDepends (hlib.dontCheck (hlib.appendConfigureFlags hsuper.cryptostore [ "-fuse_crypton" ])) [ hself.crypton hself.crypton-x509 hself.crypton-x509-validation ]; - # Make hoogle static to reduce size of the hoogle image - hoogle = hlib.justStaticExecutables hsuper.hoogle; + # doJailbreak because upstreams requires a specific crypton-connection version we don't have + hoogle = hlib.justStaticExecutables (hlib.doJailbreak (hlib.dontCheck (hsuper.hoogle))); http2-manager = hlib.enableCabalFlag hsuper.http2-manager "-f-test-trailing-dot"; sodium-crypto-sign = hlib.addPkgconfigDepend hsuper.sodium-crypto-sign libsodium.dev; types-common-journal = hlib.addBuildTool hsuper.types-common-journal protobuf; diff --git a/nix/overlay-docs.nix b/nix/overlay-docs.nix index c97cbe66e6a..210822d70dc 100644 --- a/nix/overlay-docs.nix +++ b/nix/overlay-docs.nix @@ -1,5 +1,5 @@ self: super: rec { - python3 = super.python3.override { + python310 = super.python310.override { packageOverrides = pself: psuper: { rst2pdf = pself.callPackage ./pkgs/python-docs/rst2pdf.nix { }; sphinx-multiversion = pself.callPackage ./pkgs/python-docs/sphinx-multiversion.nix { }; @@ -9,5 +9,5 @@ self: super: rec { }; }; - python3Packages = python3.pkgs; + python310Packages = python310.pkgs; } diff --git a/nix/sources.json b/nix/sources.json index 0abe53ae006..8bfa8cf9928 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -5,10 +5,10 @@ "homepage": "https://github.com/NixOS/nixpkgs", "owner": "NixOS", "repo": "nixpkgs", - "rev": "4a3fc4cf736b7d2d288d7a8bf775ac8d4c0920b4", - "sha256": "1ibmc6iijim53bpi1wc1b295l579wzxgs8ynmsi0ldgjrxhgli1a", + "rev": "4f31540079322e6013930b5b2563fd10f96917f0", + "sha256": "12748r3h44hy3a41slm5hcihn1nhrxjlgp75qz6iwzazkxnclx00", "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/4a3fc4cf736b7d2d288d7a8bf775ac8d4c0920b4.tar.gz", + "url": "https://github.com/NixOS/nixpkgs/archive/4f31540079322e6013930b5b2563fd10f96917f0.tar.gz", "url_template": "https://github.com///archive/.tar.gz" } } diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 6fafadb5efa..370397a809f 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -86,6 +86,7 @@ let integration = [ "integration" ]; rabbitmq-consumer = [ "rabbitmq-consumer" ]; test-stats = [ "test-stats" ]; + team-info = [ "team-info" ]; }; inherit (lib) attrsets; @@ -292,6 +293,7 @@ let pkgs.nginz pkgs.mls-test-cli pkgs.awscli2 + pkgs.vacuum-go integration-dynamic-backends-db-schemas integration-dynamic-backends-brig-index integration-dynamic-backends-ses @@ -413,6 +415,7 @@ let pkgs.nixpkgs-fmt pkgs.openssl pkgs.ormolu + pkgs.vacuum-go pkgs.shellcheck pkgs.treefmt pkgs.gawk @@ -477,7 +480,7 @@ let out = import ./all-toplevel-derivations.nix { inherit (pkgs) lib; fn = mk; - # more than two takes more than 32GB of RAM, so this is what + # more than two takes more than 32GB of RAM, so this is what # we're limiting ourselves to recursionDepth = 2; keyFilter = k: k != "passthru"; @@ -517,7 +520,7 @@ in pkgs.kind pkgs.netcat pkgs.niv - (hlib.justStaticExecutables pkgs.haskellPackages.apply-refact) + pkgs.haskellPackages.apply-refact (pkgs.python3.withPackages (ps: with ps; [ black @@ -547,7 +550,7 @@ in ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ # linux-only, not strictly required tools pkgs.docker-compose - pkgs.telepresence + (pkgs.telepresence.override { pythonPackages = pkgs.python310Packages; }) ]; }; diff --git a/services/background-worker/src/Wire/BackendNotificationPusher.hs b/services/background-worker/src/Wire/BackendNotificationPusher.hs index f7cfe209ad6..6a6cf2f7f62 100644 --- a/services/background-worker/src/Wire/BackendNotificationPusher.hs +++ b/services/background-worker/src/Wire/BackendNotificationPusher.hs @@ -17,8 +17,10 @@ import Imports import Network.AMQP qualified as Q import Network.AMQP.Extended import Network.AMQP.Lifted qualified as QL -import Network.RabbitMqAdmin +import Network.RabbitMqAdmin hiding (adminClient) +import Network.RabbitMqAdmin qualified as RabbitMqAdmin import Prometheus +import Servant.Client qualified as Servant import System.Logger.Class qualified as Log import UnliftIO import Wire.API.Federation.API @@ -114,7 +116,7 @@ pushNotification runningFlag targetDomain (msg, envelope) = do ceHttp2Manager <- asks http2Manager let ceOriginDomain = notif.ownDomain ceTargetDomain = targetDomain - ceOriginRequestId = fromMaybe (RequestId "N/A") notif.requestId + ceOriginRequestId = fromMaybe (RequestId defRequestId) notif.requestId cveEnv = FederatorClientEnv {..} cveVersion = Just V0 -- V0 is assumed for non-versioned queue messages fcEnv = FederatorClientVersionedEnv {..} @@ -133,7 +135,7 @@ pushNotification runningFlag targetDomain (msg, envelope) = do ceFederator = federator, ceHttp2Manager = manager, ceOriginRequestId = - fromMaybe (RequestId "N/A") . (.requestId) . NE.head $ bundle.notifications + fromMaybe (RequestId defRequestId) . (.requestId) . NE.head $ bundle.notifications } remoteVersions :: Set Int <- liftIO @@ -164,7 +166,7 @@ pushNotification runningFlag targetDomain (msg, envelope) = do ceHttp2Manager <- asks http2Manager let ceOriginDomain = notif.ownDomain ceTargetDomain = targetDomain - ceOriginRequestId = fromMaybe (RequestId "N/A") notif.requestId + ceOriginRequestId = fromMaybe (RequestId defRequestId) notif.requestId cveEnv = FederatorClientEnv {..} fcEnv = FederatorClientVersionedEnv {..} sendNotificationIgnoringVersionMismatch fcEnv notif.targetComponent notif.path notif.body @@ -197,8 +199,8 @@ pairedMaximumOn f = maximumBy (compare `on` snd) . map (id &&& f) -- FUTUREWORK: Recosider using 1 channel for many consumers. It shouldn't matter -- for a handful of remote domains. -- Consumers is passed in explicitly so that cleanup code has a reference to the consumer tags. -startPusher :: IORef (Map Domain (Q.ConsumerTag, MVar ())) -> Q.Channel -> AppT IO () -startPusher consumersRef chan = do +startPusher :: RabbitMqAdmin.AdminAPI (Servant.AsClientT IO) -> IORef (Map Domain (Q.ConsumerTag, MVar ())) -> Q.Channel -> AppT IO () +startPusher adminClient consumersRef chan = do -- This ensures that we receive notifications 1 by 1 which ensures they are -- delivered in order. markAsWorking BackendNotificationPusher @@ -221,7 +223,7 @@ startPusher consumersRef chan = do ] $ forever $ do - remotes <- getRemoteDomains + remotes <- getRemoteDomains adminClient ensureConsumers consumersRef chan remotes threadDelay timeBeforeNextRefresh @@ -259,8 +261,8 @@ ensureConsumer consumers chan domain = do -- let us come down this path if there is an old consumer. liftIO $ forM_ oldTag $ Q.cancelConsumer chan . fst -getRemoteDomains :: AppT IO [Domain] -getRemoteDomains = do +getRemoteDomains :: RabbitMqAdmin.AdminAPI (Servant.AsClientT IO) -> AppT IO [Domain] +getRemoteDomains adminClient = do -- Jittered exponential backoff with 10ms as starting delay and 60s as max -- cumulative delay. When this is reached, the operation fails. -- @@ -279,9 +281,8 @@ getRemoteDomains = do where go :: AppT IO [Domain] go = do - client <- asks rabbitmqAdminClient vhost <- asks rabbitmqVHost - queues <- liftIO $ listQueuesByVHost client vhost + queues <- liftIO $ listQueuesByVHost adminClient vhost let notifQueuesSuffixes = mapMaybe (\q -> Text.stripPrefix "backend-notifications." q.name) queues catMaybes <$> traverse (\d -> either (\e -> logInvalidDomain d e >> pure Nothing) (pure . Just) $ mkDomain d) notifQueuesSuffixes logInvalidDomain d e = @@ -290,7 +291,7 @@ getRemoteDomains = do . Log.field "queue" ("backend-notifications." <> d) . Log.field "error" e -startWorker :: RabbitMqAdminOpts -> AppT IO (IORef (Maybe Q.Channel), IORef (Map Domain (Q.ConsumerTag, MVar ()))) +startWorker :: AmqpEndpoint -> AppT IO (IORef (Maybe Q.Channel), IORef (Map Domain (Q.ConsumerTag, MVar ()))) startWorker rabbitmqOpts = do env <- ask -- These are used in the POSIX signal handlers, so we need to make @@ -304,22 +305,28 @@ startWorker rabbitmqOpts = do clearRefs = do atomicWriteIORef chanRef Nothing atomicWriteIORef consumersRef mempty - -- We can fire and forget this thread because it keeps respawning itself using the 'onConnectionClosedHandler'. - void $ - async $ - liftIO $ - openConnectionWithRetries env.logger (demoteOpts rabbitmqOpts) $ - RabbitMqHooks - { -- The exception handling in `openConnectionWithRetries` won't open a new - -- connection on an explicit close call. - onNewChannel = \chan -> do - atomicWriteIORef chanRef $ pure chan - runAppT env $ startPusher consumersRef chan, - onChannelException = \_ -> do - clearRefs - runAppT env $ markAsNotWorking BackendNotificationPusher, - onConnectionClose = do - clearRefs - runAppT env $ markAsNotWorking BackendNotificationPusher - } + case env.rabbitmqAdminClient of + Nothing -> + Log.info $ + Log.msg $ + Log.val "RabbitMQ admin client not available, skipping backend notification pusher." + Just client -> + -- We can fire and forget this thread because it keeps respawning itself using the 'onConnectionClosedHandler'. + void $ + async $ + liftIO $ + openConnectionWithRetries env.logger rabbitmqOpts $ + RabbitMqHooks + { -- The exception handling in `openConnectionWithRetries` won't open a new + -- connection on an explicit close call. + onNewChannel = \chan -> do + atomicWriteIORef chanRef $ pure chan + runAppT env $ startPusher client consumersRef chan, + onChannelException = \_ -> do + clearRefs + runAppT env $ markAsNotWorking BackendNotificationPusher, + onConnectionClose = do + clearRefs + runAppT env $ markAsNotWorking BackendNotificationPusher + } pure (chanRef, consumersRef) diff --git a/services/background-worker/src/Wire/BackgroundWorker.hs b/services/background-worker/src/Wire/BackgroundWorker.hs index 3a9bc8e298a..17b45f71ecd 100644 --- a/services/background-worker/src/Wire/BackgroundWorker.hs +++ b/services/background-worker/src/Wire/BackgroundWorker.hs @@ -8,6 +8,7 @@ import Data.Metrics.Servant qualified as Metrics import Data.Text qualified as T import Imports import Network.AMQP qualified as Q +import Network.AMQP.Extended (demoteOpts) import Network.Wai.Utilities.Server import Servant import Servant.Server.Generic @@ -21,7 +22,8 @@ import Wire.BackgroundWorker.Options run :: Opts -> IO () run opts = do env <- mkEnv opts - (notifChanRef, notifConsumersRef) <- runAppT env $ BackendNotificationPusher.startWorker opts.rabbitmq + let amqpEP = either id demoteOpts opts.rabbitmq.unRabbitMqOpts + (notifChanRef, notifConsumersRef) <- runAppT env $ BackendNotificationPusher.startWorker amqpEP let -- cleanup will run in a new thread when the signal is caught, so we need to use IORefs and -- specific exception types to message threads to clean up l = logger env @@ -48,7 +50,7 @@ run opts = do -- Close the channel. `extended` will then close the connection, flushing messages to the server. Log.info l $ Log.msg $ Log.val "Closing RabbitMQ channel" Q.closeChannel chan - let server = defaultServer (T.unpack $ opts.backgroundWorker._host) opts.backgroundWorker._port env.logger + let server = defaultServer (T.unpack $ opts.backgroundWorker.host) opts.backgroundWorker.port env.logger settings <- newSettings server -- Additional cleanup when shutting down via signals. runSettingsWithCleanup cleanup settings (servantApp env) Nothing diff --git a/services/background-worker/src/Wire/BackgroundWorker/Env.hs b/services/background-worker/src/Wire/BackgroundWorker/Env.hs index db968315947..dcf89d56d41 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Env.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Env.hs @@ -31,7 +31,7 @@ data Worker data Env = Env { http2Manager :: Http2Manager, - rabbitmqAdminClient :: RabbitMqAdmin.AdminAPI (Servant.AsClientT IO), + rabbitmqAdminClient :: Maybe (RabbitMqAdmin.AdminAPI (Servant.AsClientT IO)), rabbitmqVHost :: Text, logger :: Logger, federatorInternal :: Endpoint, @@ -66,8 +66,8 @@ mkEnv opts = do responseTimeoutNone (\t -> responseTimeoutMicro $ 1000000 * t) -- seconds to microseconds opts.defederationTimeout - rabbitmqVHost = opts.rabbitmq.vHost - rabbitmqAdminClient <- mkRabbitMqAdminClientEnv opts.rabbitmq + rabbitmqVHost = either (.vHost) (.vHost) opts.rabbitmq.unRabbitMqOpts + rabbitmqAdminClient <- for (rightToMaybe opts.rabbitmq.unRabbitMqOpts) mkRabbitMqAdminClientEnv statuses <- newIORef $ Map.fromList diff --git a/services/background-worker/src/Wire/BackgroundWorker/Options.hs b/services/background-worker/src/Wire/BackgroundWorker/Options.hs index da31c41255a..cdbeb1e5024 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Options.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Options.hs @@ -11,7 +11,7 @@ data Opts = Opts logFormat :: !(Maybe (Last LogFormat)), backgroundWorker :: !Endpoint, federatorInternal :: !Endpoint, - rabbitmq :: !RabbitMqAdminOpts, + rabbitmq :: !RabbitMqOpts, -- | Seconds, Nothing for no timeout defederationTimeout :: Maybe Int, backendNotificationPusher :: BackendNotificationsConfig @@ -37,3 +37,13 @@ data BackendNotificationsConfig = BackendNotificationsConfig deriving (Show, Generic) instance FromJSON BackendNotificationsConfig + +newtype RabbitMqOpts = RabbitMqOpts {unRabbitMqOpts :: Either AmqpEndpoint RabbitMqAdminOpts} + deriving (Show) + +instance FromJSON RabbitMqOpts where + parseJSON v = + RabbitMqOpts + <$> ( (Right <$> parseJSON v) + <|> (Left <$> parseJSON v) + ) diff --git a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs index 6b53ed6e9e3..416a2653f82 100644 --- a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs +++ b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs @@ -67,7 +67,7 @@ spec = do path = "/on-user-deleted-connections", body = RawJson $ Aeson.encode notifContent, bodyVersions = Nothing, - requestId = Just $ RequestId "N/A" + requestId = Just $ RequestId defRequestId } envelope <- newMockEnvelope let msg = @@ -104,7 +104,7 @@ spec = do notifContent <- generate $ ClientRemovedRequest <$> arbitrary <*> arbitrary <*> arbitrary - let bundle = toBundle @'OnClientRemovedTag (RequestId "N/A") origDomain notifContent + let bundle = toBundle @'OnClientRemovedTag (RequestId defRequestId) origDomain notifContent envelope <- newMockEnvelope let msg = Q.newMsg @@ -148,8 +148,8 @@ spec = do } let update0 = conversationUpdateToV0 update let bundle = - toBundle (RequestId "N/A") origDomain update - <> toBundle (RequestId "N/A") origDomain update0 + toBundle (RequestId defRequestId) origDomain update + <> toBundle (RequestId defRequestId) origDomain update0 envelope <- newMockEnvelope let msg = Q.newMsg @@ -158,7 +158,7 @@ spec = do } runningFlag <- newMVar () (env, fedReqs) <- - withTempMockFederator def {versions = [0, 2]} . runTestAppT $ do + withTempMockFederator def {versions = [0, 999999]} . runTestAppT $ do wait =<< pushNotification runningFlag targetDomain (msg, envelope) ask @@ -215,7 +215,7 @@ spec = do path = "/on-user-deleted-connections", body = RawJson $ Aeson.encode notifContent, bodyVersions = Nothing, - requestId = Just $ RequestId "N/A" + requestId = Just $ RequestId defRequestId } envelope <- newMockEnvelope let msg = @@ -270,13 +270,13 @@ spec = do let federatorInternal = Endpoint "localhost" 8097 http2Manager = undefined statuses = undefined - rabbitmqAdminClient = mockRabbitMqAdminClient mockAdmin + rabbitmqAdminClient = Just $ mockRabbitMqAdminClient mockAdmin rabbitmqVHost = "test-vhost" defederationTimeout = responseTimeoutNone backendNotificationsConfig = BackendNotificationsConfig 1000 500000 1000 backendNotificationMetrics <- mkBackendNotificationMetrics - domains <- runAppT Env {..} getRemoteDomains + domains <- runAppT Env {..} $ getRemoteDomains (fromJust rabbitmqAdminClient) domains `shouldBe` map Domain ["foo.example", "bar.example", "baz.example"] readTVarIO mockAdmin.listQueuesVHostCalls `shouldReturn` ["test-vhost"] @@ -287,12 +287,12 @@ spec = do let federatorInternal = Endpoint "localhost" 8097 http2Manager = undefined statuses = undefined - rabbitmqAdminClient = mockRabbitMqAdminClient mockAdmin + rabbitmqAdminClient = Just $ mockRabbitMqAdminClient mockAdmin rabbitmqVHost = "test-vhost" defederationTimeout = responseTimeoutNone backendNotificationsConfig = BackendNotificationsConfig 1000 500000 1000 backendNotificationMetrics <- mkBackendNotificationMetrics - domainsThread <- async $ runAppT Env {..} getRemoteDomains + domainsThread <- async $ runAppT Env {..} $ getRemoteDomains (fromJust rabbitmqAdminClient) -- Wait for first call untilM (readTVarIO mockAdmin.listQueuesVHostCalls >>= \calls -> pure $ not $ null calls) diff --git a/services/background-worker/test/Test/Wire/Util.hs b/services/background-worker/test/Test/Wire/Util.hs index 7c6fbf48aab..57ccf6bf0e1 100644 --- a/services/background-worker/test/Test/Wire/Util.hs +++ b/services/background-worker/test/Test/Wire/Util.hs @@ -9,7 +9,6 @@ import Util.Options (Endpoint (..)) import Wire.BackgroundWorker.Env hiding (federatorInternal) import Wire.BackgroundWorker.Env qualified as E import Wire.BackgroundWorker.Options -import Wire.BackgroundWorker.Util testEnv :: IO Env testEnv = do @@ -34,18 +33,3 @@ runTestAppTWithEnv :: Env -> AppT IO a -> Int -> IO a runTestAppTWithEnv Env {..} app port = do let env = Env {federatorInternal = Endpoint "localhost" (fromIntegral port), ..} runAppT env app - -data FakeEnvelope = FakeEnvelope - { rejections :: IORef [Bool], - acks :: IORef Int - } - -newFakeEnvelope :: IO FakeEnvelope -newFakeEnvelope = - FakeEnvelope - <$> newIORef [] - <*> newIORef 0 - -instance RabbitMQEnvelope FakeEnvelope where - ack e = atomicModifyIORef' e.acks $ \a -> (a + 1, ()) - reject e requeueFlag = atomicModifyIORef' e.rejections $ \r -> (r <> [requeueFlag], ()) diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 49d45527d52..29cc881005c 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -90,7 +90,6 @@ library Brig.API.MLS.KeyPackages.Validation Brig.API.MLS.Util Brig.API.OAuth - Brig.API.Properties Brig.API.Public Brig.API.Public.Swagger Brig.API.Types @@ -108,19 +107,13 @@ library Brig.Data.Activation Brig.Data.Client Brig.Data.Connection - Brig.Data.LoginCode Brig.Data.MLS.KeyPackage Brig.Data.Nonce - Brig.Data.Properties Brig.Data.Types Brig.Data.User Brig.DeleteQueue.Interpreter - Brig.Effects.BlacklistStore - Brig.Effects.BlacklistStore.Cassandra Brig.Effects.ConnectionStore Brig.Effects.ConnectionStore.Cassandra - Brig.Effects.FederationConfigStore - Brig.Effects.FederationConfigStore.Cassandra Brig.Effects.JwtTools Brig.Effects.PublicKeyBundle Brig.Effects.SFT @@ -128,8 +121,6 @@ library Brig.Effects.UserPendingActivationStore.Cassandra Brig.Federation.Client Brig.Index.Eval - Brig.Index.Migrations - Brig.Index.Migrations.Types Brig.Index.Options Brig.Index.Types Brig.InternalEvent.Process @@ -188,166 +179,158 @@ library Brig.Schema.V79_ConnectionRemoteIndex Brig.Schema.V80_KeyPackageCiphersuite Brig.Schema.V81_AddFederationRemoteTeams - Brig.Schema.V_FUTUREWORK + Brig.Schema.V82_DropPhoneColumn + Brig.Schema.V83_AddTextStatus + Brig.Schema.V84_DropTeamInvitationPhone + Brig.Schema.V85_DropUserKeysHashed + Brig.Schema.V86_WriteTimeBumper + Brig.Schema.V87_DropInvitationTables Brig.Team.API - Brig.Team.DB Brig.Team.Email Brig.Team.Template - Brig.Team.Util Brig.Template Brig.User.API.Handle - Brig.User.API.Search Brig.User.Auth Brig.User.Auth.Cookie Brig.User.Auth.Cookie.Limit Brig.User.EJPD Brig.User.Search.Index - Brig.User.Search.Index.Types Brig.User.Search.SearchIndex - Brig.User.Search.TeamSize - Brig.User.Search.TeamUserSearch Brig.User.Template Brig.Version Brig.ZAuth - other-modules: Paths_brig hs-source-dirs: src ghc-options: -funbox-strict-fields -fplugin=Polysemy.Plugin - -fplugin=TransitiveAnns.Plugin -Wredundant-constraints - -Wunused-packages + -Wredundant-constraints -Wunused-packages build-depends: - , aeson >=2.0.1.0 - , amazonka >=2 - , amazonka-core >=2 - , amazonka-dynamodb >=2 - , amazonka-ses >=2 - , amazonka-sqs >=2 + , aeson >=2.0.1.0 + , amazonka >=2 + , amazonka-core >=2 + , amazonka-dynamodb >=2 + , amazonka-ses >=2 + , amazonka-sqs >=2 , amqp - , async >=2.1 - , auto-update >=0.1 - , base >=4 && <5 + , async >=2.1 + , auto-update >=0.1 + , base >=4 && <5 , base-prelude - , base16-bytestring >=0.1 - , base64-bytestring >=1.0 - , bilge >=0.21.1 - , bloodhound >=0.13 - , brig-types >=0.91.1 - , bytestring >=0.10 - , bytestring-conversion >=0.2 - , cassandra-util >=0.16.2 + , base16-bytestring >=0.1 + , base64-bytestring >=1.0 + , bilge >=0.21.1 + , bloodhound >=0.13 + , brig-types >=0.91.1 + , bytestring >=0.10 + , bytestring-conversion >=0.2 + , cassandra-util >=0.16.2 , comonad - , conduit >=1.2.8 - , containers >=0.5 - , cookie >=0.4 - , cql - , cryptobox-haskell >=0.1.1 - , currency-codes >=2.0 + , conduit >=1.2.8 + , containers >=0.5 + , cookie >=0.4 + , cryptobox-haskell >=0.1.1 + , crypton + , currency-codes >=2.0 , data-default , dns , dns-util - , enclosed-exceptions >=1.0 - , errors >=1.4 - , exceptions >=0.5 + , enclosed-exceptions >=1.0 + , errors >=1.4 + , exceptions >=0.5 , extended , extra , file-embed , file-embed-lzma - , filepath >=1.3 - , fsnotify >=0.4 - , galley-types >=0.75.3 - , gundeck-types >=1.32.1 - , hashable >=1.2 - , HsOpenSSL >=0.10 - , http-client >=0.7 - , http-client-openssl >=0.2 + , filepath >=1.3 + , fsnotify >=0.4 + , galley-types >=0.75.3 + , hashable >=1.2 + , hs-opentelemetry-instrumentation-wai + , hs-opentelemetry-sdk + , HsOpenSSL >=0.10 + , http-client >=0.7 + , http-client-openssl >=0.2 , http-media - , http-types >=0.8 + , http-types >=0.8 , http2-manager , imports , insert-ordered-containers - , iproute >=1.5 - , iso639 >=0.1 + , iproute >=1.5 + , iso639 >=0.1 , jose , jwt-tools - , lens >=3.8 - , lens-aeson >=1.0 - , metrics-core >=0.3 - , metrics-wai >=0.3 + , lens >=3.8 + , lens-aeson >=1.0 + , memory + , metrics-core >=0.3 + , metrics-wai >=0.3 , mime - , mime-mail >=0.4 + , mime-mail >=0.4 , mmorph - , MonadRandom >=0.5 - , mtl >=2.1 - , mwc-random - , network >=2.4 + , MonadRandom >=0.5 + , mtl >=2.1 + , network >=2.4 , network-conduit-tls , openapi3 - , optparse-applicative >=0.11 + , optparse-applicative >=0.11 , polysemy , polysemy-conc , polysemy-plugin , polysemy-time , polysemy-wire-zoo , prometheus-client - , proto-lens >=0.1 - , random-shuffle >=0.0.3 + , proto-lens >=0.1 + , random-shuffle >=0.0.3 , raw-strings-qq - , resourcet >=1.1 - , retry >=0.7 - , safe-exceptions >=0.1 - , saml2-web-sso + , resourcet >=1.1 + , retry >=0.7 + , safe-exceptions >=0.1 , schema-profunctor - , scientific >=0.3.4 , servant , servant-openapi3 , servant-server , servant-swagger-ui - , sodium-crypto-sign >=0.1 - , split >=0.2 + , sodium-crypto-sign >=0.1 + , split >=0.2 , ssl-util - , statistics >=0.13 - , stomp-queue >=0.3 - , template >=0.2 + , statistics >=0.13 + , stomp-queue >=0.3 + , template >=0.2 , template-haskell - , text >=0.11 - , text-icu-translit >=0.1 - , time >=1.1 + , text >=0.11 + , time >=1.1 , time-out , time-units - , tinylog >=0.10 - , transformers >=0.3 - , transitive-anns - , types-common >=0.16 + , tinylog >=0.10 + , transformers >=0.3 + , types-common >=0.16 , types-common-aws - , types-common-journal >=0.1 - , unliftio >=0.2 - , unordered-containers >=0.2 - , uri-bytestring >=0.2 + , types-common-journal >=0.1 + , unliftio >=0.2 + , unordered-containers >=0.2 + , uri-bytestring >=0.2 , utf8-string - , uuid >=1.3.5 - , vector >=0.11 - , wai >=3.0 - , wai-extra >=3.0 - , wai-middleware-gunzip >=0.0.2 - , wai-utilities >=0.16 + , uuid >=1.3.5 + , vector >=0.11 + , wai >=3.0 + , wai-extra >=3.0 + , wai-middleware-gunzip >=0.0.2 + , wai-utilities >=0.16 , wire-api , wire-api-federation + , wire-otel , wire-subsystems - , yaml >=0.8.22 - , zauth >=0.10.3 + , zauth >=0.10.3 executable brig import: common-all main-is: exec/Main.hs - other-modules: Paths_brig ghc-options: -funbox-strict-fields -threaded "-with-rtsopts=-N -T" -rtsopts -Wredundant-constraints -Wunused-packages build-depends: - , base , brig , HsOpenSSL , imports @@ -356,7 +339,6 @@ executable brig executable brig-index import: common-all main-is: index/src/Main.hs - other-modules: Paths_brig ghc-options: -funbox-strict-fields -threaded -with-rtsopts=-N build-depends: , base @@ -374,7 +356,6 @@ executable brig-integration API.Calling API.Federation API.Internal - API.Internal.Util API.Metrics API.MLS.Util API.OAuth @@ -393,8 +374,6 @@ executable brig-integration API.User.Client API.User.Connection API.User.Handles - API.User.PasswordReset - API.User.Property API.User.RichInfo API.User.Util API.UserPendingActivation @@ -509,7 +488,6 @@ executable brig-schema ghc-options: -funbox-strict-fields -Wredundant-constraints -threaded default-extensions: TemplateHaskell build-depends: - , base , brig , cassandra-util , extended @@ -528,8 +506,6 @@ test-suite brig-tests Test.Brig.Effects.Delay Test.Brig.InternalNotification Test.Brig.MLS - Test.Brig.Roundtrip - Test.Brig.User.Search.Index.Types hs-source-dirs: test/unit ghc-options: -funbox-strict-fields -threaded -with-rtsopts=-N @@ -553,6 +529,7 @@ test-suite brig-tests , tasty , tasty-hunit , tasty-quickcheck + , text , time , tinylog , types-common diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml index e0c76b082ca..ad2c8da9560 100644 --- a/services/brig/brig.integration.yaml +++ b/services/brig/brig.integration.yaml @@ -124,6 +124,7 @@ emailSMS: team: tInvitationUrl: http://127.0.0.1:8080/register?team=${team}&team_code=${code} + tExistingUserInvitationUrl: http://127.0.0.1:8080/accept-invitation?team-code=${code} tActivationUrl: http://127.0.0.1:8080/register?team=${team}&team_code=${code} tCreatorWelcomeUrl: http://127.0.0.1:8080/creator-welcome-website tMemberWelcomeUrl: http://127.0.0.1:8080/member-welcome-website @@ -154,9 +155,9 @@ turn: tokenTTL: 21600 optSettings: - setActivationTimeout: 10 - setVerificationTimeout: 10 - setTeamInvitationTimeout: 10 + setActivationTimeout: 4 + setVerificationTimeout: 4 + setTeamInvitationTimeout: 4 setExpiredUserCleanupTimeout: 1 # setStomp: test/resources/stomp-credentials.yaml setUserMaxConnections: 16 @@ -198,6 +199,11 @@ optSettings: # Remember to keep it the same in Galley. setFederationDomain: example.com setFeatureFlags: # see #RefConfigOptions in `/docs/reference` + conferenceCalling: + defaultForNew: + status: disabled + defaultForNull: + status: disabled setFederationDomainConfigsUpdateFreq: 1 setFederationStrategy: allowAll setFederationDomainConfigs: @@ -222,6 +228,11 @@ optSettings: setOAuthEnabled: true setOAuthRefreshTokenExpirationTimeSecs: 14515200 # 24 weeks setOAuthMaxActiveRefreshTokens: 10 + setPasswordHashingOptions: # in testing, we want these settings to be faster, not secure against attacks. + algorithm: argon2id + iterations: 1 + memory: 128 + parallelism: 1 logLevel: Warn logNetStrings: false diff --git a/services/brig/deb/opt/brig/template-version b/services/brig/deb/opt/brig/template-version index fea60e70c1a..5c41189b952 100644 --- a/services/brig/deb/opt/brig/template-version +++ b/services/brig/deb/opt/brig/template-version @@ -1 +1 @@ -v1.0.121 +v1.0.122 diff --git a/services/brig/deb/opt/brig/templates/de/provider/email/activation.html b/services/brig/deb/opt/brig/templates/de/provider/email/activation.html index e0f5f48f6a9..940ad39572a 100644 --- a/services/brig/deb/opt/brig/templates/de/provider/email/activation.html +++ b/services/brig/deb/opt/brig/templates/de/provider/email/activation.html @@ -1 +1 @@ -Ihr ${brand_service}-Benutzerkonto

${brand_label_url}

Bestätigen Sie Ihre E-Mail-Adresse

Ihre E-Mail-Adresse ${email} wurde verwendet, um sich als ${brand_service} zu registrieren.

Um die Registrierung abzuschließen, bestätigen Sie bitte Ihre E-Mail-Adresse, indem Sie auf den unteren Button klicken.

Bitte beachten Sie, dass das Service-Provider-Konto nach der Bestätigung der E-Mail-Adresse noch durch uns freigeschaltet werden muss. Dies geschieht üblicherweise innerhalb von 24 Stunden. Sie werden in einer separaten E-Mail über die Freischaltung informiert.

 
Bestätigen
 

Falls Sie nicht auf den Button klicken können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie sich nicht mit dieser E-Mail-Adresse für ein ${brand}-Benutzerkonto registriert haben, können Sie diese Nachricht ignorieren. Wenn Sie den Missbrauch Ihrer E-Mail-Adresse melden möchten, kontaktiere Sie uns bitte.

Bitte antworten Sie nicht auf diese Nachricht.

                                                           
\ No newline at end of file +Ihr ${brand_service}-Benutzerkonto

${brand_label_url}

Bestätigen Sie Ihre E-Mail-Adresse

Ihre E-Mail-Adresse ${email} wurde verwendet, um sich als ${brand_service} zu registrieren.

Um die Registrierung abzuschließen, bestätigen Sie bitte Ihre E-Mail-Adresse, indem Sie auf den unteren Button klicken.

Bitte beachten Sie, dass das Service-Provider-Konto nach der Bestätigung der E-Mail-Adresse noch durch uns freigeschaltet werden muss. Dies geschieht üblicherweise innerhalb von 24 Stunden. Sie werden in einer separaten E-Mail über die Freischaltung informiert.

 
Bestätigen
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie sich nicht mit dieser E-Mail-Adresse für ein ${brand}-Benutzerkonto registriert haben, können Sie diese Nachricht ignorieren. Wenn Sie den Missbrauch Ihrer E-Mail-Adresse melden möchten, kontaktiere Sie uns bitte.

Bitte antworten Sie nicht auf diese Nachricht.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/provider/email/activation.txt b/services/brig/deb/opt/brig/templates/de/provider/email/activation.txt index 6447353b46f..8626e814a04 100644 --- a/services/brig/deb/opt/brig/templates/de/provider/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/de/provider/email/activation.txt @@ -14,8 +14,8 @@ E-Mail-Adresse noch durch uns freigeschaltet werden muss. Dies geschieht üblicherweise innerhalb von 24 Stunden. Sie werden in einer separaten E-Mail über die Freischaltung informiert. -Bestätigen [${url}]Falls Sie nicht auf den Button klicken können, kopieren Sie -diesen Link und fügen Sie ihn in Ihren Browser ein: +Bestätigen [${url}]Wenn Sie die Schaltfläche nicht auswählen können, kopieren +Sie diesen Link und fügen Sie ihn in Ihren Browser ein: ${url} diff --git a/services/brig/deb/opt/brig/templates/de/provider/email/approval-request.html b/services/brig/deb/opt/brig/templates/de/provider/email/approval-request.html index c15511e2223..65e903084d2 100644 --- a/services/brig/deb/opt/brig/templates/de/provider/email/approval-request.html +++ b/services/brig/deb/opt/brig/templates/de/provider/email/approval-request.html @@ -1 +1 @@ -Genehmigungsanfrage: ${brand_service}

${brand_label_url}

Genehmigungsanfrage

Ein neuer ${brand_service} ist registriert und wartet auf die Genehmigung. Bitte lesen Sie die unten angegebenen Informationen.

Name: ${name}

E-Mail: ${email}

Website: ${url}

Beschreibung: ${description}

Wenn die Anfrage echt scheint, können Sie den Anbieter genehmigen, indem Sie auf den unteren Button klicken. Sobald genehmigt, kann sich der Anbieter anmelden und mit der Registrierung von Diensten beginnen, die ${brand}-Nutzer ihren Unterhaltungen hinzufügen können.

Falls die Anfrage zweifelhaft scheint, wenden Sie sich bitte an den Anbieter zur Klärung, bevor Sie fortfahren.

 
Genehmigen
 

Falls Sie nicht auf den Button klicken können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Bitte antworten Sie nicht auf diese Nachricht.

                                                           
\ No newline at end of file +Genehmigungsanfrage: ${brand_service}

${brand_label_url}

Genehmigungsanfrage

Ein neuer ${brand_service} ist registriert und wartet auf die Genehmigung. Bitte lesen Sie die unten angegebenen Informationen.

Name: ${name}

E-Mail: ${email}

Website: ${url}

Beschreibung: ${description}

Wenn die Anfrage echt scheint, können Sie den Anbieter genehmigen, indem Sie auf den unteren Button klicken. Sobald genehmigt, kann sich der Anbieter anmelden und mit der Registrierung von Diensten beginnen, die ${brand}-Nutzer ihren Unterhaltungen hinzufügen können.

Falls die Anfrage zweifelhaft scheint, wenden Sie sich bitte an den Anbieter zur Klärung, bevor Sie fortfahren.

 
Genehmigen
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Bitte antworten Sie nicht auf diese Nachricht.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/provider/email/approval-request.txt b/services/brig/deb/opt/brig/templates/de/provider/email/approval-request.txt index efc6a992d13..5859663943b 100644 --- a/services/brig/deb/opt/brig/templates/de/provider/email/approval-request.txt +++ b/services/brig/deb/opt/brig/templates/de/provider/email/approval-request.txt @@ -22,8 +22,8 @@ Unterhaltungen hinzufügen können. Falls die Anfrage zweifelhaft scheint, wenden Sie sich bitte an den Anbieter zur Klärung, bevor Sie fortfahren. -Genehmigen [${url}]Falls Sie nicht auf den Button klicken können, kopieren Sie -diesen Link und fügen Sie ihn in Ihren Browser ein: +Genehmigen [${url}]Wenn Sie die Schaltfläche nicht auswählen können, kopieren +Sie diesen Link und fügen Sie ihn in Ihren Browser ein: ${url} diff --git a/services/brig/deb/opt/brig/templates/de/team/email/invitation.html b/services/brig/deb/opt/brig/templates/de/team/email/invitation.html index 7abcafc58aa..56090496f2d 100644 --- a/services/brig/deb/opt/brig/templates/de/team/email/invitation.html +++ b/services/brig/deb/opt/brig/templates/de/team/email/invitation.html @@ -1 +1 @@ -Sie wurden eingeladen, einem ${brand}-Team beizutreten

${brand_label_url}

Einladung zum Team

${inviter} hat Sie auf ${brand} zu einem Team eingeladen. Klicken Sie bitte auf den unteren Button, um die Einladung anzunehmen.

 
Team beitreten
 

Falls Sie nicht auf den Button klicken können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

Was ist Wire?
Wire ist die sicherste Plattform für Ihre Kommunikation. Wo auch immer Sie sind, arbeiten Sie mit Ihrem Team und externen Partnern zusammen – mittels Nachrichten, Videokonferenzen und Dateiaustausch, alles mit Ende-zu-Ende-Verschlüsselung. Mehr erfahren.

                                                           
\ No newline at end of file +Sie wurden eingeladen, einem ${brand}-Team beizutreten

${brand_label_url}

Einladung zum Team

${inviter} hat Sie auf ${brand} zu einem Team eingeladen. Wählen Sie die folgende Schaltfläche, um die Einladung anzunehmen.

 
Team beitreten
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

Was ist Wire?
Wire ist die sicherste Plattform für Ihre Kommunikation. Wo auch immer Sie sind, arbeiten Sie mit Ihrem Team und externen Partnern zusammen – mittels Nachrichten, Videokonferenzen und Dateiaustausch, alles mit Ende-zu-Ende-Verschlüsselung. Mehr erfahren.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/team/email/invitation.txt b/services/brig/deb/opt/brig/templates/de/team/email/invitation.txt index 1301fc13af1..342ba5b4086 100644 --- a/services/brig/deb/opt/brig/templates/de/team/email/invitation.txt +++ b/services/brig/deb/opt/brig/templates/de/team/email/invitation.txt @@ -3,11 +3,11 @@ ${brand_label_url} [${brand_url}] EINLADUNG ZUM TEAM -${inviter} hat Sie auf ${brand} zu einem Team eingeladen. Klicken Sie bitte auf -den unteren Button, um die Einladung anzunehmen. +${inviter} hat Sie auf ${brand} zu einem Team eingeladen. Wählen Sie die +folgende Schaltfläche, um die Einladung anzunehmen. -Team beitreten [${url}]Falls Sie nicht auf den Button klicken können, kopieren -Sie diesen Link und fügen Sie ihn in Ihren Browser ein: +Team beitreten [${url}]Wenn Sie die Schaltfläche nicht auswählen können, +kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein: ${url} diff --git a/services/brig/deb/opt/brig/templates/de/team/email/migration-subject.txt b/services/brig/deb/opt/brig/templates/de/team/email/migration-subject.txt new file mode 100644 index 00000000000..3bd825679e9 --- /dev/null +++ b/services/brig/deb/opt/brig/templates/de/team/email/migration-subject.txt @@ -0,0 +1 @@ +Sie wurden eingeladen, einem Team auf ${brand} beizutreten \ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/team/email/migration.html b/services/brig/deb/opt/brig/templates/de/team/email/migration.html new file mode 100644 index 00000000000..5ea4eef51b2 --- /dev/null +++ b/services/brig/deb/opt/brig/templates/de/team/email/migration.html @@ -0,0 +1 @@ +Sie wurden eingeladen, einem Team auf ${brand} beizutreten

${brand_label_url}

Einladung zum Team

${inviter} hat Sie auf ${brand} zu einem Team eingeladen.

Wenn Sie dem Team beitreten, wird Ihr persönliches Wire Benutzerkonto in ein Team-Konto umgewandelt.

Wählen Sie die folgende Schaltfläche, um mit der Einladung fortzufahren.

 
Team beitreten
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Missbräuchlichen Einladungslink melden

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

Was ist Wire?
Wire ist die sicherste Plattform für Ihre Kommunikation. Wo auch immer Sie sind, arbeiten Sie mit Ihrem Team und externen Partnern zusammen – mittels Nachrichten, Videokonferenzen und Dateiaustausch, alles mit Ende-zu-Ende-Verschlüsselung. Mehr erfahren.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/team/email/migration.txt b/services/brig/deb/opt/brig/templates/de/team/email/migration.txt new file mode 100644 index 00000000000..5c211d09a3e --- /dev/null +++ b/services/brig/deb/opt/brig/templates/de/team/email/migration.txt @@ -0,0 +1,33 @@ +[${brand_logo}] + +${brand_label_url} [${brand_url}] + +EINLADUNG ZUM TEAM +${inviter} hat Sie auf ${brand} zu einem Team eingeladen. + +Wenn Sie dem Team beitreten, wird Ihr persönliches Wire Benutzerkonto in ein +Team-Konto umgewandelt. + +Wählen Sie die folgende Schaltfläche, um mit der Einladung fortzufahren. + +Team beitreten [${url}]Wenn Sie die Schaltfläche nicht auswählen können, +kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein: + +${url} + +Missbräuchlichen Einladungslink melden [${support}] + +Wenn Sie Fragen haben, dann kontaktieren Sie uns [${support}] bitte. + +Was ist Wire? +Wire ist die sicherste Plattform für Ihre Kommunikation. Wo auch immer Sie sind, +arbeiten Sie mit Ihrem Team und externen Partnern zusammen – mittels +Nachrichten, Videokonferenzen und Dateiaustausch, alles mit +Ende-zu-Ende-Verschlüsselung. Mehr erfahren [https://wire.com/]. + + +-------------------------------------------------------------------------------- + +Datenschutzrichtlinien und Nutzungsbedingungen [${legal}] · Missbrauch melden +[${misuse}] +${copyright}. ALLE RECHTE VORBEHALTEN. \ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/team/email/new-member-welcome.html b/services/brig/deb/opt/brig/templates/de/team/email/new-member-welcome.html index 03c007c723f..f97a060f14b 100644 --- a/services/brig/deb/opt/brig/templates/de/team/email/new-member-welcome.html +++ b/services/brig/deb/opt/brig/templates/de/team/email/new-member-welcome.html @@ -1 +1 @@ -Sie sind einem Team auf ${brand} beigetreten

${brand_label_url}

Willkommen bei ${team_name}.

Sie sind soeben mit ${email} einem Team namens ${team_name} auf ${brand} beigetreten.

 

${brand} vereint sichere Verschlüsselung mit reichhaltigem Funktionsumfang und einfacher Bedienung in einer einzigen App. Unterstützt alle gängigen Plattformen.

 
${brand} herunterladen
 

Falls Sie nicht auf den Button klicken können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

Team ID: ${team_id}

                                                           
\ No newline at end of file +Sie sind einem Team auf ${brand} beigetreten

${brand_label_url}

Willkommen bei ${team_name}.

Sie sind soeben mit ${email} einem Team namens ${team_name} auf ${brand} beigetreten.

 

${brand} vereint sichere Verschlüsselung mit reichhaltigem Funktionsumfang und einfacher Bedienung in einer einzigen App. Unterstützt alle gängigen Plattformen.

 
${brand} herunterladen
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

Team ID: ${team_id}

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/team/email/new-member-welcome.txt b/services/brig/deb/opt/brig/templates/de/team/email/new-member-welcome.txt index b0281ab55a9..b11a98b9728 100644 --- a/services/brig/deb/opt/brig/templates/de/team/email/new-member-welcome.txt +++ b/services/brig/deb/opt/brig/templates/de/team/email/new-member-welcome.txt @@ -10,7 +10,7 @@ ${brand} vereint sichere Verschlüsselung mit reichhaltigem Funktionsumfang und einfacher Bedienung in einer einzigen App. Unterstützt alle gängigen Plattformen. -${brand} herunterladen [${url}]Falls Sie nicht auf den Button klicken können, +${brand} herunterladen [${url}]Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein: ${url} diff --git a/services/brig/deb/opt/brig/templates/de/user/email/activation.html b/services/brig/deb/opt/brig/templates/de/user/email/activation.html index ec58a8e1a32..29ce8712831 100644 --- a/services/brig/deb/opt/brig/templates/de/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/de/user/email/activation.html @@ -1 +1 @@ -Ihr ${brand}-Benutzerkonto

${brand_label_url}

Bestätigen Sie Ihre E-Mail-Adresse

${email} wurde verwendet, um ein Benutzerkonto auf ${brand} zu erstellen.
Klicken Sie auf den folgenden Button, um Ihre E-Mail-Adresse zu bestätigen.

 
Bestätigen
 

Falls Sie nicht auf den Button klicken können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

                                                           
\ No newline at end of file +Ihr ${brand}-Benutzerkonto

${brand_label_url}

Bestätigen Sie Ihre E-Mail-Adresse

${email} wurde verwendet, um ein Benutzerkonto auf ${brand} zu erstellen.
Klicken Sie auf den folgenden Button, um Ihre E-Mail-Adresse zu bestätigen.

 
Bestätigen
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/user/email/activation.txt b/services/brig/deb/opt/brig/templates/de/user/email/activation.txt index fbebd7779e7..27ab0068b88 100644 --- a/services/brig/deb/opt/brig/templates/de/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/de/user/email/activation.txt @@ -6,8 +6,8 @@ BESTÄTIGEN SIE IHRE E-MAIL-ADRESSE ${email} wurde verwendet, um ein Benutzerkonto auf ${brand} zu erstellen. Klicken Sie auf den folgenden Button, um Ihre E-Mail-Adresse zu bestätigen. -Bestätigen [${url}]Falls Sie nicht auf den Button klicken können, kopieren Sie -diesen Link und fügen Sie ihn in Ihren Browser ein: +Bestätigen [${url}]Wenn Sie die Schaltfläche nicht auswählen können, kopieren +Sie diesen Link und fügen Sie ihn in Ihren Browser ein: ${url} diff --git a/services/brig/deb/opt/brig/templates/de/user/email/deletion.html b/services/brig/deb/opt/brig/templates/de/user/email/deletion.html index 7c6ba323943..15959fc3888 100644 --- a/services/brig/deb/opt/brig/templates/de/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/de/user/email/deletion.html @@ -1 +1 @@ -Benutzerkonto löschen?

${brand_label_url}

Ihr Benutzerkonto löschen

Wir haben eine Anfrage zur Löschung Ihrer ${brand}-Benutzerkontos erhalten. Klicken Sie innerhalb der nächsten 10 Minuten auf den folgenden Link, um alle Ihre Unterhaltungen, Nachrichten und Kontakte zu löschen.

 
Benutzerkonto löschen
 

Falls Sie nicht auf den Button klicken können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Falls Sie dies nicht angefordert haben, setzen Sie Ihr Passwort zurück.

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

                                                           
\ No newline at end of file +Benutzerkonto löschen?

${brand_label_url}

Ihr Benutzerkonto löschen

Wir haben eine Anfrage zur Löschung Ihrer ${brand}-Benutzerkontos erhalten. Klicken Sie innerhalb der nächsten 10 Minuten auf den folgenden Link, um alle Ihre Unterhaltungen, Nachrichten und Kontakte zu löschen.

 
Benutzerkonto löschen
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Falls Sie dies nicht angefordert haben, setzen Sie Ihr Passwort zurück.

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/de/user/email/deletion.txt index 2dc9a61aa06..7e093b709de 100644 --- a/services/brig/deb/opt/brig/templates/de/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/de/user/email/deletion.txt @@ -7,7 +7,7 @@ Wir haben eine Anfrage zur Löschung Ihrer ${brand}-Benutzerkontos erhalten. Klicken Sie innerhalb der nächsten 10 Minuten auf den folgenden Link, um alle Ihre Unterhaltungen, Nachrichten und Kontakte zu löschen. -Benutzerkonto löschen [${url}]Falls Sie nicht auf den Button klicken können, +Benutzerkonto löschen [${url}]Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein: ${url} diff --git a/services/brig/deb/opt/brig/templates/de/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/de/user/email/password-reset.html index de528deb585..4546794f9df 100644 --- a/services/brig/deb/opt/brig/templates/de/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/de/user/email/password-reset.html @@ -1 +1 @@ -Änderung des Passworts auf ${brand}

${brand_label_url}

Passwort zurücksetzen

Wir haben eine Anfrage zum Zurücksetzen des Passworts für Ihr ${brand}-Benutzerkonto erhalten. Klicken Sie auf den folgenden Button, um ein neues Passwort zu erstellen.

 
Passwort zurücksetzen
 

Falls Sie nicht auf den Button klicken können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

                                                           
\ No newline at end of file +Änderung des Passworts auf ${brand}

${brand_label_url}

Passwort zurücksetzen

Wir haben eine Anfrage zum Zurücksetzen des Passworts für Ihr ${brand}-Benutzerkonto erhalten. Klicken Sie auf den folgenden Button, um ein neues Passwort zu erstellen.

 
Passwort zurücksetzen
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/de/user/email/password-reset.txt index 3378e9c7e1d..f42ac0e6cd7 100644 --- a/services/brig/deb/opt/brig/templates/de/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/de/user/email/password-reset.txt @@ -7,7 +7,7 @@ Wir haben eine Anfrage zum Zurücksetzen des Passworts für Ihr ${brand}-Benutzerkonto erhalten. Klicken Sie auf den folgenden Button, um ein neues Passwort zu erstellen. -Passwort zurücksetzen [${url}]Falls Sie nicht auf den Button klicken können, +Passwort zurücksetzen [${url}]Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein: ${url} diff --git a/services/brig/deb/opt/brig/templates/de/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/de/user/email/team-activation.html index 6818d31b724..77d987204f1 100644 --- a/services/brig/deb/opt/brig/templates/de/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/de/user/email/team-activation.html @@ -1 +1 @@ -${brand} Benutzerkonto

${brand_label_url}

Ihr neues ${brand}-Benutzerkonto

Ein neues ${brand} Team wurde mit ${email} erstellt. Bitte bestätigen Sie Ihre E-Mail-Adresse.

 
Bestätigen
 

Falls Sie nicht auf den Button klicken können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

                                                           
\ No newline at end of file +${brand} Benutzerkonto

${brand_label_url}

Ihr neues ${brand}-Benutzerkonto

Ein neues ${brand} Team wurde mit ${email} erstellt. Bitte bestätigen Sie Ihre E-Mail-Adresse.

 
Bestätigen
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/de/user/email/team-activation.txt index c2499a99a9e..de9a501ae30 100644 --- a/services/brig/deb/opt/brig/templates/de/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/de/user/email/team-activation.txt @@ -6,8 +6,8 @@ IHR NEUES ${brand}-BENUTZERKONTO Ein neues ${brand} Team wurde mit ${email} erstellt. Bitte bestätigen Sie Ihre E-Mail-Adresse. -Bestätigen [${url}]Falls Sie nicht auf den Button klicken können, kopieren Sie -diesen Link und fügen Sie ihn in Ihren Browser ein: +Bestätigen [${url}]Wenn Sie die Schaltfläche nicht auswählen können, kopieren +Sie diesen Link und fügen Sie ihn in Ihren Browser ein: ${url} diff --git a/services/brig/deb/opt/brig/templates/de/user/email/update.html b/services/brig/deb/opt/brig/templates/de/user/email/update.html index 61148ef262b..672df632d51 100644 --- a/services/brig/deb/opt/brig/templates/de/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/de/user/email/update.html @@ -1 +1 @@ -Ihre neue E-Mail-Adresse auf ${brand}

${brand_label_url}

Bestätigen Sie Ihre E-Mail-Adresse

${email} wurde als Ihre neue E-Mail-Adresse auf ${brand} registriert. Klicken Sie auf den folgenden Button, um Ihre neue Adresse zu bestätigen.

 
Bestätigen
 

Falls Sie nicht auf den Button klicken können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

                                                           
\ No newline at end of file +Ihre neue E-Mail-Adresse auf ${brand}

${brand_label_url}

Bestätigen Sie Ihre E-Mail-Adresse

${email} wurde als Ihre neue E-Mail-Adresse auf ${brand} registriert. Klicken Sie auf den folgenden Button, um Ihre neue Adresse zu bestätigen.

 
Bestätigen
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/user/email/update.txt b/services/brig/deb/opt/brig/templates/de/user/email/update.txt index 783804f8fc1..61af385b2be 100644 --- a/services/brig/deb/opt/brig/templates/de/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/de/user/email/update.txt @@ -6,8 +6,8 @@ BESTÄTIGEN SIE IHRE E-MAIL-ADRESSE ${email} wurde als Ihre neue E-Mail-Adresse auf ${brand} registriert. Klicken Sie auf den folgenden Button, um Ihre neue Adresse zu bestätigen. -Bestätigen [${url}]Falls Sie nicht auf den Button klicken können, kopieren Sie -diesen Link und fügen Sie ihn in Ihren Browser ein: +Bestätigen [${url}]Wenn Sie die Schaltfläche nicht auswählen können, kopieren +Sie diesen Link und fügen Sie ihn in Ihren Browser ein: ${url} diff --git a/services/brig/deb/opt/brig/templates/en/provider/email/activation.html b/services/brig/deb/opt/brig/templates/en/provider/email/activation.html index fce3dd2e80b..228d9624960 100644 --- a/services/brig/deb/opt/brig/templates/en/provider/email/activation.html +++ b/services/brig/deb/opt/brig/templates/en/provider/email/activation.html @@ -1 +1 @@ -Your ${brand_service} Account

${brand_label_url}

Verify your email

Your email address ${email} was used to register as a ${brand_service}.

To complete the registration, it is necessary that you verify your e-mail address by clicking on the button below.

Please note that upon successful verification of your e-mail, your ${brand_service} account is still subject to approval through our staff, which usually happens within 24 hours. You will be informed of the approval via a separate e-mail.

 
Verify
 

If you can’t click the button, copy and paste this link to your browser:

${url}

If you didn’t register for a ${brand} service provider account using this e-mail address, you can safely ignore this message. If you want to report abuse of your e-mail address, please contact us.

Please don’t reply to this message.

                                                           
\ No newline at end of file +Your ${brand_service} Account

${brand_label_url}

Verify your email

Your email address ${email} was used to register as a ${brand_service}.

To complete the registration, it is necessary that you verify your e-mail address by clicking on the button below.

Please note that upon successful verification of your e-mail, your ${brand_service} account is still subject to approval through our staff, which usually happens within 24 hours. You will be informed of the approval via a separate e-mail.

 
Verify
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you didn’t register for a ${brand} service provider account using this e-mail address, you can safely ignore this message. If you want to report abuse of your e-mail address, please contact us.

Please don’t reply to this message.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/provider/email/activation.txt b/services/brig/deb/opt/brig/templates/en/provider/email/activation.txt index ded53a93c34..319a9d5ccae 100644 --- a/services/brig/deb/opt/brig/templates/en/provider/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/en/provider/email/activation.txt @@ -13,7 +13,7 @@ ${brand_service} account is still subject to approval through our staff, which usually happens within 24 hours. You will be informed of the approval via a separate e-mail. -Verify [${url}]If you can’t click the button, copy and paste this link to your +Verify [${url}]If you can’t select the button, copy and paste this link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/en/provider/email/approval-request.html b/services/brig/deb/opt/brig/templates/en/provider/email/approval-request.html index d2900b45d9a..f587afc7800 100644 --- a/services/brig/deb/opt/brig/templates/en/provider/email/approval-request.html +++ b/services/brig/deb/opt/brig/templates/en/provider/email/approval-request.html @@ -1 +1 @@ -Approval Request: ${brand_service}

${brand_label_url}

Approval request

A new ${brand_service} has registered and is awaiting approval. Please review the information provided below.

Name: ${name}

E-mail: ${email}

Website: ${url}

Description: ${description}

If the request seems genuine, you can approve the provider by clicking on the button below. Once approved, the provider will be able to sign in and start registering services that ${brand} users can add to their conversations.

If the request seems dubious, please contact the provider for clarifications before proceeding.

 
Approve
 

If you can’t click the button, copy and paste this link to your browser:

${url}

Please don’t reply to this message.

                                                           
\ No newline at end of file +Approval Request: ${brand_service}

${brand_label_url}

Approval request

A new ${brand_service} has registered and is awaiting approval. Please review the information provided below.

Name: ${name}

E-mail: ${email}

Website: ${url}

Description: ${description}

If the request seems genuine, you can approve the provider by clicking on the button below. Once approved, the provider will be able to sign in and start registering services that ${brand} users can add to their conversations.

If the request seems dubious, please contact the provider for clarifications before proceeding.

 
Approve
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Please don’t reply to this message.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/provider/email/approval-request.txt b/services/brig/deb/opt/brig/templates/en/provider/email/approval-request.txt index 2332083c199..04679fd8abe 100644 --- a/services/brig/deb/opt/brig/templates/en/provider/email/approval-request.txt +++ b/services/brig/deb/opt/brig/templates/en/provider/email/approval-request.txt @@ -21,7 +21,7 @@ registering services that ${brand} users can add to their conversations. If the request seems dubious, please contact the provider for clarifications before proceeding. -Approve [${url}]If you can’t click the button, copy and paste this link to your +Approve [${url}]If you can’t select the button, copy and paste this link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/en/team/email/invitation.html b/services/brig/deb/opt/brig/templates/en/team/email/invitation.html index 1643844f28e..47dd7cd0f14 100644 --- a/services/brig/deb/opt/brig/templates/en/team/email/invitation.html +++ b/services/brig/deb/opt/brig/templates/en/team/email/invitation.html @@ -1 +1 @@ -You have been invited to join a team on ${brand}

${brand_label_url}

Team invitation

${inviter} has invited you to join a team on ${brand}. Click the button below to accept the invitation.

 
Join team
 

If you can’t click the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

What is Wire?
Wire is the most secure collaboration platform. Work with your team and external partners wherever you are through messages, video conferencing and file sharing – always secured with end-to-end-encryption. Learn more.

                                                           
\ No newline at end of file +You have been invited to join a team on ${brand}

${brand_label_url}

Team invitation

${inviter} has invited you to join a team on ${brand}. Select the button below to accept the invitation.

 
Join team
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

What is Wire?
Wire is the most secure collaboration platform. Work with your team and external partners wherever you are through messages, video conferencing and file sharing – always secured with end-to-end-encryption. Learn more.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/team/email/invitation.txt b/services/brig/deb/opt/brig/templates/en/team/email/invitation.txt index 918c8fde767..ae49c91b8da 100644 --- a/services/brig/deb/opt/brig/templates/en/team/email/invitation.txt +++ b/services/brig/deb/opt/brig/templates/en/team/email/invitation.txt @@ -3,10 +3,10 @@ ${brand_label_url} [${brand_url}] TEAM INVITATION -${inviter} has invited you to join a team on ${brand}. Click the button below to -accept the invitation. +${inviter} has invited you to join a team on ${brand}. Select the button below +to accept the invitation. -Join team [${url}]If you can’t click the button, copy and paste this link to +Join team [${url}]If you can’t select the button, copy and paste this link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/en/team/email/migration-subject.txt b/services/brig/deb/opt/brig/templates/en/team/email/migration-subject.txt new file mode 100644 index 00000000000..9fef363e407 --- /dev/null +++ b/services/brig/deb/opt/brig/templates/en/team/email/migration-subject.txt @@ -0,0 +1 @@ +You have been invited to join a team on ${brand} \ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/team/email/migration.html b/services/brig/deb/opt/brig/templates/en/team/email/migration.html new file mode 100644 index 00000000000..e6647120d03 --- /dev/null +++ b/services/brig/deb/opt/brig/templates/en/team/email/migration.html @@ -0,0 +1 @@ +You have been invited to join a team on ${brand}

${brand_label_url}

Team invitation

${inviter} has invited you to join a team on ${brand}.

By joining Wire migrates your personal account into a team account.

Select the button below to proceed with the invitation.

 
Join team
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Report abusive invitation link

If you have any questions, please contact us.

What is Wire?
Wire is the most secure collaboration platform. Work with your team and external partners wherever you are through messages, video conferencing and file sharing – always secured with end-to-end-encryption. Learn more.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/team/email/migration.txt b/services/brig/deb/opt/brig/templates/en/team/email/migration.txt new file mode 100644 index 00000000000..6283ee15add --- /dev/null +++ b/services/brig/deb/opt/brig/templates/en/team/email/migration.txt @@ -0,0 +1,30 @@ +[${brand_logo}] + +${brand_label_url} [${brand_url}] + +TEAM INVITATION +${inviter} has invited you to join a team on ${brand}. + +By joining Wire migrates your personal account into a team account. + +Select the button below to proceed with the invitation. + +Join team [${url}]If you can’t select the button, copy and paste this link to +your browser: + +${url} + +Report abusive invitation link [${support}] + +If you have any questions, please contact us [${support}]. + +What is Wire? +Wire is the most secure collaboration platform. Work with your team and external +partners wherever you are through messages, video conferencing and file sharing +– always secured with end-to-end-encryption. Learn more [https://wire.com/]. + + +-------------------------------------------------------------------------------- + +Privacy policy and terms of use [${legal}] · Report Misuse [${misuse}] +${copyright}. ALL RIGHTS RESERVED. \ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/team/email/new-member-welcome.html b/services/brig/deb/opt/brig/templates/en/team/email/new-member-welcome.html index a63b3a1d9c6..5496be6e83a 100644 --- a/services/brig/deb/opt/brig/templates/en/team/email/new-member-welcome.html +++ b/services/brig/deb/opt/brig/templates/en/team/email/new-member-welcome.html @@ -1 +1 @@ -You joined a team on ${brand}

${brand_label_url}

Welcome to ${team_name}.

You have just joined a team called ${team_name} on ${brand} with ${email}.

 

${brand} combines strong encryption, a rich feature set and ease-of-use in one app like never before. Works on all popular platforms.

 
Download ${brand}
 

If you can’t click the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

Team ID: ${team_id}

                                                           
\ No newline at end of file +You joined a team on ${brand}

${brand_label_url}

Welcome to ${team_name}.

You have just joined a team called ${team_name} on ${brand} with ${email}.

 

${brand} combines strong encryption, a rich feature set and ease-of-use in one app like never before. Works on all popular platforms.

 
Download ${brand}
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

Team ID: ${team_id}

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/team/email/new-member-welcome.txt b/services/brig/deb/opt/brig/templates/en/team/email/new-member-welcome.txt index 413c4fdf2b9..0fb7ebe1569 100644 --- a/services/brig/deb/opt/brig/templates/en/team/email/new-member-welcome.txt +++ b/services/brig/deb/opt/brig/templates/en/team/email/new-member-welcome.txt @@ -8,7 +8,7 @@ You have just joined a team called ${team_name} on ${brand} with ${email}. ${brand} combines strong encryption, a rich feature set and ease-of-use in one app like never before. Works on all popular platforms. -Download ${brand} [${url}]If you can’t click the button, copy and paste this +Download ${brand} [${url}]If you can’t select the button, copy and paste this link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/en/user/email/activation.html b/services/brig/deb/opt/brig/templates/en/user/email/activation.html index c67376c606b..cd89b8e1bcc 100644 --- a/services/brig/deb/opt/brig/templates/en/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/en/user/email/activation.html @@ -1 +1 @@ -Your ${brand} Account

${brand_label_url}

Verify your email

${email} was used to register on ${brand}.
Click the button to verify your address.

 
Verify
 

If you can’t click the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file +Your ${brand} Account

${brand_label_url}

Verify your email

${email} was used to register on ${brand}.
Click the button to verify your address.

 
Verify
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/user/email/activation.txt b/services/brig/deb/opt/brig/templates/en/user/email/activation.txt index af771cc00a7..a50f4da4be0 100644 --- a/services/brig/deb/opt/brig/templates/en/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/en/user/email/activation.txt @@ -6,7 +6,7 @@ VERIFY YOUR EMAIL ${email} was used to register on ${brand}. Click the button to verify your address. -Verify [${url}]If you can’t click the button, copy and paste this link to your +Verify [${url}]If you can’t select the button, copy and paste this link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/en/user/email/deletion.html b/services/brig/deb/opt/brig/templates/en/user/email/deletion.html index 690b0104fdd..f4ec9a50034 100644 --- a/services/brig/deb/opt/brig/templates/en/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/en/user/email/deletion.html @@ -1 +1 @@ -Delete account?

${brand_label_url}

Delete your account

We’ve received a request to delete your ${brand} account. Click the button below within 10 minutes to delete all your conversations, content and connections.

 
Delete account
 

If you can’t click the button, copy and paste this link to your browser:

${url}

If you didn’t request this, reset your password.

If you have any questions, please contact us.

                                                           
\ No newline at end of file +Delete account?

${brand_label_url}

Delete your account

We’ve received a request to delete your ${brand} account. Click the button below within 10 minutes to delete all your conversations, content and connections.

 
Delete account
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you didn’t request this, reset your password.

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/en/user/email/deletion.txt index 744da7dc05c..4d8792997f6 100644 --- a/services/brig/deb/opt/brig/templates/en/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/en/user/email/deletion.txt @@ -6,7 +6,7 @@ DELETE YOUR ACCOUNT We’ve received a request to delete your ${brand} account. Click the button below within 10 minutes to delete all your conversations, content and connections. -Delete account [${url}]If you can’t click the button, copy and paste this link +Delete account [${url}]If you can’t select the button, copy and paste this link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/en/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/en/user/email/password-reset.html index 53ffea05fde..dc84cabd359 100644 --- a/services/brig/deb/opt/brig/templates/en/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/en/user/email/password-reset.html @@ -1 +1 @@ -Password Change at ${brand}

${brand_label_url}

Reset your password

We’ve received a request to reset the password for your ${brand} account. To create a new password, click the button below.

 
Reset password
 

If you can’t click the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file +Password Change at ${brand}

${brand_label_url}

Reset your password

We’ve received a request to reset the password for your ${brand} account. To create a new password, click the button below.

 
Reset password
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/en/user/email/password-reset.txt index d16da88792f..e35ece49b89 100644 --- a/services/brig/deb/opt/brig/templates/en/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/en/user/email/password-reset.txt @@ -6,7 +6,7 @@ RESET YOUR PASSWORD We’ve received a request to reset the password for your ${brand} account. To create a new password, click the button below. -Reset password [${url}]If you can’t click the button, copy and paste this link +Reset password [${url}]If you can’t select the button, copy and paste this link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/en/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/en/user/email/team-activation.html index e34ca5f3894..81cc402e4b4 100644 --- a/services/brig/deb/opt/brig/templates/en/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/en/user/email/team-activation.html @@ -1 +1 @@ -${brand} Account

${brand_label_url}

Your new account on ${brand}

A new ${brand} team was created with ${email}. Please verify your email.

 
Verify
 

If you can’t click the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file +${brand} Account

${brand_label_url}

Your new account on ${brand}

A new ${brand} team was created with ${email}. Please verify your email.

 
Verify
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/en/user/email/team-activation.txt index 39a00b2089a..4b8db92ee7c 100644 --- a/services/brig/deb/opt/brig/templates/en/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/en/user/email/team-activation.txt @@ -5,7 +5,7 @@ ${brand_label_url} [${brand_url}] YOUR NEW ACCOUNT ON ${brand} A new ${brand} team was created with ${email}. Please verify your email. -Verify [${url}]If you can’t click the button, copy and paste this link to your +Verify [${url}]If you can’t select the button, copy and paste this link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/en/user/email/update.html b/services/brig/deb/opt/brig/templates/en/user/email/update.html index 339aad5dea7..0eb38d8ea41 100644 --- a/services/brig/deb/opt/brig/templates/en/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/en/user/email/update.html @@ -1 +1 @@ -Your new email address on ${brand}

${brand_label_url}

Verify your email

${email} was registered as your new email address on ${brand}. Click the button below to verify your address.

 
Verify
 

If you can’t click the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file +Your new email address on ${brand}

${brand_label_url}

Verify your email

${email} was registered as your new email address on ${brand}. Click the button below to verify your address.

 
Verify
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/user/email/update.txt b/services/brig/deb/opt/brig/templates/en/user/email/update.txt index 5ee25666aa2..8d7ed7187bc 100644 --- a/services/brig/deb/opt/brig/templates/en/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/en/user/email/update.txt @@ -6,7 +6,7 @@ VERIFY YOUR EMAIL ${email} was registered as your new email address on ${brand}. Click the button below to verify your address. -Verify [${url}]If you can’t click the button, copy and paste this link to your +Verify [${url}]If you can’t select the button, copy and paste this link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/et/user/email/activation.html b/services/brig/deb/opt/brig/templates/et/user/email/activation.html index bd77e33958a..1dc6b7d98d8 100644 --- a/services/brig/deb/opt/brig/templates/et/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/et/user/email/activation.html @@ -1 +1 @@ -Your ${brand} Account

${brand_label_url}

Kinnita oma e-posti aadress

${email} was used to register on ${brand}.
Click the button to verify your address.

 
Kinnita
 

Kui sul pole võimalik nuppu klikkida, siis kopeeri allolev aadress veebibrauserisse:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file +Your ${brand} Account

${brand_label_url}

Kinnita oma e-posti aadress

${email} was used to register on ${brand}.
Click the button to verify your address.

 
Kinnita
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/et/user/email/activation.txt b/services/brig/deb/opt/brig/templates/et/user/email/activation.txt index 3e98f0aad17..9fd3d1c07e3 100644 --- a/services/brig/deb/opt/brig/templates/et/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/et/user/email/activation.txt @@ -6,8 +6,8 @@ KINNITA OMA E-POSTI AADRESS ${email} was used to register on ${brand}. Click the button to verify your address. -Kinnita [${url}]Kui sul pole võimalik nuppu klikkida, siis kopeeri allolev -aadress veebibrauserisse: +Kinnita [${url}]If you can’t select the button, copy and paste this link to your +browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/et/user/email/deletion.html b/services/brig/deb/opt/brig/templates/et/user/email/deletion.html index 36eb10cdf00..4b31965c646 100644 --- a/services/brig/deb/opt/brig/templates/et/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/et/user/email/deletion.html @@ -1 +1 @@ -Kustuta konto?

${brand_label_url}

Kustuta konto

We’ve received a request to delete your ${brand} account. Kogu kontoga seotud info kustutamise kinnitamiseks kliki kümne minuti jooksul alloleval lingil.

 
Kustuta konto
 

Kui sul pole võimalik nuppu klikkida, siis kopeeri allolev aadress veebibrauserisse:

${url}

If you didn’t request this, reset your password.

If you have any questions, please contact us.

                                                           
\ No newline at end of file +Kustuta konto?

${brand_label_url}

Kustuta konto

We’ve received a request to delete your ${brand} account. Kogu kontoga seotud info kustutamise kinnitamiseks kliki kümne minuti jooksul alloleval lingil.

 
Kustuta konto
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you didn’t request this, reset your password.

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/et/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/et/user/email/deletion.txt index 76dfe344a41..852f61239a9 100644 --- a/services/brig/deb/opt/brig/templates/et/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/et/user/email/deletion.txt @@ -6,8 +6,8 @@ KUSTUTA KONTO We’ve received a request to delete your ${brand} account. Kogu kontoga seotud info kustutamise kinnitamiseks kliki kümne minuti jooksul alloleval lingil. -Kustuta konto [${url}]Kui sul pole võimalik nuppu klikkida, siis kopeeri allolev -aadress veebibrauserisse: +Kustuta konto [${url}]If you can’t select the button, copy and paste this link +to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/et/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/et/user/email/password-reset.html index 2055ca88695..15430614298 100644 --- a/services/brig/deb/opt/brig/templates/et/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/et/user/email/password-reset.html @@ -1 +1 @@ -Password Change at ${brand}

${brand_label_url}

Lähtesta oma parool

We’ve received a request to reset the password for your ${brand} account. Uue salasõna loomiseks vajutage järgmisele lingile:

 
Lähesta parool
 

Kui sul pole võimalik nuppu klikkida, siis kopeeri allolev aadress veebibrauserisse:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file +Password Change at ${brand}

${brand_label_url}

Lähtesta oma parool

We’ve received a request to reset the password for your ${brand} account. Uue salasõna loomiseks vajutage järgmisele lingile:

 
Lähesta parool
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/et/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/et/user/email/password-reset.txt index 5af078b790f..81cdf9ad7b2 100644 --- a/services/brig/deb/opt/brig/templates/et/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/et/user/email/password-reset.txt @@ -6,8 +6,8 @@ LÄHTESTA OMA PAROOL We’ve received a request to reset the password for your ${brand} account. Uue salasõna loomiseks vajutage järgmisele lingile: -Lähesta parool [${url}]Kui sul pole võimalik nuppu klikkida, siis kopeeri -allolev aadress veebibrauserisse: +Lähesta parool [${url}]If you can’t select the button, copy and paste this link +to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/et/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/et/user/email/team-activation.html index d042ee19056..e17b5ef7269 100644 --- a/services/brig/deb/opt/brig/templates/et/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/et/user/email/team-activation.html @@ -1 +1 @@ -${brand} Account

${brand_label_url}

Your new account on ${brand}

A new ${brand} team was created with ${email}. Palun kinnita oma meiliaadress.

 
Kinnita
 

Kui sul pole võimalik nuppu klikkida, siis kopeeri allolev aadress veebibrauserisse:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file +${brand} Account

${brand_label_url}

Your new account on ${brand}

A new ${brand} team was created with ${email}. Palun kinnita oma meiliaadress.

 
Kinnita
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/et/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/et/user/email/team-activation.txt index a323a447e26..e70b4d0b21e 100644 --- a/services/brig/deb/opt/brig/templates/et/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/et/user/email/team-activation.txt @@ -5,8 +5,8 @@ ${brand_label_url} [${brand_url}] YOUR NEW ACCOUNT ON ${brand} A new ${brand} team was created with ${email}. Palun kinnita oma meiliaadress. -Kinnita [${url}]Kui sul pole võimalik nuppu klikkida, siis kopeeri allolev -aadress veebibrauserisse: +Kinnita [${url}]If you can’t select the button, copy and paste this link to your +browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/et/user/email/update.html b/services/brig/deb/opt/brig/templates/et/user/email/update.html index 92c86559af6..f0305744870 100644 --- a/services/brig/deb/opt/brig/templates/et/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/et/user/email/update.html @@ -1 +1 @@ -Your new email address on ${brand}

${brand_label_url}

Kinnita oma e-posti aadress

${email} was registered as your new email address on ${brand}. Aadressi kinnitamiseks kliki alloleval lingil.

 
Kinnita
 

Kui sul pole võimalik nuppu klikkida, siis kopeeri allolev aadress veebibrauserisse:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file +Your new email address on ${brand}

${brand_label_url}

Kinnita oma e-posti aadress

${email} was registered as your new email address on ${brand}. Aadressi kinnitamiseks kliki alloleval lingil.

 
Kinnita
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/et/user/email/update.txt b/services/brig/deb/opt/brig/templates/et/user/email/update.txt index b9808bff228..4fb4fd47018 100644 --- a/services/brig/deb/opt/brig/templates/et/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/et/user/email/update.txt @@ -6,8 +6,8 @@ KINNITA OMA E-POSTI AADRESS ${email} was registered as your new email address on ${brand}. Aadressi kinnitamiseks kliki alloleval lingil. -Kinnita [${url}]Kui sul pole võimalik nuppu klikkida, siis kopeeri allolev -aadress veebibrauserisse: +Kinnita [${url}]If you can’t select the button, copy and paste this link to your +browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/fr/user/email/activation.html b/services/brig/deb/opt/brig/templates/fr/user/email/activation.html index 8435bf74e19..0c29777cb98 100644 --- a/services/brig/deb/opt/brig/templates/fr/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/fr/user/email/activation.html @@ -1 +1 @@ -Votre Compte ${brand}

${brand_label_url}

Vérification de votre adresse email

${email} a été utilisé pour s'enregistrer sur ${brand}.
Cliquez sur le bouton ci-dessous pour vérifier votre adresse.

 
Vérifier
 

Si vous ne pouvez pas cliquer sur le bouton, copiez et collez ce lien dans votre navigateur :

${url}

Si vous avez des questions, veuillez nous contacter.

                                                           
\ No newline at end of file +Votre Compte ${brand}

${brand_label_url}

Vérification de votre adresse email

${email} a été utilisé pour s'enregistrer sur ${brand}.
Cliquez sur le bouton ci-dessous pour vérifier votre adresse.

 
Vérifier
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Si vous avez des questions, veuillez nous contacter.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/fr/user/email/activation.txt b/services/brig/deb/opt/brig/templates/fr/user/email/activation.txt index 3a1dce3c8ff..e8cd745c33a 100644 --- a/services/brig/deb/opt/brig/templates/fr/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/fr/user/email/activation.txt @@ -6,8 +6,8 @@ VÉRIFICATION DE VOTRE ADRESSE EMAIL ${email} a été utilisé pour s'enregistrer sur ${brand}. Cliquez sur le bouton ci-dessous pour vérifier votre adresse. -Vérifier [${url}]Si vous ne pouvez pas cliquer sur le bouton, copiez et collez -ce lien dans votre navigateur : +Vérifier [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/fr/user/email/deletion.html b/services/brig/deb/opt/brig/templates/fr/user/email/deletion.html index 331a6b0cbcf..e2d9b2cef1d 100644 --- a/services/brig/deb/opt/brig/templates/fr/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/fr/user/email/deletion.html @@ -1 +1 @@ -Supprimer votre compte ?

${brand_label_url}

Supprimer votre compte

Nous avons reçu une demande de suppression de votre compte ${brand}. Cliquez sur le lien ci-dessous dans les 10 minutes pour supprimer toutes vos conversations, contenus et connexions.

 
Supprimer le compte
 

Si vous ne pouvez pas cliquer sur le bouton, copiez et collez ce lien dans votre navigateur :

${url}

Si vous n'êtes pas à l'origine de cette demande, réinitialisez votre mot de passe.

Si vous avez des questions, veuillez nous contacter.

                                                           
\ No newline at end of file +Supprimer votre compte ?

${brand_label_url}

Supprimer votre compte

Nous avons reçu une demande de suppression de votre compte ${brand}. Cliquez sur le lien ci-dessous dans les 10 minutes pour supprimer toutes vos conversations, contenus et connexions.

 
Supprimer le compte
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Si vous n'êtes pas à l'origine de cette demande, réinitialisez votre mot de passe.

Si vous avez des questions, veuillez nous contacter.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/fr/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/fr/user/email/deletion.txt index 5ddec54da01..377579c30d4 100644 --- a/services/brig/deb/opt/brig/templates/fr/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/fr/user/email/deletion.txt @@ -7,8 +7,8 @@ Nous avons reçu une demande de suppression de votre compte ${brand}. Cliquez su le lien ci-dessous dans les 10 minutes pour supprimer toutes vos conversations, contenus et connexions. -Supprimer le compte [${url}]Si vous ne pouvez pas cliquer sur le bouton, copiez -et collez ce lien dans votre navigateur : +Supprimer le compte [${url}]If you can’t select the button, copy and paste this +link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/fr/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/fr/user/email/password-reset.html index 02cc9de42e7..a45bbd145cf 100644 --- a/services/brig/deb/opt/brig/templates/fr/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/fr/user/email/password-reset.html @@ -1 +1 @@ -Réinitialisation du mot de passe ${brand}

${brand_label_url}

Réinitialiser votre mot de passe

Nous avons reçu une demande pour réinitialiser le mot de passe de votre compte ${brand}. Pour créer un nouveau mot de passe, cliquez sur le bouton ci-dessous.

 
Réinitialiser le mot de passe
 

Si vous ne pouvez pas cliquer sur le bouton, copiez et collez ce lien dans votre navigateur :

${url}

Si vous avez des questions, veuillez nous contacter.

                                                           
\ No newline at end of file +Réinitialisation du mot de passe ${brand}

${brand_label_url}

Réinitialiser votre mot de passe

Nous avons reçu une demande pour réinitialiser le mot de passe de votre compte ${brand}. Pour créer un nouveau mot de passe, cliquez sur le bouton ci-dessous.

 
Réinitialiser le mot de passe
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Si vous avez des questions, veuillez nous contacter.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/fr/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/fr/user/email/password-reset.txt index 4462d79c348..dd813b8933f 100644 --- a/services/brig/deb/opt/brig/templates/fr/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/fr/user/email/password-reset.txt @@ -6,8 +6,8 @@ RÉINITIALISER VOTRE MOT DE PASSE Nous avons reçu une demande pour réinitialiser le mot de passe de votre compte ${brand}. Pour créer un nouveau mot de passe, cliquez sur le bouton ci-dessous. -Réinitialiser le mot de passe [${url}]Si vous ne pouvez pas cliquer sur le -bouton, copiez et collez ce lien dans votre navigateur : +Réinitialiser le mot de passe [${url}]If you can’t select the button, copy and +paste this link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/fr/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/fr/user/email/team-activation.html index d4450a20a3d..c4d27a37706 100644 --- a/services/brig/deb/opt/brig/templates/fr/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/fr/user/email/team-activation.html @@ -1 +1 @@ -Compte ${brand}

${brand_label_url}

Votre nouveau compte ${brand}

Une nouvelle équipé a été créée sur ${brand} avec ${email}. Veuillez vérifier votre adresse email s’il vous plaît.

 
Vérifier
 

Si vous ne pouvez pas cliquer sur le bouton, copiez et collez ce lien dans votre navigateur :

${url}

Si vous avez des questions, veuillez nous contacter.

                                                           
\ No newline at end of file +Compte ${brand}

${brand_label_url}

Votre nouveau compte ${brand}

Une nouvelle équipé a été créée sur ${brand} avec ${email}. Veuillez vérifier votre adresse email s’il vous plaît.

 
Vérifier
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Si vous avez des questions, veuillez nous contacter.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/fr/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/fr/user/email/team-activation.txt index ab5984b3312..aa94af39e3e 100644 --- a/services/brig/deb/opt/brig/templates/fr/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/fr/user/email/team-activation.txt @@ -6,8 +6,8 @@ VOTRE NOUVEAU COMPTE ${brand} Une nouvelle équipé a été créée sur ${brand} avec ${email}. Veuillez vérifier votre adresse email s’il vous plaît. -Vérifier [${url}]Si vous ne pouvez pas cliquer sur le bouton, copiez et collez -ce lien dans votre navigateur : +Vérifier [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/fr/user/email/update.html b/services/brig/deb/opt/brig/templates/fr/user/email/update.html index 5aeb1430126..fba83729283 100644 --- a/services/brig/deb/opt/brig/templates/fr/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/fr/user/email/update.html @@ -1 +1 @@ -Votre nouvelle adresse e-mail sur ${brand}

${brand_label_url}

Vérification de votre adresse email

${email} a été enregistré comme votre nouvelle adresse email sur ${brand}. Veuillez vérifier votre email s’il vous plaît. Cliquez sur le bouton ci-dessous pour vérifier votre adresse email.

 
Vérifier
 

Si vous ne pouvez pas cliquer sur le bouton, copiez et collez ce lien dans votre navigateur :

${url}

Si vous avez des questions, veuillez nous contacter.

                                                           
\ No newline at end of file +Votre nouvelle adresse e-mail sur ${brand}

${brand_label_url}

Vérification de votre adresse email

${email} a été enregistré comme votre nouvelle adresse email sur ${brand}. Veuillez vérifier votre email s’il vous plaît. Cliquez sur le bouton ci-dessous pour vérifier votre adresse email.

 
Vérifier
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Si vous avez des questions, veuillez nous contacter.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/fr/user/email/update.txt b/services/brig/deb/opt/brig/templates/fr/user/email/update.txt index 2517b9a9328..cc719c283a7 100644 --- a/services/brig/deb/opt/brig/templates/fr/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/fr/user/email/update.txt @@ -7,8 +7,8 @@ ${email} a été enregistré comme votre nouvelle adresse email sur ${brand}. Veuillez vérifier votre email s’il vous plaît. Cliquez sur le bouton ci-dessous pour vérifier votre adresse email. -Vérifier [${url}]Si vous ne pouvez pas cliquer sur le bouton, copiez et collez -ce lien dans votre navigateur : +Vérifier [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/index.html b/services/brig/deb/opt/brig/templates/index.html index f0d4029dc3a..76b2de86f7b 100644 --- a/services/brig/deb/opt/brig/templates/index.html +++ b/services/brig/deb/opt/brig/templates/index.html @@ -4,4 +4,4 @@ link.rel = 'stylesheet'; link.href = '//cdnjs.cloudflare.com/ajax/libs/flag-icon-css/2.9.0/css/flag-icon.min.css'; document.head.appendChild(link); - }
 

Wire Email Templates Preview

Click the links below to display the content of each message:

Provider
  1. Activationtxt
  2. Approval confirmtxt
  3. Approval requesttxt
Team
  1. Invitationtxt
  2. New member welcometxt
User
  1. Activationtxt
  2. Deletiontxt
  3. New clienttxt
  4. Password resettxt
  5. Updatetxt
  6. Verificationtxt
  7. Team activationtxt
  8. Second factor verification for logintxt
  9. Second factor verification create SCIM tokentxt
  10. Second factor verification delete teamtxt
Billing
  1. Suspensiontxt

For source and instructions, see github.com/wireapp/wire-emails or visit the Crowdin project to help with translations.

                                                           
\ No newline at end of file + }
 

Wire Email Templates Preview

Click the links below to display the content of each message:

Provider
  1. Activationtxt
  2. Approval confirmtxt
  3. Approval requesttxt
Team
  1. Invitationtxt
  2. New member welcometxt
  3. Migration from private to team usertxt
User
  1. Activationtxt
  2. Deletiontxt
  3. New clienttxt
  4. Password resettxt
  5. Updatetxt
  6. Verificationtxt
  7. Team activationtxt
  8. Second factor verification for logintxt
  9. Second factor verification create SCIM tokentxt
  10. Second factor verification delete teamtxt
Billing
  1. Suspensiontxt

For source and instructions, see github.com/wireapp/wire-emails or visit the Crowdin project to help with translations.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/it/user/email/activation.html b/services/brig/deb/opt/brig/templates/it/user/email/activation.html index 0812e96af31..ec62e15dc1a 100644 --- a/services/brig/deb/opt/brig/templates/it/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/it/user/email/activation.html @@ -1 +1 @@ -Il tuo account ${brand}

${brand_label_url}

Verifica il tuo indirizzo e-mail

${email} è stato utilizzata per registrarsi su ${brand}.
Clicca il pulsante per verificare il tuo indirizzo.

 
Verifica
 

Se non puoi fare clic sul pulsante, copia e incolla questo link nel tuo browser:

${url}

Se hai domande, per favore contattaci.

                                                           
\ No newline at end of file +Il tuo account ${brand}

${brand_label_url}

Verifica il tuo indirizzo e-mail

${email} è stato utilizzata per registrarsi su ${brand}.
Clicca il pulsante per verificare il tuo indirizzo.

 
Verifica
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Se hai domande, per favore contattaci.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/it/user/email/activation.txt b/services/brig/deb/opt/brig/templates/it/user/email/activation.txt index b655bab11cb..10cb398928b 100644 --- a/services/brig/deb/opt/brig/templates/it/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/it/user/email/activation.txt @@ -6,8 +6,8 @@ VERIFICA IL TUO INDIRIZZO E-MAIL ${email} è stato utilizzata per registrarsi su ${brand}. Clicca il pulsante per verificare il tuo indirizzo. -Verifica [${url}]Se non puoi fare clic sul pulsante, copia e incolla questo link -nel tuo browser: +Verifica [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/it/user/email/deletion.html b/services/brig/deb/opt/brig/templates/it/user/email/deletion.html index 10d09fa2521..de6afb8ce49 100644 --- a/services/brig/deb/opt/brig/templates/it/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/it/user/email/deletion.html @@ -1 +1 @@ -Eliminare account?

${brand_label_url}

Elimina il tuo account

Abbiamo ricevuto una richiesta per eliminare il tuo account ${brand}. Clicca sul pulsante qui sotto entro 10 minuti per eliminare tutte le conversazioni, i contenuti e le connessioni.

 
Elimina account
 

Se non puoi fare clic sul pulsante, copia e incolla questo link nel tuo browser:

${url}

Se non lo hai richiesto, reimposta la password.

Se hai domande, per favore contattaci.

                                                           
\ No newline at end of file +Eliminare account?

${brand_label_url}

Elimina il tuo account

Abbiamo ricevuto una richiesta per eliminare il tuo account ${brand}. Clicca sul pulsante qui sotto entro 10 minuti per eliminare tutte le conversazioni, i contenuti e le connessioni.

 
Elimina account
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Se non lo hai richiesto, reimposta la password.

Se hai domande, per favore contattaci.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/it/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/it/user/email/deletion.txt index 376449de73c..478a0adba4d 100644 --- a/services/brig/deb/opt/brig/templates/it/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/it/user/email/deletion.txt @@ -7,8 +7,8 @@ Abbiamo ricevuto una richiesta per eliminare il tuo account ${brand}. Clicca sul pulsante qui sotto entro 10 minuti per eliminare tutte le conversazioni, i contenuti e le connessioni. -Elimina account [${url}]Se non puoi fare clic sul pulsante, copia e incolla -questo link nel tuo browser: +Elimina account [${url}]If you can’t select the button, copy and paste this link +to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/it/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/it/user/email/password-reset.html index 475f1d82b97..1b33b8f07fa 100644 --- a/services/brig/deb/opt/brig/templates/it/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/it/user/email/password-reset.html @@ -1 +1 @@ -Cambio di password di ${brand}

${brand_label_url}

Reimposta la tua password

Abbiamo ricevuto una richiesta di reimpostazione della password del tuo account ${brand}. Per creare una nuova password, fai clic sul pulsante qui sotto.

 
Reimposta password
 

Se non puoi fare clic sul pulsante, copia e incolla questo link nel tuo browser:

${url}

Se hai domande, per favore contattaci.

                                                           
\ No newline at end of file +Cambio di password di ${brand}

${brand_label_url}

Reimposta la tua password

Abbiamo ricevuto una richiesta di reimpostazione della password del tuo account ${brand}. Per creare una nuova password, fai clic sul pulsante qui sotto.

 
Reimposta password
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Se hai domande, per favore contattaci.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/it/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/it/user/email/password-reset.txt index 3aa152a0db3..e501ff726b0 100644 --- a/services/brig/deb/opt/brig/templates/it/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/it/user/email/password-reset.txt @@ -6,8 +6,8 @@ REIMPOSTA LA TUA PASSWORD Abbiamo ricevuto una richiesta di reimpostazione della password del tuo account ${brand}. Per creare una nuova password, fai clic sul pulsante qui sotto. -Reimposta password [${url}]Se non puoi fare clic sul pulsante, copia e incolla -questo link nel tuo browser: +Reimposta password [${url}]If you can’t select the button, copy and paste this +link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/it/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/it/user/email/team-activation.html index 0f6eeadb38b..5ce23562d3f 100644 --- a/services/brig/deb/opt/brig/templates/it/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/it/user/email/team-activation.html @@ -1 +1 @@ -Profilo di ${brand}

${brand_label_url}

Il tuo nuovo profilo su ${brand}

Un nuovo team di ${brand} è stato creato con ${email}. Sei pregato di verificare la tua email.

 
Verifica
 

Se non puoi fare clic sul pulsante, copia e incolla questo link nel tuo browser:

${url}

Se hai domande, per favore contattaci.

                                                           
\ No newline at end of file +Profilo di ${brand}

${brand_label_url}

Il tuo nuovo profilo su ${brand}

Un nuovo team di ${brand} è stato creato con ${email}. Sei pregato di verificare la tua email.

 
Verifica
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Se hai domande, per favore contattaci.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/it/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/it/user/email/team-activation.txt index 83096c121e5..d208c53ac6c 100644 --- a/services/brig/deb/opt/brig/templates/it/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/it/user/email/team-activation.txt @@ -6,8 +6,8 @@ IL TUO NUOVO PROFILO SU ${brand} Un nuovo team di ${brand} è stato creato con ${email}. Sei pregato di verificare la tua email. -Verifica [${url}]Se non puoi fare clic sul pulsante, copia e incolla questo link -nel tuo browser: +Verifica [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/it/user/email/update.html b/services/brig/deb/opt/brig/templates/it/user/email/update.html index 0685328ddb6..aa6ae28ed3c 100644 --- a/services/brig/deb/opt/brig/templates/it/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/it/user/email/update.html @@ -1 +1 @@ -Il tuo nuovo indirizzo email su ${brand}

${brand_label_url}

Verifica il tuo indirizzo e-mail

${email} è stato registrato come tuo nuovo indirizzo email su ${brand}. Clicca il pulsante sotto per verificare il tuo indirizzo.

 
Verifica
 

Se non puoi fare clic sul pulsante, copia e incolla questo link nel tuo browser:

${url}

Se hai domande, per favore contattaci.

                                                           
\ No newline at end of file +Il tuo nuovo indirizzo email su ${brand}

${brand_label_url}

Verifica il tuo indirizzo e-mail

${email} è stato registrato come tuo nuovo indirizzo email su ${brand}. Clicca il pulsante sotto per verificare il tuo indirizzo.

 
Verifica
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Se hai domande, per favore contattaci.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/it/user/email/update.txt b/services/brig/deb/opt/brig/templates/it/user/email/update.txt index 881ea68a8b0..69b7a35f63b 100644 --- a/services/brig/deb/opt/brig/templates/it/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/it/user/email/update.txt @@ -6,8 +6,8 @@ VERIFICA IL TUO INDIRIZZO E-MAIL ${email} è stato registrato come tuo nuovo indirizzo email su ${brand}. Clicca il pulsante sotto per verificare il tuo indirizzo. -Verifica [${url}]Se non puoi fare clic sul pulsante, copia e incolla questo link -nel tuo browser: +Verifica [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/ja/user/email/activation.html b/services/brig/deb/opt/brig/templates/ja/user/email/activation.html index 5628de34d87..4961e529590 100644 --- a/services/brig/deb/opt/brig/templates/ja/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/ja/user/email/activation.html @@ -1 +1 @@ -あなたの ${brand} アカウント

${brand_label_url}

メールアドレス認証

${email} は、${brand} への登録に使用されました。
ボタンをクリックしてメールアドレスの認証を行ってください。

 
認証
 

ボタンをクリックできない場合は、以下のリンクをブラウザにコピー&ペーストして下さい。

${url}

ご不明な点がございましたら、 こちら から私たちにご連絡ください。

                                                           
\ No newline at end of file +あなたの ${brand} アカウント

${brand_label_url}

メールアドレス認証

${email} は、${brand} への登録に使用されました。
ボタンをクリックしてメールアドレスの認証を行ってください。

 
認証
 

If you can’t select the button, copy and paste this link to your browser:

${url}

ご不明な点がございましたら、 こちら から私たちにご連絡ください。

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/ja/user/email/activation.txt b/services/brig/deb/opt/brig/templates/ja/user/email/activation.txt index 9b1e2f77f15..c1e78cb1a8d 100644 --- a/services/brig/deb/opt/brig/templates/ja/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/ja/user/email/activation.txt @@ -6,7 +6,8 @@ ${brand_label_url} [${brand_url}] ${email} は、${brand} への登録に使用されました。 ボタンをクリックしてメールアドレスの認証を行ってください。 -認証 [${url}]ボタンをクリックできない場合は、以下のリンクをブラウザにコピー&ペーストして下さい。 +認証 [${url}]If you can’t select the button, copy and paste this link to your +browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/ja/user/email/deletion.html b/services/brig/deb/opt/brig/templates/ja/user/email/deletion.html index 72e2c8d9330..e41f86d5a71 100644 --- a/services/brig/deb/opt/brig/templates/ja/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/ja/user/email/deletion.html @@ -1 +1 @@ -アカウントを削除しますか?

${brand_label_url}

アカウントを削除

あなたの ${brand} アカウントの削除リクエストを受け付けました。 あなたのすべての会話、コンテンツ、友人を削除するには10分以内に下記のリンクをクリックしてください。

 
アカウント削除
 

ボタンをクリックできない場合は、以下のリンクをブラウザにコピー&ペーストして下さい。

${url}

あなたがこのリクエスト行っていない場合は、パスワードをリセットしてください。

ご不明な点がございましたら、 こちら から私たちにご連絡ください。

                                                           
\ No newline at end of file +アカウントを削除しますか?

${brand_label_url}

アカウントを削除

あなたの ${brand} アカウントの削除リクエストを受け付けました。 あなたのすべての会話、コンテンツ、友人を削除するには10分以内に下記のリンクをクリックしてください。

 
アカウント削除
 

If you can’t select the button, copy and paste this link to your browser:

${url}

あなたがこのリクエスト行っていない場合は、パスワードをリセットしてください。

ご不明な点がございましたら、 こちら から私たちにご連絡ください。

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/ja/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/ja/user/email/deletion.txt index d11c0c14f8c..77192fb1a2f 100644 --- a/services/brig/deb/opt/brig/templates/ja/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/ja/user/email/deletion.txt @@ -6,7 +6,8 @@ ${brand_label_url} [${brand_url}] あなたの ${brand} アカウントの削除リクエストを受け付けました。 あなたのすべての会話、コンテンツ、友人を削除するには10分以内に下記のリンクをクリックしてください。 -アカウント削除 [${url}]ボタンをクリックできない場合は、以下のリンクをブラウザにコピー&ペーストして下さい。 +アカウント削除 [${url}]If you can’t select the button, copy and paste this link to your +browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/ja/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/ja/user/email/password-reset.html index eb0d59f2aa7..940737c58f6 100644 --- a/services/brig/deb/opt/brig/templates/ja/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/ja/user/email/password-reset.html @@ -1 +1 @@ -${brand} でのパスワードリセット

${brand_label_url}

パスワードリセット

${brand} アカウントのパスワードをリセット要求を受け取りました。 新しいパスワードを作成するには、以下のボタンをクリックしてください。

 
パスワードリセット
 

ボタンをクリックできない場合は、以下のリンクをブラウザにコピー&ペーストして下さい。

${url}

ご不明な点がございましたら、 こちら から私たちにご連絡ください。

                                                           
\ No newline at end of file +${brand} でのパスワードリセット

${brand_label_url}

パスワードリセット

${brand} アカウントのパスワードをリセット要求を受け取りました。 新しいパスワードを作成するには、以下のボタンをクリックしてください。

 
パスワードリセット
 

If you can’t select the button, copy and paste this link to your browser:

${url}

ご不明な点がございましたら、 こちら から私たちにご連絡ください。

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/ja/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/ja/user/email/password-reset.txt index 0ffdc49b479..fe62bcf77d5 100644 --- a/services/brig/deb/opt/brig/templates/ja/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/ja/user/email/password-reset.txt @@ -5,7 +5,8 @@ ${brand_label_url} [${brand_url}] パスワードリセット ${brand} アカウントのパスワードをリセット要求を受け取りました。 新しいパスワードを作成するには、以下のボタンをクリックしてください。 -パスワードリセット [${url}]ボタンをクリックできない場合は、以下のリンクをブラウザにコピー&ペーストして下さい。 +パスワードリセット [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/ja/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/ja/user/email/team-activation.html index 63c73884208..4f34cac8c64 100644 --- a/services/brig/deb/opt/brig/templates/ja/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/ja/user/email/team-activation.html @@ -1 +1 @@ -${brand} アカウント

${brand_label_url}

あなたの新しい ${brand} アカウント

新しい ${brand} チーム が、 ${email} によって作成されました。 メールアドレスの認証をお願いします。

 
認証
 

ボタンをクリックできない場合は、以下のリンクをブラウザにコピー&ペーストして下さい。

${url}

ご不明な点がございましたら、 こちら から私たちにご連絡ください。

                                                           
\ No newline at end of file +${brand} アカウント

${brand_label_url}

あなたの新しい ${brand} アカウント

新しい ${brand} チーム が、 ${email} によって作成されました。 メールアドレスの認証をお願いします。

 
認証
 

If you can’t select the button, copy and paste this link to your browser:

${url}

ご不明な点がございましたら、 こちら から私たちにご連絡ください。

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/ja/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/ja/user/email/team-activation.txt index 89248d20a57..919484eb42d 100644 --- a/services/brig/deb/opt/brig/templates/ja/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/ja/user/email/team-activation.txt @@ -5,7 +5,8 @@ ${brand_label_url} [${brand_url}] あなたの新しい ${brand} アカウント 新しい ${brand} チーム が、 ${email} によって作成されました。 メールアドレスの認証をお願いします。 -認証 [${url}]ボタンをクリックできない場合は、以下のリンクをブラウザにコピー&ペーストして下さい。 +認証 [${url}]If you can’t select the button, copy and paste this link to your +browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/ja/user/email/update.html b/services/brig/deb/opt/brig/templates/ja/user/email/update.html index 8a0b25f9a3f..864cf8dea86 100644 --- a/services/brig/deb/opt/brig/templates/ja/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/ja/user/email/update.html @@ -1 +1 @@ -${brand} での新しいメールアドレス

${brand_label_url}

メールアドレス認証

${email} は、 ${brand} で新しいメールアドレスとして登録されました。 新しいメールアドレスを認証するために下のボタンをクリックしてください。

 
認証
 

ボタンをクリックできない場合は、以下のリンクをブラウザにコピー&ペーストして下さい。

${url}

ご不明な点がございましたら、 こちら から私たちにご連絡ください。

                                                           
\ No newline at end of file +${brand} での新しいメールアドレス

${brand_label_url}

メールアドレス認証

${email} は、 ${brand} で新しいメールアドレスとして登録されました。 新しいメールアドレスを認証するために下のボタンをクリックしてください。

 
認証
 

If you can’t select the button, copy and paste this link to your browser:

${url}

ご不明な点がございましたら、 こちら から私たちにご連絡ください。

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/ja/user/email/update.txt b/services/brig/deb/opt/brig/templates/ja/user/email/update.txt index bb5992939f7..9efec032298 100644 --- a/services/brig/deb/opt/brig/templates/ja/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/ja/user/email/update.txt @@ -5,7 +5,8 @@ ${brand_label_url} [${brand_url}] メールアドレス認証 ${email} は、 ${brand} で新しいメールアドレスとして登録されました。 新しいメールアドレスを認証するために下のボタンをクリックしてください。 -認証 [${url}]ボタンをクリックできない場合は、以下のリンクをブラウザにコピー&ペーストして下さい。 +認証 [${url}]If you can’t select the button, copy and paste this link to your +browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/lt/user/email/activation.html b/services/brig/deb/opt/brig/templates/lt/user/email/activation.html index 1fb608768bc..3ac19fa7576 100644 --- a/services/brig/deb/opt/brig/templates/lt/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/lt/user/email/activation.html @@ -1 +1 @@ -Jūsų „${brand}“ paskyra

${brand_label_url}

Patvirtinkite savo el. paštą

${email} buvo panaudotas, registruojantis „${brand}“.
Norėdami patvirtinti savo adresą, spustelėkite mygtuką.

 
Patvirtinti
 

Jeigu negalite spustelėti ant mygtuko, nukopijuokite ir įdėkite šią nuorodą į savo naršyklę:

${url}

Jei turite klausimų, susisiekite su mumis.

                                                           
\ No newline at end of file +Jūsų „${brand}“ paskyra

${brand_label_url}

Patvirtinkite savo el. paštą

${email} buvo panaudotas, registruojantis „${brand}“.
Norėdami patvirtinti savo adresą, spustelėkite mygtuką.

 
Patvirtinti
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Jei turite klausimų, susisiekite su mumis.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/lt/user/email/activation.txt b/services/brig/deb/opt/brig/templates/lt/user/email/activation.txt index bf8c020d177..f7597745ca2 100644 --- a/services/brig/deb/opt/brig/templates/lt/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/lt/user/email/activation.txt @@ -6,8 +6,8 @@ PATVIRTINKITE SAVO EL. PAŠTĄ ${email} buvo panaudotas, registruojantis „${brand}“. Norėdami patvirtinti savo adresą, spustelėkite mygtuką. -Patvirtinti [${url}]Jeigu negalite spustelėti ant mygtuko, nukopijuokite ir -įdėkite šią nuorodą į savo naršyklę: +Patvirtinti [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/lt/user/email/deletion.html b/services/brig/deb/opt/brig/templates/lt/user/email/deletion.html index 21df982477e..d72c2f5799e 100644 --- a/services/brig/deb/opt/brig/templates/lt/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/lt/user/email/deletion.html @@ -1 +1 @@ -Ištrinti paskyrą?

${brand_label_url}

Ištrinti jūsų paskyrą

Mes gavome užklausą ištrinti jūsų ${brand} paskyrą. Norėdami ištrinti visus savo pokalbius, visą turinį ir ryšius, 10 minučių bėgyje spustelėkite žemiau esantį mygtuką.

 
Ištrinti paskyrą
 

Jeigu negalite spustelėti ant mygtuko, nukopijuokite ir įdėkite šią nuorodą į savo naršyklę:

${url}

Jeigu jūs nebuvote to užklausę, atstatykite savo slaptažodį.

Jei turite klausimų, susisiekite su mumis.

                                                           
\ No newline at end of file +Ištrinti paskyrą?

${brand_label_url}

Ištrinti jūsų paskyrą

Mes gavome užklausą ištrinti jūsų ${brand} paskyrą. Norėdami ištrinti visus savo pokalbius, visą turinį ir ryšius, 10 minučių bėgyje spustelėkite žemiau esantį mygtuką.

 
Ištrinti paskyrą
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Jeigu jūs nebuvote to užklausę, atstatykite savo slaptažodį.

Jei turite klausimų, susisiekite su mumis.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/lt/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/lt/user/email/deletion.txt index 9ed2259ecc0..a647afdcb60 100644 --- a/services/brig/deb/opt/brig/templates/lt/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/lt/user/email/deletion.txt @@ -7,8 +7,8 @@ Mes gavome užklausą ištrinti jūsų ${brand} paskyrą. Norėdami ištrinti vi pokalbius, visą turinį ir ryšius, 10 minučių bėgyje spustelėkite žemiau esantį mygtuką. -Ištrinti paskyrą [${url}]Jeigu negalite spustelėti ant mygtuko, nukopijuokite ir -įdėkite šią nuorodą į savo naršyklę: +Ištrinti paskyrą [${url}]If you can’t select the button, copy and paste this +link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/lt/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/lt/user/email/password-reset.html index 1345946ca57..9f2eb657a00 100644 --- a/services/brig/deb/opt/brig/templates/lt/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/lt/user/email/password-reset.html @@ -1 +1 @@ -„${brand}“ slaptažodžio pakeitimas

${brand_label_url}

Atstatyti jūsų slaptažodį

Gavome užklausą atstatyti jūsų ${brand} paskyros slaptažodį. Norėdami susikurti naują slaptažodį, spustelėkite mygtuką žemiau.

 
Atstatyti slaptažodį
 

Jeigu negalite spustelėti ant mygtuko, nukopijuokite ir įdėkite šią nuorodą į savo naršyklę:

${url}

Jei turite klausimų, susisiekite su mumis.

                                                           
\ No newline at end of file +„${brand}“ slaptažodžio pakeitimas

${brand_label_url}

Atstatyti jūsų slaptažodį

Gavome užklausą atstatyti jūsų ${brand} paskyros slaptažodį. Norėdami susikurti naują slaptažodį, spustelėkite mygtuką žemiau.

 
Atstatyti slaptažodį
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Jei turite klausimų, susisiekite su mumis.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/lt/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/lt/user/email/password-reset.txt index 53da058a2d6..557987093fa 100644 --- a/services/brig/deb/opt/brig/templates/lt/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/lt/user/email/password-reset.txt @@ -6,8 +6,8 @@ ATSTATYTI JŪSŲ SLAPTAŽODĮ Gavome užklausą atstatyti jūsų ${brand} paskyros slaptažodį. Norėdami susikurti naują slaptažodį, spustelėkite mygtuką žemiau. -Atstatyti slaptažodį [${url}]Jeigu negalite spustelėti ant mygtuko, -nukopijuokite ir įdėkite šią nuorodą į savo naršyklę: +Atstatyti slaptažodį [${url}]If you can’t select the button, copy and paste this +link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/lt/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/lt/user/email/team-activation.html index 66def145ce8..97a6fc62b47 100644 --- a/services/brig/deb/opt/brig/templates/lt/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/lt/user/email/team-activation.html @@ -1 +1 @@ -„${brand}“ paskyra

${brand_label_url}

Jūsų nauja „${brand}“ paskyra

Naudojant ${email}, buvo sukurta nauja „${brand}“ komanda. Patvirtinkite savo el. paštą.

 
Patvirtinti
 

Jeigu negalite spustelėti ant mygtuko, nukopijuokite ir įdėkite šią nuorodą į savo naršyklę:

${url}

Jei turite klausimų, susisiekite su mumis.

                                                           
\ No newline at end of file +„${brand}“ paskyra

${brand_label_url}

Jūsų nauja „${brand}“ paskyra

Naudojant ${email}, buvo sukurta nauja „${brand}“ komanda. Patvirtinkite savo el. paštą.

 
Patvirtinti
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Jei turite klausimų, susisiekite su mumis.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/lt/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/lt/user/email/team-activation.txt index c6d6fd958a6..d3459c84aba 100644 --- a/services/brig/deb/opt/brig/templates/lt/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/lt/user/email/team-activation.txt @@ -6,8 +6,8 @@ JŪSŲ NAUJA „${brand}“ PASKYRA Naudojant ${email}, buvo sukurta nauja „${brand}“ komanda. Patvirtinkite savo el. paštą. -Patvirtinti [${url}]Jeigu negalite spustelėti ant mygtuko, nukopijuokite ir -įdėkite šią nuorodą į savo naršyklę: +Patvirtinti [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/lt/user/email/update.html b/services/brig/deb/opt/brig/templates/lt/user/email/update.html index e7bc8d4a286..b4c77aed14e 100644 --- a/services/brig/deb/opt/brig/templates/lt/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/lt/user/email/update.html @@ -1 +1 @@ -Jūsų naujas „${brand}“ el. pašto adresas

${brand_label_url}

Patvirtinkite savo el. paštą

${email} buvo užregistruotas kaip naujas „${brand}“ el. pašto adresas. Norėdami patvirtinti savo adresą, spustelėkite mygtuką žemiau.

 
Patvirtinti
 

Jeigu negalite spustelėti ant mygtuko, nukopijuokite ir įdėkite šią nuorodą į savo naršyklę:

${url}

Jei turite klausimų, susisiekite su mumis.

                                                           
\ No newline at end of file +Jūsų naujas „${brand}“ el. pašto adresas

${brand_label_url}

Patvirtinkite savo el. paštą

${email} buvo užregistruotas kaip naujas „${brand}“ el. pašto adresas. Norėdami patvirtinti savo adresą, spustelėkite mygtuką žemiau.

 
Patvirtinti
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Jei turite klausimų, susisiekite su mumis.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/lt/user/email/update.txt b/services/brig/deb/opt/brig/templates/lt/user/email/update.txt index f6d6c4eba1d..d7aafc33d36 100644 --- a/services/brig/deb/opt/brig/templates/lt/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/lt/user/email/update.txt @@ -6,8 +6,8 @@ PATVIRTINKITE SAVO EL. PAŠTĄ ${email} buvo užregistruotas kaip naujas „${brand}“ el. pašto adresas. Norėdami patvirtinti savo adresą, spustelėkite mygtuką žemiau. -Patvirtinti [${url}]Jeigu negalite spustelėti ant mygtuko, nukopijuokite ir -įdėkite šią nuorodą į savo naršyklę: +Patvirtinti [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/pl/user/email/activation.html b/services/brig/deb/opt/brig/templates/pl/user/email/activation.html index d264b4c23f5..de43cc47a89 100644 --- a/services/brig/deb/opt/brig/templates/pl/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/pl/user/email/activation.html @@ -1 +1 @@ -Twoje konto ${brand}

${brand_label_url}

Potwierdź swój adres email

${email} został użyty do rejestracji ${brand}.
Kliknij przycisk, aby zweryfikować swój adres.

 
Zweryfikuj
 

Jeśli nie możesz kliknąć przycisku, skopiuj i wklej ten link do swojej przeglądarki:

${url}

Jeśli masz jakieś pytania, prosimy skontaktuj się z nami.

                                                           
\ No newline at end of file +Twoje konto ${brand}

${brand_label_url}

Potwierdź swój adres email

${email} został użyty do rejestracji ${brand}.
Kliknij przycisk, aby zweryfikować swój adres.

 
Zweryfikuj
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Jeśli masz jakieś pytania, prosimy skontaktuj się z nami.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/pl/user/email/activation.txt b/services/brig/deb/opt/brig/templates/pl/user/email/activation.txt index 3921797a145..401df8e4561 100644 --- a/services/brig/deb/opt/brig/templates/pl/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/pl/user/email/activation.txt @@ -6,8 +6,8 @@ POTWIERDŹ SWÓJ ADRES EMAIL ${email} został użyty do rejestracji ${brand}. Kliknij przycisk, aby zweryfikować swój adres. -Zweryfikuj [${url}]Jeśli nie możesz kliknąć przycisku, skopiuj i wklej ten link -do swojej przeglądarki: +Zweryfikuj [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/pl/user/email/deletion.html b/services/brig/deb/opt/brig/templates/pl/user/email/deletion.html index 76459d62cb6..25ae76e5809 100644 --- a/services/brig/deb/opt/brig/templates/pl/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/pl/user/email/deletion.html @@ -1 +1 @@ -Usunąć konto?

${brand_label_url}

Usuń swoje konto

Otrzymaliśmy prośbę o usunięcie konta ${brand}. Kliknij przycisk poniżej w ciągu 10 minut, aby usunąć wszystkie konwersacje, treści i połączenia.

 
Usuń konto
 

Jeśli nie możesz kliknąć przycisku, skopiuj i wklej ten link do swojej przeglądarki:

${url}

Jeśli nie poprosiłeś o to, zresetuj swoje hasło.

Jeśli masz jakieś pytania, prosimy skontaktuj się z nami.

                                                           
\ No newline at end of file +Usunąć konto?

${brand_label_url}

Usuń swoje konto

Otrzymaliśmy prośbę o usunięcie konta ${brand}. Kliknij przycisk poniżej w ciągu 10 minut, aby usunąć wszystkie konwersacje, treści i połączenia.

 
Usuń konto
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Jeśli nie poprosiłeś o to, zresetuj swoje hasło.

Jeśli masz jakieś pytania, prosimy skontaktuj się z nami.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/pl/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/pl/user/email/deletion.txt index 33a4f532af8..c3f57e1e1ed 100644 --- a/services/brig/deb/opt/brig/templates/pl/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/pl/user/email/deletion.txt @@ -6,8 +6,8 @@ USUŃ SWOJE KONTO Otrzymaliśmy prośbę o usunięcie konta ${brand}. Kliknij przycisk poniżej w ciągu 10 minut, aby usunąć wszystkie konwersacje, treści i połączenia. -Usuń konto [${url}]Jeśli nie możesz kliknąć przycisku, skopiuj i wklej ten link -do swojej przeglądarki: +Usuń konto [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/pl/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/pl/user/email/password-reset.html index 139c47eb9ea..2ed71518521 100644 --- a/services/brig/deb/opt/brig/templates/pl/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/pl/user/email/password-reset.html @@ -1 +1 @@ -Zmiana hasła w ${brand}

${brand_label_url}

Zresetuj hasło

Otrzymaliśmy prośbę o zresetowanie hasła do Twojego konta ${brand}. Aby utworzyć nowe hasło, kliknij poniższy przycisk.

 
Zresetuj hasło
 

Jeśli nie możesz kliknąć przycisku, skopiuj i wklej ten link do swojej przeglądarki:

${url}

Jeśli masz jakieś pytania, prosimy skontaktuj się z nami.

                                                           
\ No newline at end of file +Zmiana hasła w ${brand}

${brand_label_url}

Zresetuj hasło

Otrzymaliśmy prośbę o zresetowanie hasła do Twojego konta ${brand}. Aby utworzyć nowe hasło, kliknij poniższy przycisk.

 
Zresetuj hasło
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Jeśli masz jakieś pytania, prosimy skontaktuj się z nami.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/pl/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/pl/user/email/password-reset.txt index 780fb104cc0..b7026373366 100644 --- a/services/brig/deb/opt/brig/templates/pl/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/pl/user/email/password-reset.txt @@ -6,8 +6,8 @@ ZRESETUJ HASŁO Otrzymaliśmy prośbę o zresetowanie hasła do Twojego konta ${brand}. Aby utworzyć nowe hasło, kliknij poniższy przycisk. -Zresetuj hasło [${url}]Jeśli nie możesz kliknąć przycisku, skopiuj i wklej ten -link do swojej przeglądarki: +Zresetuj hasło [${url}]If you can’t select the button, copy and paste this link +to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/pl/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/pl/user/email/team-activation.html index 067c12da167..1cb310daba2 100644 --- a/services/brig/deb/opt/brig/templates/pl/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/pl/user/email/team-activation.html @@ -1 +1 @@ -Konto ${brand}

${brand_label_url}

Twoje nowe konto na ${brand}

Nowy zespół ${brand} został utworzony z ${email}. Prosimy, zweryfikuj swój adres email.

 
Zweryfikuj
 

Jeśli nie możesz kliknąć przycisku, skopiuj i wklej ten link do swojej przeglądarki:

${url}

Jeśli masz jakieś pytania, prosimy skontaktuj się z nami.

                                                           
\ No newline at end of file +Konto ${brand}

${brand_label_url}

Twoje nowe konto na ${brand}

Nowy zespół ${brand} został utworzony z ${email}. Prosimy, zweryfikuj swój adres email.

 
Zweryfikuj
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Jeśli masz jakieś pytania, prosimy skontaktuj się z nami.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/pl/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/pl/user/email/team-activation.txt index f1054d62cfa..f448de9be4e 100644 --- a/services/brig/deb/opt/brig/templates/pl/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/pl/user/email/team-activation.txt @@ -2,12 +2,12 @@ ${brand_label_url} [${brand_url}] -TWOJE NOWE KONTO NA ${BRAND} +TWOJE NOWE KONTO NA ${brand} Nowy zespół ${brand} został utworzony z ${email}. Prosimy, zweryfikuj swój adres email. -Zweryfikuj [${url}]Jeśli nie możesz kliknąć przycisku, skopiuj i wklej ten link -do swojej przeglądarki: +Zweryfikuj [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/pl/user/email/update.html b/services/brig/deb/opt/brig/templates/pl/user/email/update.html index 8a0a1d35d99..bfbff27d748 100644 --- a/services/brig/deb/opt/brig/templates/pl/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/pl/user/email/update.html @@ -1 +1 @@ -Twój nowy adres e-mail na ${brand}

${brand_label_url}

Potwierdź swój adres email

${email} został zarejestrowany jako Twój nowy adres e-mail na ${brand}. Kliknij poniższy przycisk, aby zweryfikować swój adres.

 
Zweryfikuj
 

Jeśli nie możesz kliknąć przycisku, skopiuj i wklej ten link do swojej przeglądarki:

${url}

Jeśli masz jakieś pytania, prosimy skontaktuj się z nami.

                                                           
\ No newline at end of file +Twój nowy adres e-mail na ${brand}

${brand_label_url}

Potwierdź swój adres email

${email} został zarejestrowany jako Twój nowy adres e-mail na ${brand}. Kliknij poniższy przycisk, aby zweryfikować swój adres.

 
Zweryfikuj
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Jeśli masz jakieś pytania, prosimy skontaktuj się z nami.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/pl/user/email/update.txt b/services/brig/deb/opt/brig/templates/pl/user/email/update.txt index 63e46b58a26..c90fb831039 100644 --- a/services/brig/deb/opt/brig/templates/pl/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/pl/user/email/update.txt @@ -6,8 +6,8 @@ POTWIERDŹ SWÓJ ADRES EMAIL ${email} został zarejestrowany jako Twój nowy adres e-mail na ${brand}. Kliknij poniższy przycisk, aby zweryfikować swój adres. -Zweryfikuj [${url}]Jeśli nie możesz kliknąć przycisku, skopiuj i wklej ten link -do swojej przeglądarki: +Zweryfikuj [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/pt/user/email/activation.html b/services/brig/deb/opt/brig/templates/pt/user/email/activation.html index ea3081ffced..fd31bec4c6d 100644 --- a/services/brig/deb/opt/brig/templates/pt/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/pt/user/email/activation.html @@ -1 +1 @@ -Sua Conta ${brand}

${brand_label_url}

Verifique seu e-mail

${email} foi usado para se registrar no ${brand}.
Clique no botão para verificar seu e-mail.

 
Verificar
 

Se você não conseguir clicar no botão, copie e cole este link no seu navegador:

${url}

Se você tiver alguma dúvida, por favor, entre em contato conosco.

                                                           
\ No newline at end of file +Sua Conta ${brand}

${brand_label_url}

Verifique seu e-mail

${email} foi usado para se registrar no ${brand}.
Clique no botão para verificar seu e-mail.

 
Verificar
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Se você tiver alguma dúvida, por favor, entre em contato conosco.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/pt/user/email/activation.txt b/services/brig/deb/opt/brig/templates/pt/user/email/activation.txt index ada1772adab..d071c74e2f8 100644 --- a/services/brig/deb/opt/brig/templates/pt/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/pt/user/email/activation.txt @@ -6,8 +6,8 @@ VERIFIQUE SEU E-MAIL ${email} foi usado para se registrar no ${brand}. Clique no botão para verificar seu e-mail. -Verificar [${url}]Se você não conseguir clicar no botão, copie e cole este link -no seu navegador: +Verificar [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/pt/user/email/deletion.html b/services/brig/deb/opt/brig/templates/pt/user/email/deletion.html index a9fd902f42a..802d15a741e 100644 --- a/services/brig/deb/opt/brig/templates/pt/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/pt/user/email/deletion.html @@ -1 +1 @@ -Excluir conta?

${brand_label_url}

Excluir sua conta

Nós recebemos uma solicitação para excluir sua conta ${brand}. Clique no botão abaixo em até 10 minutos para excluir todas as suas conversas, conteúdo e conexões.

 
Excluir conta
 

Se você não conseguir clicar no botão, copie e cole este link no seu navegador:

${url}

Se você não solicitou isso, redefina sua senha.

Se você tiver alguma dúvida, por favor, entre em contato conosco.

                                                           
\ No newline at end of file +Excluir conta?

${brand_label_url}

Excluir sua conta

Nós recebemos uma solicitação para excluir sua conta ${brand}. Clique no botão abaixo em até 10 minutos para excluir todas as suas conversas, conteúdo e conexões.

 
Excluir conta
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Se você não solicitou isso, redefina sua senha.

Se você tiver alguma dúvida, por favor, entre em contato conosco.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/pt/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/pt/user/email/deletion.txt index 2b9dab30e3b..6e3b614a630 100644 --- a/services/brig/deb/opt/brig/templates/pt/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/pt/user/email/deletion.txt @@ -7,8 +7,8 @@ Nós recebemos uma solicitação para excluir sua conta ${brand}. Clique no bot abaixo em até 10 minutos para excluir todas as suas conversas, conteúdo e conexões. -Excluir conta [${url}]Se você não conseguir clicar no botão, copie e cole este -link no seu navegador: +Excluir conta [${url}]If you can’t select the button, copy and paste this link +to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/pt/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/pt/user/email/password-reset.html index 1066973603f..4e8b5079103 100644 --- a/services/brig/deb/opt/brig/templates/pt/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/pt/user/email/password-reset.html @@ -1 +1 @@ -Mudança de Senha no ${brand}

${brand_label_url}

Redefinir sua senha

Recebemos uma solicitação para redefinir a senha de sua conta ${brand}. Para criar uma nova senha, clique no botão abaixo.

 
Redefinir senha
 

Se você não conseguir clicar no botão, copie e cole este link no seu navegador:

${url}

Se você tiver alguma dúvida, por favor, entre em contato conosco.

                                                           
\ No newline at end of file +Mudança de Senha no ${brand}

${brand_label_url}

Redefinir sua senha

Recebemos uma solicitação para redefinir a senha de sua conta ${brand}. Para criar uma nova senha, clique no botão abaixo.

 
Redefinir senha
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Se você tiver alguma dúvida, por favor, entre em contato conosco.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/pt/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/pt/user/email/password-reset.txt index ce009cf1502..4dc9902c64e 100644 --- a/services/brig/deb/opt/brig/templates/pt/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/pt/user/email/password-reset.txt @@ -6,8 +6,8 @@ REDEFINIR SUA SENHA Recebemos uma solicitação para redefinir a senha de sua conta ${brand}. Para criar uma nova senha, clique no botão abaixo. -Redefinir senha [${url}]Se você não conseguir clicar no botão, copie e cole este -link no seu navegador: +Redefinir senha [${url}]If you can’t select the button, copy and paste this link +to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/pt/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/pt/user/email/team-activation.html index acc4378363a..f209d3dd916 100644 --- a/services/brig/deb/opt/brig/templates/pt/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/pt/user/email/team-activation.html @@ -1 +1 @@ -Conta ${brand}

${brand_label_url}

Sua nova conta em ${brand}

Um nova conta na equipe ${brand} foi criada com ${email}. Por favor, verifique seu e-mail.

 
Verificar
 

Se você não conseguir clicar no botão, copie e cole este link no seu navegador:

${url}

Se você tiver alguma dúvida, por favor, entre em contato conosco.

                                                           
\ No newline at end of file +Conta ${brand}

${brand_label_url}

Sua nova conta em ${brand}

Um nova conta na equipe ${brand} foi criada com ${email}. Por favor, verifique seu e-mail.

 
Verificar
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Se você tiver alguma dúvida, por favor, entre em contato conosco.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/pt/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/pt/user/email/team-activation.txt index 876e24f88b5..8b8640f94a7 100644 --- a/services/brig/deb/opt/brig/templates/pt/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/pt/user/email/team-activation.txt @@ -2,12 +2,12 @@ ${brand_label_url} [${brand_url}] -SUA NOVA CONTA EM ${BRAND} +SUA NOVA CONTA EM ${brand} Um nova conta na equipe ${brand} foi criada com ${email}. Por favor, verifique seu e-mail. -Verificar [${url}]Se você não conseguir clicar no botão, copie e cole este link -no seu navegador: +Verificar [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/pt/user/email/update.html b/services/brig/deb/opt/brig/templates/pt/user/email/update.html index 60c2d425e94..b559e366380 100644 --- a/services/brig/deb/opt/brig/templates/pt/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/pt/user/email/update.html @@ -1 +1 @@ -Seu novo endereço de email no ${brand}

${brand_label_url}

Confirme o seu e-mail

${email} foi registrado como seu novo endereço de e-mail no ${brand}. Clique no botão para confirmar seu endereço de e-mail.

 
Verificar
 

Se você não conseguir clicar no botão, copie e cole este link no seu navegador:

${url}

Se você tiver alguma dúvida, por favor, entre em contato conosco.

                                                           
\ No newline at end of file +Seu novo endereço de email no ${brand}

${brand_label_url}

Confirme o seu e-mail

${email} foi registrado como seu novo endereço de e-mail no ${brand}. Clique no botão para confirmar seu endereço de e-mail.

 
Verificar
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Se você tiver alguma dúvida, por favor, entre em contato conosco.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/pt/user/email/update.txt b/services/brig/deb/opt/brig/templates/pt/user/email/update.txt index ea858533a49..455f8424b69 100644 --- a/services/brig/deb/opt/brig/templates/pt/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/pt/user/email/update.txt @@ -6,8 +6,8 @@ CONFIRME O SEU E-MAIL ${email} foi registrado como seu novo endereço de e-mail no ${brand}. Clique no botão para confirmar seu endereço de e-mail. -Verificar [${url}]Se você não conseguir clicar no botão, copie e cole este link -no seu navegador: +Verificar [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/ru/user/email/activation.html b/services/brig/deb/opt/brig/templates/ru/user/email/activation.html index 8470a280639..038c9242d4f 100644 --- a/services/brig/deb/opt/brig/templates/ru/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/ru/user/email/activation.html @@ -1 +1 @@ -Ваша учетная запись ${brand}

${brand_label_url}

Подтвердите ваш email

${email} был использован для регистрации в ${brand}.
Нажмите на кнопку для подтверждения вашего email адреса.

 
Подтвердить
 

Если вы не можете нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер:

${url}

Если у вас возникли вопросы или нужна помощь, пожалуйста свяжитесь с нами.

                                                           
\ No newline at end of file +Ваша учетная запись ${brand}

${brand_label_url}

Подтвердите ваш email

${email} был использован для регистрации в ${brand}.
Нажмите на кнопку для подтверждения вашего email адреса.

 
Подтвердить
 

Если вам не удается нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер:

${url}

Если у вас возникли вопросы или нужна помощь, пожалуйста свяжитесь с нами.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/ru/user/email/activation.txt b/services/brig/deb/opt/brig/templates/ru/user/email/activation.txt index 4d1b80f496a..f8d08b612f7 100644 --- a/services/brig/deb/opt/brig/templates/ru/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/ru/user/email/activation.txt @@ -6,7 +6,7 @@ ${brand_label_url} [${brand_url}] ${email} был использован для регистрации в ${brand}. Нажмите на кнопку для подтверждения вашего email адреса. -Подтвердить [${url}]Если вы не можете нажать на кнопку, скопируйте и вставьте +Подтвердить [${url}]Если вам не удается нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер: ${url} diff --git a/services/brig/deb/opt/brig/templates/ru/user/email/deletion.html b/services/brig/deb/opt/brig/templates/ru/user/email/deletion.html index cb4f186bde7..dbc44cf8837 100644 --- a/services/brig/deb/opt/brig/templates/ru/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/ru/user/email/deletion.html @@ -1 +1 @@ -Удалить учетную запись?

${brand_label_url}

Удалить учетную запись

Мы получили запрос на удаление вашего аккаунта ${brand}. Нажмите на кнопку ниже в течение 10 минут для удаления всех ваших разговоров, контента и контактов.

 
Удалить учетную запись
 

Если вы не можете нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер:

${url}

Если вы не запрашивали удаление вашего аккаунта, то сбросьте ваш пароль.

Если у вас возникли вопросы или нужна помощь, пожалуйста свяжитесь с нами.

                                                           
\ No newline at end of file +Удалить учетную запись?

${brand_label_url}

Удалить учетную запись

Мы получили запрос на удаление вашего аккаунта ${brand}. Нажмите на кнопку ниже в течение 10 минут для удаления всех ваших разговоров, контента и контактов.

 
Удалить учетную запись
 

Если вам не удается нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер:

${url}

Если вы не запрашивали удаление вашего аккаунта, то сбросьте ваш пароль.

Если у вас возникли вопросы или нужна помощь, пожалуйста свяжитесь с нами.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/ru/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/ru/user/email/deletion.txt index 79d1d70eae7..bdebda4c025 100644 --- a/services/brig/deb/opt/brig/templates/ru/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/ru/user/email/deletion.txt @@ -6,8 +6,8 @@ ${brand_label_url} [${brand_url}] Мы получили запрос на удаление вашего аккаунта ${brand}. Нажмите на кнопку ниже в течение 10 минут для удаления всех ваших разговоров, контента и контактов. -Удалить учетную запись [${url}]Если вы не можете нажать на кнопку, скопируйте и -вставьте эту ссылку в свой браузер: +Удалить учетную запись [${url}]Если вам не удается нажать на кнопку, скопируйте +и вставьте эту ссылку в свой браузер: ${url} diff --git a/services/brig/deb/opt/brig/templates/ru/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/ru/user/email/password-reset.html index fd2ea12f9ce..47eb1e8f7fe 100644 --- a/services/brig/deb/opt/brig/templates/ru/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/ru/user/email/password-reset.html @@ -1 +1 @@ -Смена пароля в ${brand}

${brand_label_url}

Сбросить пароль

Мы получили запрос на сброс пароля для вашей учетной записи ${brand}. Чтобы создать новый пароль нажмите на кнопку ниже.

 
Сбросить пароль
 

Если вы не можете нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер:

${url}

Если у вас возникли вопросы или нужна помощь, пожалуйста свяжитесь с нами.

                                                           
\ No newline at end of file +Смена пароля в ${brand}

${brand_label_url}

Сбросить пароль

Мы получили запрос на сброс пароля для вашей учетной записи ${brand}. Чтобы создать новый пароль нажмите на кнопку ниже.

 
Сбросить пароль
 

Если вам не удается нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер:

${url}

Если у вас возникли вопросы или нужна помощь, пожалуйста свяжитесь с нами.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/ru/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/ru/user/email/password-reset.txt index 13d6f1d10ec..17c9afd959d 100644 --- a/services/brig/deb/opt/brig/templates/ru/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/ru/user/email/password-reset.txt @@ -6,7 +6,7 @@ ${brand_label_url} [${brand_url}] Мы получили запрос на сброс пароля для вашей учетной записи ${brand}. Чтобы создать новый пароль нажмите на кнопку ниже. -Сбросить пароль [${url}]Если вы не можете нажать на кнопку, скопируйте и +Сбросить пароль [${url}]Если вам не удается нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер: ${url} diff --git a/services/brig/deb/opt/brig/templates/ru/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/ru/user/email/team-activation.html index 8302577b73e..14099793b83 100644 --- a/services/brig/deb/opt/brig/templates/ru/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/ru/user/email/team-activation.html @@ -1 +1 @@ -Ваша учетная запись ${brand}

${brand_label_url}

Ваша новая учетная запись ${brand}

В ${brand} была создана новая команда с использованием email адреса ${email}. Подтвердите ваш email адрес.

 
Подтвердить
 

Если вы не можете нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер:

${url}

Если у вас возникли вопросы или нужна помощь, пожалуйста свяжитесь с нами.

                                                           
\ No newline at end of file +Ваша учетная запись ${brand}

${brand_label_url}

Ваша новая учетная запись ${brand}

В ${brand} была создана новая команда с использованием email адреса ${email}. Подтвердите ваш email адрес.

 
Подтвердить
 

Если вам не удается нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер:

${url}

Если у вас возникли вопросы или нужна помощь, пожалуйста свяжитесь с нами.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/ru/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/ru/user/email/team-activation.txt index 9ea2873c2f4..44364fcee6d 100644 --- a/services/brig/deb/opt/brig/templates/ru/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/ru/user/email/team-activation.txt @@ -2,11 +2,11 @@ ${brand_label_url} [${brand_url}] -ВАША НОВАЯ УЧЕТНАЯ ЗАПИСЬ ${BRAND} +ВАША НОВАЯ УЧЕТНАЯ ЗАПИСЬ ${brand} В ${brand} была создана новая команда с использованием email адреса ${email}. Подтвердите ваш email адрес. -Подтвердить [${url}]Если вы не можете нажать на кнопку, скопируйте и вставьте +Подтвердить [${url}]Если вам не удается нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер: ${url} diff --git a/services/brig/deb/opt/brig/templates/ru/user/email/update.html b/services/brig/deb/opt/brig/templates/ru/user/email/update.html index 36577320f83..c6970ef13dd 100644 --- a/services/brig/deb/opt/brig/templates/ru/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/ru/user/email/update.html @@ -1 +1 @@ -Ваш новый email адрес в ${brand}

${brand_label_url}

Подтвердите ваш email адрес

${email} был указан как ваш новый email адрес в ${brand}. Нажмите на кнопку ниже для подтверждения своего адреса.

 
Подтвердить
 

Если вы не можете нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер:

${url}

Если у вас возникли вопросы или нужна помощь, пожалуйста свяжитесь с нами.

                                                           
\ No newline at end of file +Ваш новый email адрес в ${brand}

${brand_label_url}

Подтвердите ваш email адрес

${email} был указан как ваш новый email адрес в ${brand}. Нажмите на кнопку ниже для подтверждения своего адреса.

 
Подтвердить
 

Если вам не удается нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер:

${url}

Если у вас возникли вопросы или нужна помощь, пожалуйста свяжитесь с нами.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/ru/user/email/update.txt b/services/brig/deb/opt/brig/templates/ru/user/email/update.txt index 7e28368272a..b72528a35d4 100644 --- a/services/brig/deb/opt/brig/templates/ru/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/ru/user/email/update.txt @@ -6,7 +6,7 @@ ${brand_label_url} [${brand_url}] ${email} был указан как ваш новый email адрес в ${brand}. Нажмите на кнопку ниже для подтверждения своего адреса. -Подтвердить [${url}]Если вы не можете нажать на кнопку, скопируйте и вставьте +Подтвердить [${url}]Если вам не удается нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер: ${url} diff --git a/services/brig/deb/opt/brig/templates/si/user/email/activation.html b/services/brig/deb/opt/brig/templates/si/user/email/activation.html index 5469f094995..236178d9b3e 100644 --- a/services/brig/deb/opt/brig/templates/si/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/si/user/email/activation.html @@ -1 +1 @@ -ඔබගේ ${brand} ගිණුම

${brand_label_url}

ඔබගේ වි-තැපෑල සත්‍යාපනය කරන්න

${brand} හි ලියාපදිංචියට ${email} භාවිතා කර ඇත.
ඔබගේ ලිපිනය සත්‍යාපනයට පහත බොත්තම ඔබන්න.

 
සත්‍යාපනය
 

බොත්තම එබීමට නොහැකි නම් මෙම සබැඳිය පිටපත් කර ඔබගේ අතිරික්සුවෙහි අලවන්න:

${url}

ඔබට කිසියම් ප්‍රශ්නයක් ඇත්නම් කරුණාකර අප අමතන්න.

                                                           
\ No newline at end of file +ඔබගේ ${brand} ගිණුම

${brand_label_url}

ඔබගේ වි-තැපෑල සත්‍යාපනය කරන්න

${brand} හි ලියාපදිංචියට ${email} භාවිතා කර ඇත.
ඔබගේ ලිපිනය සත්‍යාපනයට පහත බොත්තම ඔබන්න.

 
සත්‍යාපනය
 

If you can’t select the button, copy and paste this link to your browser:

${url}

ඔබට කිසියම් ප්‍රශ්නයක් ඇත්නම් කරුණාකර අප අමතන්න.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/si/user/email/activation.txt b/services/brig/deb/opt/brig/templates/si/user/email/activation.txt index bab1d042dc5..a172c2998f9 100644 --- a/services/brig/deb/opt/brig/templates/si/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/si/user/email/activation.txt @@ -6,8 +6,8 @@ ${brand_label_url} [${brand_url}] ${brand} හි ලියාපදිංචියට ${email} භාවිතා කර ඇත. ඔබගේ ලිපිනය සත්‍යාපනයට පහත බොත්තම ඔබන්න. -සත්‍යාපනය [${url}]බොත්තම එබීමට නොහැකි නම් මෙම සබැඳිය පිටපත් කර ඔබගේ -අතිරික්සුවෙහි අලවන්න: +සත්‍යාපනය [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/si/user/email/deletion.html b/services/brig/deb/opt/brig/templates/si/user/email/deletion.html index 6852a1f7796..319f24c339f 100644 --- a/services/brig/deb/opt/brig/templates/si/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/si/user/email/deletion.html @@ -1 +1 @@ -ගිණුම මකනවාද?

${brand_label_url}

ඔබගේ ගිණුම මකන්න

ඔබගේ ${brand} ගිණුම මැකීම සඳහා අපට ඉල්ලීමක් ලැබුණි. ඔබගේ සියළුම සංවාද, අන්තර්ගත සහ සම්බන්ධතා මැකීමට විනාඩි 10 ක් ඇතුළත පහත බොත්තම ඔබන්න.

 
ගිණුම මකන්න
 

බොත්තම එබීමට නොහැකි නම් මෙම සබැඳිය පිටපත් කර ඔබගේ අතිරික්සුවෙහි අලවන්න:

${url}

ඔබ මෙය ඉල්ලුවේ නැති නම්, මුරපදය යළි සකසන්න.

ඔබට කිසියම් ප්‍රශ්නයක් ඇත්නම් කරුණාකර අප අමතන්න.

                                                           
\ No newline at end of file +ගිණුම මකනවාද?

${brand_label_url}

ඔබගේ ගිණුම මකන්න

ඔබගේ ${brand} ගිණුම මැකීම සඳහා අපට ඉල්ලීමක් ලැබුණි. ඔබගේ සියළුම සංවාද, අන්තර්ගත සහ සම්බන්ධතා මැකීමට විනාඩි 10 ක් ඇතුළත පහත බොත්තම ඔබන්න.

 
ගිණුම මකන්න
 

If you can’t select the button, copy and paste this link to your browser:

${url}

ඔබ මෙය ඉල්ලුවේ නැති නම්, මුරපදය යළි සකසන්න.

ඔබට කිසියම් ප්‍රශ්නයක් ඇත්නම් කරුණාකර අප අමතන්න.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/si/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/si/user/email/deletion.txt index 07207417957..ce8bc509b06 100644 --- a/services/brig/deb/opt/brig/templates/si/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/si/user/email/deletion.txt @@ -6,8 +6,8 @@ ${brand_label_url} [${brand_url}] ඔබගේ ${brand} ගිණුම මැකීම සඳහා අපට ඉල්ලීමක් ලැබුණි. ඔබගේ සියළුම සංවාද, අන්තර්ගත සහ සම්බන්ධතා මැකීමට විනාඩි 10 ක් ඇතුළත පහත බොත්තම ඔබන්න. -ගිණුම මකන්න [${url}]බොත්තම එබීමට නොහැකි නම් මෙම සබැඳිය පිටපත් කර ඔබගේ -අතිරික්සුවෙහි අලවන්න: +ගිණුම මකන්න [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/si/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/si/user/email/password-reset.html index fd5fe0d1863..ca31f5eaf9a 100644 --- a/services/brig/deb/opt/brig/templates/si/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/si/user/email/password-reset.html @@ -1 +1 @@ -${brand} මුරපදය වෙනස් කිරීම

${brand_label_url}

මුරපදය යළි සකසන්න

ඔබගේ ${brand} ගිණුමේ මුරපදය යළි සැකසීම සඳහා අපට ඉල්ලීමක් ලැබුණි. නව මුරපදයක් සෑදීමට පහත බොත්තම ඔබන්න.

 
මුරපදය යළි සකසන්න
 

බොත්තම එබීමට නොහැකි නම් මෙම සබැඳිය පිටපත් කර ඔබගේ අතිරික්සුවෙහි අලවන්න:

${url}

ඔබට කිසියම් ප්‍රශ්නයක් ඇත්නම් කරුණාකර අප අමතන්න.

                                                           
\ No newline at end of file +${brand} මුරපදය වෙනස් කිරීම

${brand_label_url}

මුරපදය යළි සකසන්න

ඔබගේ ${brand} ගිණුමේ මුරපදය යළි සැකසීම සඳහා අපට ඉල්ලීමක් ලැබුණි. නව මුරපදයක් සෑදීමට පහත බොත්තම ඔබන්න.

 
මුරපදය යළි සකසන්න
 

If you can’t select the button, copy and paste this link to your browser:

${url}

ඔබට කිසියම් ප්‍රශ්නයක් ඇත්නම් කරුණාකර අප අමතන්න.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/si/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/si/user/email/password-reset.txt index fddd05d4af4..4f862173417 100644 --- a/services/brig/deb/opt/brig/templates/si/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/si/user/email/password-reset.txt @@ -6,8 +6,8 @@ ${brand_label_url} [${brand_url}] ඔබගේ ${brand} ගිණුමේ මුරපදය යළි සැකසීම සඳහා අපට ඉල්ලීමක් ලැබුණි. නව මුරපදයක් සෑදීමට පහත බොත්තම ඔබන්න. -මුරපදය යළි සකසන්න [${url}]බොත්තම එබීමට නොහැකි නම් මෙම සබැඳිය පිටපත් කර ඔබගේ -අතිරික්සුවෙහි අලවන්න: +මුරපදය යළි සකසන්න [${url}]If you can’t select the button, copy and paste this +link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/si/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/si/user/email/team-activation.html index 7017f3c8545..aab81c604fc 100644 --- a/services/brig/deb/opt/brig/templates/si/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/si/user/email/team-activation.html @@ -1 +1 @@ -${brand} ගිණුම

${brand_label_url}

ඔබගේ නව ${brand} ගිණුම

${email} සමඟ නව ${brand} කණ්ඩායමක් සාදා ඇත. ඔබගේ වි-තැපෑල සත්‍යාපනය කරන්න.

 
සත්‍යාපනය
 

බොත්තම එබීමට නොහැකි නම් මෙම සබැඳිය පිටපත් කර ඔබගේ අතිරික්සුවෙහි අලවන්න:

${url}

ඔබට කිසියම් ප්‍රශ්නයක් ඇත්නම් කරුණාකර අප අමතන්න.

                                                           
\ No newline at end of file +${brand} ගිණුම

${brand_label_url}

ඔබගේ නව ${brand} ගිණුම

${email} සමඟ නව ${brand} කණ්ඩායමක් සාදා ඇත. ඔබගේ වි-තැපෑල සත්‍යාපනය කරන්න.

 
සත්‍යාපනය
 

If you can’t select the button, copy and paste this link to your browser:

${url}

ඔබට කිසියම් ප්‍රශ්නයක් ඇත්නම් කරුණාකර අප අමතන්න.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/si/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/si/user/email/team-activation.txt index 520e00970c2..d585732b70e 100644 --- a/services/brig/deb/opt/brig/templates/si/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/si/user/email/team-activation.txt @@ -2,11 +2,11 @@ ${brand_label_url} [${brand_url}] -ඔබගේ නව ${BRAND} ගිණුම +ඔබගේ නව ${brand} ගිණුම ${email} සමඟ නව ${brand} කණ්ඩායමක් සාදා ඇත. ඔබගේ වි-තැපෑල සත්‍යාපනය කරන්න. -සත්‍යාපනය [${url}]බොත්තම එබීමට නොහැකි නම් මෙම සබැඳිය පිටපත් කර ඔබගේ -අතිරික්සුවෙහි අලවන්න: +සත්‍යාපනය [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/si/user/email/update.html b/services/brig/deb/opt/brig/templates/si/user/email/update.html index a0ad8cff780..54101e81d60 100644 --- a/services/brig/deb/opt/brig/templates/si/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/si/user/email/update.html @@ -1 +1 @@ -${brand} සඳහා නව වි-තැපැල් ලිපිනය

${brand_label_url}

වි-තැපෑල සත්‍යාපනය කරන්න

ඔබගේ නව ${brand} වි-තැපැල් ලිපිනය ලෙස ${email} ලියාපදිංචි කර ඇත. ඔබගේ ලිපිනය සත්‍යාපනයට පහත බොත්තම ඔබන්න.

 
සත්‍යාපනය
 

බොත්තම එබීමට නොහැකි නම් මෙම සබැඳිය පිටපත් කර ඔබගේ අතිරික්සුවෙහි අලවන්න:

${url}

ඔබට කිසියම් ප්‍රශ්නයක් ඇත්නම් කරුණාකර අප අමතන්න.

                                                           
\ No newline at end of file +${brand} සඳහා නව වි-තැපැල් ලිපිනය

${brand_label_url}

වි-තැපෑල සත්‍යාපනය කරන්න

ඔබගේ නව ${brand} වි-තැපැල් ලිපිනය ලෙස ${email} ලියාපදිංචි කර ඇත. ඔබගේ ලිපිනය සත්‍යාපනයට පහත බොත්තම ඔබන්න.

 
සත්‍යාපනය
 

If you can’t select the button, copy and paste this link to your browser:

${url}

ඔබට කිසියම් ප්‍රශ්නයක් ඇත්නම් කරුණාකර අප අමතන්න.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/si/user/email/update.txt b/services/brig/deb/opt/brig/templates/si/user/email/update.txt index 326cfeb0d08..77332028d5e 100644 --- a/services/brig/deb/opt/brig/templates/si/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/si/user/email/update.txt @@ -6,8 +6,8 @@ ${brand_label_url} [${brand_url}] ඔබගේ නව ${brand} වි-තැපැල් ලිපිනය ලෙස ${email} ලියාපදිංචි කර ඇත. ඔබගේ ලිපිනය සත්‍යාපනයට පහත බොත්තම ඔබන්න. -සත්‍යාපනය [${url}]බොත්තම එබීමට නොහැකි නම් මෙම සබැඳිය පිටපත් කර ඔබගේ -අතිරික්සුවෙහි අලවන්න: +සත්‍යාපනය [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/tr/user/email/activation.html b/services/brig/deb/opt/brig/templates/tr/user/email/activation.html index 024f68bd64c..acc10e2d534 100644 --- a/services/brig/deb/opt/brig/templates/tr/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/tr/user/email/activation.html @@ -1 +1 @@ -${brand} Hesabınız

${brand_label_url}

E-postanızı doğrulayın

${brand}} a kaydolmak için ${email} kullanıldı.
Adresinizi doğrulamak için düğmeyi tıklayın.

 
Doğrula
 

Düğmeyi tıklayamıyorsanız, bu bağlantıyı kopyalayıp tarayıcınıza yapıştırın:

${url}

Herhangi bir sorunuz veya yardıma ihtiyacınız varsa, lütfen bize ulaşın.

                                                           
\ No newline at end of file +${brand} Hesabınız

${brand_label_url}

E-postanızı doğrulayın

${brand}} a kaydolmak için ${email} kullanıldı.
Adresinizi doğrulamak için düğmeyi tıklayın.

 
Doğrula
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Herhangi bir sorunuz veya yardıma ihtiyacınız varsa, lütfen bize ulaşın.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/tr/user/email/activation.txt b/services/brig/deb/opt/brig/templates/tr/user/email/activation.txt index 17b7f8815c8..3939ecbc58c 100644 --- a/services/brig/deb/opt/brig/templates/tr/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/tr/user/email/activation.txt @@ -6,8 +6,8 @@ E-POSTANIZI DOĞRULAYIN ${brand}} a kaydolmak için ${email} kullanıldı. Adresinizi doğrulamak için düğmeyi tıklayın. -Doğrula [${url}]Düğmeyi tıklayamıyorsanız, bu bağlantıyı kopyalayıp tarayıcınıza -yapıştırın: +Doğrula [${url}]If you can’t select the button, copy and paste this link to your +browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/tr/user/email/deletion.html b/services/brig/deb/opt/brig/templates/tr/user/email/deletion.html index 56e31fa36bd..eacb1369eb3 100644 --- a/services/brig/deb/opt/brig/templates/tr/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/tr/user/email/deletion.html @@ -1 +1 @@ -Hesabı sil?

${brand_label_url}

Hesabını Sil

${brand} hesabınızı silmek için bir istek aldık. Tüm konuşmalarınızı, içeriğinizi ve bağlantılarınızı silmek için 10 dakika içinde aşağıdaki düğmeyi tıklayın.

 
Hesabı Sil
 

Düğmeyi tıklayamıyorsanız, bu bağlantıyı kopyalayıp tarayıcınıza yapıştırın:

${url}

Bunu istemediyseniz, şifrenizi sıfırlayın.

Herhangi bir sorunuz veya yardıma ihtiyacınız varsa, lütfen bize ulaşın.

                                                           
\ No newline at end of file +Hesabı sil?

${brand_label_url}

Hesabını Sil

${brand} hesabınızı silmek için bir istek aldık. Tüm konuşmalarınızı, içeriğinizi ve bağlantılarınızı silmek için 10 dakika içinde aşağıdaki düğmeyi tıklayın.

 
Hesabı Sil
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Bunu istemediyseniz, şifrenizi sıfırlayın.

Herhangi bir sorunuz veya yardıma ihtiyacınız varsa, lütfen bize ulaşın.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/tr/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/tr/user/email/deletion.txt index 4fa840086de..98c21039085 100644 --- a/services/brig/deb/opt/brig/templates/tr/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/tr/user/email/deletion.txt @@ -7,8 +7,8 @@ ${brand} hesabınızı silmek için bir istek aldık. Tüm konuşmalarınızı, içeriğinizi ve bağlantılarınızı silmek için 10 dakika içinde aşağıdaki düğmeyi tıklayın. -Hesabı Sil [${url}]Düğmeyi tıklayamıyorsanız, bu bağlantıyı kopyalayıp -tarayıcınıza yapıştırın: +Hesabı Sil [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/tr/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/tr/user/email/password-reset.html index bb1d0aa60a9..a210d31fa59 100644 --- a/services/brig/deb/opt/brig/templates/tr/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/tr/user/email/password-reset.html @@ -1 +1 @@ -${brand} 'da Şifre Değişikliği

${brand_label_url}

Şifrenizi sıfırlayın

${brand} hesabınızın şifresini sıfırlama isteği aldık. Yeni bir şifre oluşturmak için aşağıdaki butona tıklayın.

 
Şifreni sıfırla
 

Düğmeyi tıklayamıyorsanız, bu bağlantıyı kopyalayıp tarayıcınıza yapıştırın:

${url}

Herhangi bir sorunuz veya yardıma ihtiyacınız varsa, lütfen bize ulaşın.

                                                           
\ No newline at end of file +${brand} 'da Şifre Değişikliği

${brand_label_url}

Şifrenizi sıfırlayın

${brand} hesabınızın şifresini sıfırlama isteği aldık. Yeni bir şifre oluşturmak için aşağıdaki butona tıklayın.

 
Şifreni sıfırla
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Herhangi bir sorunuz veya yardıma ihtiyacınız varsa, lütfen bize ulaşın.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/tr/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/tr/user/email/password-reset.txt index 7c9925c6ca7..f2d1107f5d8 100644 --- a/services/brig/deb/opt/brig/templates/tr/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/tr/user/email/password-reset.txt @@ -6,8 +6,8 @@ ${brand_label_url} [${brand_url}] ${brand} hesabınızın şifresini sıfırlama isteği aldık. Yeni bir şifre oluşturmak için aşağıdaki butona tıklayın. -Şifreni sıfırla [${url}]Düğmeyi tıklayamıyorsanız, bu bağlantıyı kopyalayıp -tarayıcınıza yapıştırın: +Şifreni sıfırla [${url}]If you can’t select the button, copy and paste this link +to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/tr/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/tr/user/email/team-activation.html index 6e2a676f7e9..5d0f488d504 100644 --- a/services/brig/deb/opt/brig/templates/tr/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/tr/user/email/team-activation.html @@ -1 +1 @@ -${brand} Hesap

${brand_label_url}

${brand}'da yeni hesabınız

${email} ile yeni bir ${brand} takımı oluşturuldu. Lütfen e-postanızı doğrulayın.

 
Doğrula
 

Düğmeyi tıklayamıyorsanız, bu bağlantıyı kopyalayıp tarayıcınıza yapıştırın:

${url}

Herhangi bir sorunuz veya yardıma ihtiyacınız varsa, lütfen bize ulaşın.

                                                           
\ No newline at end of file +${brand} Hesap

${brand_label_url}

${brand}'da yeni hesabınız

${email} ile yeni bir ${brand} takımı oluşturuldu. Lütfen e-postanızı doğrulayın.

 
Doğrula
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Herhangi bir sorunuz veya yardıma ihtiyacınız varsa, lütfen bize ulaşın.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/tr/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/tr/user/email/team-activation.txt index 8615ceb768e..0e18b3aead6 100644 --- a/services/brig/deb/opt/brig/templates/tr/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/tr/user/email/team-activation.txt @@ -2,12 +2,12 @@ ${brand_label_url} [${brand_url}] -${BRAND}'DA YENI HESABINIZ +${brand}'DA YENI HESABINIZ ${email} ile yeni bir ${brand} takımı oluşturuldu. Lütfen e-postanızı doğrulayın. -Doğrula [${url}]Düğmeyi tıklayamıyorsanız, bu bağlantıyı kopyalayıp tarayıcınıza -yapıştırın: +Doğrula [${url}]If you can’t select the button, copy and paste this link to your +browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/tr/user/email/update.html b/services/brig/deb/opt/brig/templates/tr/user/email/update.html index 5b14f678898..a8ed525178b 100644 --- a/services/brig/deb/opt/brig/templates/tr/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/tr/user/email/update.html @@ -1 +1 @@ -${brand} üzerindeki yeni e-posta adresiniz

${brand_label_url}

E-postanızı doğrulayın

${email}, ${brand}'daki yeni e-posta adresiniz olarak kaydedildi. Adresinizi doğrulamak için aşağıdaki düğmeye tıklayın.

 
Doğrula
 

Düğmeyi tıklayamıyorsanız, bu bağlantıyı kopyalayıp tarayıcınıza yapıştırın:

${url}

Herhangi bir sorunuz veya yardıma ihtiyacınız varsa, lütfen bize ulaşın.

                                                           
\ No newline at end of file +${brand} üzerindeki yeni e-posta adresiniz

${brand_label_url}

E-postanızı doğrulayın

${email}, ${brand}'daki yeni e-posta adresiniz olarak kaydedildi. Adresinizi doğrulamak için aşağıdaki düğmeye tıklayın.

 
Doğrula
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Herhangi bir sorunuz veya yardıma ihtiyacınız varsa, lütfen bize ulaşın.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/tr/user/email/update.txt b/services/brig/deb/opt/brig/templates/tr/user/email/update.txt index e8346877e6d..4dc190c81ae 100644 --- a/services/brig/deb/opt/brig/templates/tr/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/tr/user/email/update.txt @@ -6,8 +6,8 @@ E-POSTANIZI DOĞRULAYIN ${email}, ${brand}'daki yeni e-posta adresiniz olarak kaydedildi. Adresinizi doğrulamak için aşağıdaki düğmeye tıklayın. -Doğrula [${url}]Düğmeyi tıklayamıyorsanız, bu bağlantıyı kopyalayıp tarayıcınıza -yapıştırın: +Doğrula [${url}]If you can’t select the button, copy and paste this link to your +browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/version b/services/brig/deb/opt/brig/templates/version index fea60e70c1a..5c41189b952 100644 --- a/services/brig/deb/opt/brig/templates/version +++ b/services/brig/deb/opt/brig/templates/version @@ -1 +1 @@ -v1.0.121 +v1.0.122 diff --git a/services/brig/deb/opt/brig/templates/vi/user/email/activation.html b/services/brig/deb/opt/brig/templates/vi/user/email/activation.html index 3b47400c118..19833198536 100644 --- a/services/brig/deb/opt/brig/templates/vi/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/vi/user/email/activation.html @@ -1 +1 @@ -Tài khoản ${brand} của bạn

${brand_label_url}

Xác minh địa chỉ emal của bạn

${email} đã được dùng để đăng ký ${brand}.
Nhấp vào nút để xác minh địa chỉ của bạn.

 
Xác minh
 

Nếu bạn không thể nhấp vào nút, sao chép và gán đường dẫn này vào trình duyệt của bạn:

${url}

Nếu bạn có bất kỳ thắc mắc nào, xin vui lòng liên hệ với chúng tôi.

                                                           
\ No newline at end of file +Tài khoản ${brand} của bạn

${brand_label_url}

Xác minh địa chỉ emal của bạn

${email} đã được dùng để đăng ký ${brand}.
Nhấp vào nút để xác minh địa chỉ của bạn.

 
Xác minh
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Nếu bạn có bất kỳ thắc mắc nào, xin vui lòng liên hệ với chúng tôi.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/vi/user/email/activation.txt b/services/brig/deb/opt/brig/templates/vi/user/email/activation.txt index 9fd76c0cace..a2db915b4ae 100644 --- a/services/brig/deb/opt/brig/templates/vi/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/vi/user/email/activation.txt @@ -6,8 +6,8 @@ XÁC MINH ĐỊA CHỈ EMAL CỦA BẠN ${email} đã được dùng để đăng ký ${brand}. Nhấp vào nút để xác minh địa chỉ của bạn. -Xác minh [${url}]Nếu bạn không thể nhấp vào nút, sao chép và gán đường dẫn này -vào trình duyệt của bạn: +Xác minh [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/vi/user/email/deletion.html b/services/brig/deb/opt/brig/templates/vi/user/email/deletion.html index 274ee2d08b4..a19046cbf1b 100644 --- a/services/brig/deb/opt/brig/templates/vi/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/vi/user/email/deletion.html @@ -1 +1 @@ -Xoá tài khoản?

${brand_label_url}

Xoá tài khoản của bạn

Chúng tôi nhận được một yêu cầu xoá tài khoản ${brand} của bạn. Nhấp vào nút phía bên dưới trong vòng 10 phút để xoá toàn bộ cuộc hội thoại, nội dung và mọi kết nối của bạn.

 
Xoá tài khoản
 

Nếu bạn không thể nhấp vào nút, sao chép và gán đường dẫn này vào trình duyệt của bạn:

${url}

Nếu bạn không thực hiện yêu cầu này, thay đổi mật khẩu của bạn ngay.

Nếu bạn có bất kỳ thắc mắc nào, xin vui lòng liên hệ với chúng tôi.

                                                           
\ No newline at end of file +Xoá tài khoản?

${brand_label_url}

Xoá tài khoản của bạn

Chúng tôi nhận được một yêu cầu xoá tài khoản ${brand} của bạn. Nhấp vào nút phía bên dưới trong vòng 10 phút để xoá toàn bộ cuộc hội thoại, nội dung và mọi kết nối của bạn.

 
Xoá tài khoản
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Nếu bạn không thực hiện yêu cầu này, thay đổi mật khẩu của bạn ngay.

Nếu bạn có bất kỳ thắc mắc nào, xin vui lòng liên hệ với chúng tôi.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/vi/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/vi/user/email/deletion.txt index 3dfd5366e04..5fdb53d1cdd 100644 --- a/services/brig/deb/opt/brig/templates/vi/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/vi/user/email/deletion.txt @@ -7,8 +7,8 @@ Chúng tôi nhận được một yêu cầu xoá tài khoản ${brand} của b phía bên dưới trong vòng 10 phút để xoá toàn bộ cuộc hội thoại, nội dung và mọi kết nối của bạn. -Xoá tài khoản [${url}]Nếu bạn không thể nhấp vào nút, sao chép và gán đường dẫn -này vào trình duyệt của bạn: +Xoá tài khoản [${url}]If you can’t select the button, copy and paste this link +to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/vi/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/vi/user/email/password-reset.html index 3aad8d78af1..949db860f09 100644 --- a/services/brig/deb/opt/brig/templates/vi/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/vi/user/email/password-reset.html @@ -1 +1 @@ -Thay đổi mật khẩu ${brand}

${brand_label_url}

Đặt lại mật khẩu của bạn

Chúng tôi nhận được một yêu cầu đặt lại mật khẩu cho tài khoản ${brand} của bạn. Để tạo một tài khoản mới, nhấp vào nút phía bên dưới.

 
Đặt lại mật khẩu
 

Nếu bạn không thể nhấp vào nút, sao chép và gán đường dẫn này vào trình duyệt của bạn:

${url}

Nếu bạn có bất kỳ thắc mắc nào, xin vui lòng liên hệ với chúng tôi.

                                                           
\ No newline at end of file +Thay đổi mật khẩu ${brand}

${brand_label_url}

Đặt lại mật khẩu của bạn

Chúng tôi nhận được một yêu cầu đặt lại mật khẩu cho tài khoản ${brand} của bạn. Để tạo một tài khoản mới, nhấp vào nút phía bên dưới.

 
Đặt lại mật khẩu
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Nếu bạn có bất kỳ thắc mắc nào, xin vui lòng liên hệ với chúng tôi.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/vi/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/vi/user/email/password-reset.txt index 19be97f5f4e..e77c47a7214 100644 --- a/services/brig/deb/opt/brig/templates/vi/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/vi/user/email/password-reset.txt @@ -6,8 +6,8 @@ ${brand_label_url} [${brand_url}] Chúng tôi nhận được một yêu cầu đặt lại mật khẩu cho tài khoản ${brand} của bạn. Để tạo một tài khoản mới, nhấp vào nút phía bên dưới. -Đặt lại mật khẩu [${url}]Nếu bạn không thể nhấp vào nút, sao chép và gán đường -dẫn này vào trình duyệt của bạn: +Đặt lại mật khẩu [${url}]If you can’t select the button, copy and paste this +link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/vi/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/vi/user/email/team-activation.html index 48edcff50b3..d7e1d16c93e 100644 --- a/services/brig/deb/opt/brig/templates/vi/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/vi/user/email/team-activation.html @@ -1 +1 @@ -Tài khoản ${brand}

${brand_label_url}

Tài khoản mới của bạn trên ${brand}

Một nhóm ${brand} đã được tại với ${email}. Vui lòng xác minh địa chỉ email của bạn.

 
Xác minh
 

Nếu bạn không thể nhấp vào nút, sao chép và gán đường dẫn này vào trình duyệt của bạn:

${url}

Nếu bạn có bất kỳ thắc mắc nào, xin vui lòng liên hệ với chúng tôi.

                                                           
\ No newline at end of file +Tài khoản ${brand}

${brand_label_url}

Tài khoản mới của bạn trên ${brand}

Một nhóm ${brand} đã được tại với ${email}. Vui lòng xác minh địa chỉ email của bạn.

 
Xác minh
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Nếu bạn có bất kỳ thắc mắc nào, xin vui lòng liên hệ với chúng tôi.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/vi/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/vi/user/email/team-activation.txt index 021963e3ac9..effd3a21a6a 100644 --- a/services/brig/deb/opt/brig/templates/vi/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/vi/user/email/team-activation.txt @@ -2,12 +2,12 @@ ${brand_label_url} [${brand_url}] -TÀI KHOẢN MỚI CỦA BẠN TRÊN ${BRAND} +TÀI KHOẢN MỚI CỦA BẠN TRÊN ${brand} Một nhóm ${brand} đã được tại với ${email}. Vui lòng xác minh địa chỉ email của bạn. -Xác minh [${url}]Nếu bạn không thể nhấp vào nút, sao chép và gán đường dẫn này -vào trình duyệt của bạn: +Xác minh [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/vi/user/email/update.html b/services/brig/deb/opt/brig/templates/vi/user/email/update.html index d227a8e59d7..8648dbebfa9 100644 --- a/services/brig/deb/opt/brig/templates/vi/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/vi/user/email/update.html @@ -1 +1 @@ -Địa chỉ eamil mới trên ${brand}

${brand_label_url}

Xác minh địa chỉ emal của bạn

${email} đã được đăng ký như là địa chỉ email mới của bạn trên ${brand}. Nhấp vào nút phía bên dưới để xác minh địa chỉ email của bạn.

 
Xác minh
 

Nếu bạn không thể nhấp vào nút, sao chép và gán đường dẫn này vào trình duyệt của bạn:

${url}

Nếu bạn có bất kỳ thắc mắc nào, xin vui lòng liên hệ với chúng tôi.

                                                           
\ No newline at end of file +Địa chỉ eamil mới trên ${brand}

${brand_label_url}

Xác minh địa chỉ emal của bạn

${email} đã được đăng ký như là địa chỉ email mới của bạn trên ${brand}. Nhấp vào nút phía bên dưới để xác minh địa chỉ email của bạn.

 
Xác minh
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Nếu bạn có bất kỳ thắc mắc nào, xin vui lòng liên hệ với chúng tôi.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/vi/user/email/update.txt b/services/brig/deb/opt/brig/templates/vi/user/email/update.txt index 721f2a11b0f..f8f5a9ce53a 100644 --- a/services/brig/deb/opt/brig/templates/vi/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/vi/user/email/update.txt @@ -6,8 +6,8 @@ XÁC MINH ĐỊA CHỈ EMAL CỦA BẠN ${email} đã được đăng ký như là địa chỉ email mới của bạn trên ${brand}. Nhấp vào nút phía bên dưới để xác minh địa chỉ email của bạn. -Xác minh [${url}]Nếu bạn không thể nhấp vào nút, sao chép và gán đường dẫn này -vào trình duyệt của bạn: +Xác minh [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/default.nix b/services/brig/default.nix index 4a2369d7813..ed77acac6e5 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -29,8 +29,8 @@ , conduit , containers , cookie -, cql , cryptobox-haskell +, crypton , currency-codes , data-default , data-timeout @@ -49,8 +49,9 @@ , fsnotify , galley-types , gitignoreSource -, gundeck-types , hashable +, hs-opentelemetry-instrumentation-wai +, hs-opentelemetry-sdk , hscim , HsOpenSSL , http-api-data @@ -70,6 +71,7 @@ , lens , lens-aeson , lib +, memory , metrics-core , metrics-wai , mime @@ -77,7 +79,6 @@ , mmorph , MonadRandom , mtl -, mwc-random , network , network-conduit-tls , network-uri @@ -104,7 +105,6 @@ , safe-exceptions , saml2-web-sso , schema-profunctor -, scientific , servant , servant-client , servant-client-core @@ -128,13 +128,11 @@ , template-haskell , temporary , text -, text-icu-translit , time , time-out , time-units , tinylog , transformers -, transitive-anns , types-common , types-common-aws , types-common-journal @@ -153,6 +151,7 @@ , warp-tls , wire-api , wire-api-federation +, wire-otel , wire-subsystems , yaml , zauth @@ -187,8 +186,8 @@ mkDerivation { conduit containers cookie - cql cryptobox-haskell + crypton currency-codes data-default dns @@ -203,8 +202,9 @@ mkDerivation { filepath fsnotify galley-types - gundeck-types hashable + hs-opentelemetry-instrumentation-wai + hs-opentelemetry-sdk HsOpenSSL http-client http-client-openssl @@ -219,6 +219,7 @@ mkDerivation { jwt-tools lens lens-aeson + memory metrics-core metrics-wai mime @@ -226,7 +227,6 @@ mkDerivation { mmorph MonadRandom mtl - mwc-random network network-conduit-tls openapi3 @@ -243,9 +243,7 @@ mkDerivation { resourcet retry safe-exceptions - saml2-web-sso schema-profunctor - scientific servant servant-openapi3 servant-server @@ -258,13 +256,11 @@ mkDerivation { template template-haskell text - text-icu-translit time time-out time-units tinylog transformers - transitive-anns types-common types-common-aws types-common-journal @@ -280,8 +276,8 @@ mkDerivation { wai-utilities wire-api wire-api-federation + wire-otel wire-subsystems - yaml zauth ]; executableHaskellDepends = [ @@ -397,6 +393,7 @@ mkDerivation { tasty tasty-hunit tasty-quickcheck + text time tinylog types-common diff --git a/services/brig/docs/swagger-v6.json b/services/brig/docs/swagger-v6.json new file mode 100644 index 00000000000..8b4dab3dbef --- /dev/null +++ b/services/brig/docs/swagger-v6.json @@ -0,0 +1,31085 @@ +{ + "components": { + "schemas": { + "": { + "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" + }, + "ASCII": { + "example": "aGVsbG8", + "type": "string" + }, + "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" + }, + "Action": { + "enum": [ + "add_conversation_member", + "remove_conversation_member", + "modify_conversation_name", + "modify_conversation_message_timer", + "modify_conversation_receipt_mode", + "modify_conversation_access", + "modify_other_conversation_member", + "leave_conversation", + "delete_conversation" + ], + "type": "string" + }, + "Activate": { + "description": "Data for an activation request.", + "properties": { + "code": { + "$ref": "#/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" + }, + "AllTeamFeatures": { + "properties": { + "appLock": { + "$ref": "#/components/schemas/AppLockConfig.LockableFeature" + }, + "classifiedDomains": { + "$ref": "#/components/schemas/ClassifiedDomainsConfig.LockableFeature" + }, + "conferenceCalling": { + "$ref": "#/components/schemas/ConferenceCallingConfig.LockableFeature" + }, + "conversationGuestLinks": { + "$ref": "#/components/schemas/GuestLinksConfig.LockableFeature" + }, + "digitalSignatures": { + "$ref": "#/components/schemas/DigitalSignaturesConfig.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" + }, + "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" + }, + "sndFactorPasswordChallenge": { + "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.LockableFeature" + }, + "sso": { + "$ref": "#/components/schemas/SSOConfig.LockableFeature" + }, + "validateSAMLemails": { + "$ref": "#/components/schemas/ValidateSAMLEmailsConfig.LockableFeature" + } + }, + "required": [ + "legalhold", + "sso", + "searchVisibility", + "searchVisibilityInbound", + "validateSAMLemails", + "digitalSignatures", + "appLock", + "fileSharing", + "classifiedDomains", + "conferenceCalling", + "selfDeletingMessages", + "conversationGuestLinks", + "sndFactorPasswordChallenge", + "mls", + "exposeInvitationURLsToTeamAdmin", + "outlookCalIntegration", + "mlsE2EId", + "mlsMigration", + "enforceFileDownloadLocation", + "limitedEventFanout" + ], + "type": "object" + }, + "Alpha": { + "enum": [ + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BOV", + "BRL", + "BSD", + "BTN", + "BWP", + "BYN", + "BZD", + "CAD", + "CDF", + "CHE", + "CHF", + "CHW", + "CLF", + "CLP", + "CNY", + "COP", + "COU", + "CRC", + "CUC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "IRR", + "ISK", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRO", + "MUR", + "MVR", + "MWK", + "MXN", + "MXV", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLL", + "SOS", + "SRD", + "SSP", + "STD", + "SVC", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "USN", + "UYI", + "UYU", + "UZS", + "VEF", + "VND", + "VUV", + "WST", + "XAF", + "XAG", + "XAU", + "XBA", + "XBB", + "XBC", + "XBD", + "XCD", + "XDR", + "XOF", + "XPD", + "XPF", + "XPT", + "XSU", + "XTS", + "XUA", + "XXX", + "YER", + "ZAR", + "ZMW", + "ZWL" + ], + "type": "string" + }, + "AppLockConfig": { + "properties": { + "enforceAppLock": { + "type": "boolean" + }, + "inactivityTimeoutSecs": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "enforceAppLock", + "inactivityTimeoutSecs" + ], + "type": "object" + }, + "AppLockConfig.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" + }, + "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" + }, + "AssetKey": { + "example": "3-1-47de4580-ae51-4650-acbb-d10c028cb0ac", + "type": "string" + }, + "AssetSize": { + "enum": [ + "preview", + "complete" + ], + "type": "string" + }, + "AssetSource": {}, + "AssetType": { + "enum": [ + "image" + ], + "type": "string" + }, + "AuthnRequest": { + "properties": { + "iD": { + "$ref": "#/components/schemas/ID_*_AuthnRequest" + }, + "issueInstant": { + "$ref": "#/components/schemas/Time" + }, + "issuer": { + "type": "string" + }, + "nameIDPolicy": { + "$ref": "#/components/schemas/NameIdPolicy" + } + }, + "required": [ + "iD", + "issueInstant", + "issuer" + ], + "type": "object" + }, + "Base64ByteString": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "BaseProtocol": { + "enum": [ + "proteus", + "mls" + ], + "type": "string" + }, + "BindingNewTeamUser": { + "properties": { + "currency": { + "$ref": "#/components/schemas/Alpha" + }, + "icon": { + "$ref": "#/components/schemas/Icon" + }, + "icon_key": { + "description": "team icon asset key", + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "members": { + "description": "initial team member ids (between 1 and 127)" + }, + "name": { + "description": "team name", + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name", + "icon" + ], + "type": "object" + }, + "Body": {}, + "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" + }, + "CheckHandles": { + "properties": { + "handles": { + "items": { + "type": "string" + }, + "maxItems": 50, + "minItems": 1, + "type": "array" + }, + "return": { + "maximum": 10, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "handles", + "return" + ], + "type": "object" + }, + "CipherSuiteTag": { + "description": "The cipher suite of the corresponding MLS group", + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "ClassifiedDomainsConfig": { + "properties": { + "domains": { + "items": { + "$ref": "#/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" + ], + "type": "string" + }, + "ClientCapabilityList": { + "properties": { + "capabilities": { + "description": "Hints provided by the client for the backend so it can behave in a backwards-compatible way.", + "items": { + "$ref": "#/components/schemas/ClientCapability" + }, + "type": "array" + } + }, + "required": [ + "capabilities" + ], + "type": "object" + }, + "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" + }, + "ClientListv6": { + "items": { + "$ref": "#/components/schemas/Client" + }, + "type": "array" + }, + "ClientMismatch": { + "properties": { + "deleted": { + "$ref": "#/components/schemas/UserClients" + }, + "missing": { + "$ref": "#/components/schemas/UserClients" + }, + "redundant": { + "$ref": "#/components/schemas/UserClients" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + } + }, + "required": [ + "time", + "missing", + "redundant", + "deleted" + ], + "type": "object" + }, + "ClientPrekey": { + "properties": { + "client": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "prekey": { + "$ref": "#/components/schemas/Prekey" + } + }, + "required": [ + "client", + "prekey" + ], + "type": "object" + }, + "ClientType": { + "enum": [ + "temporary", + "permanent", + "legalhold" + ], + "type": "string" + }, + "Clientv6": { + "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" + }, + "CodeChallengeMethod": { + "description": "The method used to encode the code challenge. Only `S256` is supported.", + "enum": [ + "S256" + ], + "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" + }, + "Contact": { + "description": "Contact discovered through search", + "properties": { + "accent_id": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "handle": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "qualified_id", + "name" + ], + "type": "object" + }, + "ConvMembers": { + "description": "Users of a conversation", + "properties": { + "others": { + "description": "All other current users of this conversation", + "items": { + "$ref": "#/components/schemas/OtherMember" + }, + "type": "array" + }, + "self": { + "$ref": "#/components/schemas/Member" + } + }, + "required": [ + "self", + "others" + ], + "type": "object" + }, + "ConvTeamInfo": { + "description": "Team information of this conversation", + "properties": { + "managed": { + "description": "This field MUST NOT be used by clients. It is here only for backwards compatibility of the interface." + }, + "teamid": { + "$ref": "#/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" + }, + "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_id": { + "$ref": "#/components/schemas/GroupId" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "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" + }, + "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" + }, + "uri": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "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", + "has_password" + ], + "type": "object" + }, + "ConversationCoverView": { + "description": "Limited view of Conversation.", + "properties": { + "has_password": { + "type": "boolean" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "has_password" + ], + "type": "object" + }, + "ConversationIds_Page": { + "properties": { + "has_more": { + "type": "boolean" + }, + "paging_state": { + "$ref": "#/components/schemas/ConversationIds_PagingState" + }, + "qualified_conversations": { + "items": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "type": "array" + } + }, + "required": [ + "qualified_conversations", + "has_more", + "paging_state" + ], + "type": "object" + }, + "ConversationIds_PagingState": { + "type": "string" + }, + "ConversationMessageTimerUpdate": { + "description": "Contains conversation properties to update", + "properties": { + "message_timer": { + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "type": "object" + }, + "ConversationReceiptModeUpdate": { + "description": "Contains conversation receipt mode to update to. Receipt mode tells clients whether certain types of receipts should be sent in the given conversation or not. How this value is interpreted is up to clients.", + "properties": { + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "receipt_mode" + ], + "type": "object" + }, + "ConversationRename": { + "properties": { + "name": { + "description": "The new conversation name", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "ConversationRole": { + "properties": { + "actions": { + "description": "The set of actions allowed for this role", + "items": { + "$ref": "#/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" + }, + "ConversationV2": { + "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" + }, + "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/Epoch Timestamp" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "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" + }, + "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" + }, + "ConversationV3v3": { + "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" + }, + "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/Epoch Timestamp" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "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" + }, + "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" + }, + "ConversationV6v6": { + "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" + }, + "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_id": { + "$ref": "#/components/schemas/GroupId" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "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" + }, + "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" + }, + "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/Conversation" + }, + "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" + }, + "CreateGroupConversationv6": { + "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" + }, + "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_id": { + "$ref": "#/components/schemas/GroupId" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "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" + }, + "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" + }, + "password": { + "type": "string" + }, + "verification_code": { + "type": "string" + } + }, + "required": [ + "description" + ], + "type": "object" + }, + "CreateScimTokenResponse": { + "properties": { + "info": { + "$ref": "#/components/schemas/ScimTokenInfo" + }, + "token": { + "description": "Authentication token", + "type": "string" + } + }, + "required": [ + "token", + "info" + ], + "type": "object" + }, + "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" + }, + "DeleteSubConversationRequest": { + "description": "Delete an MLS subconversation", + "properties": { + "epoch": { + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + } + }, + "required": [ + "group_id", + "epoch" + ], + "type": "object" + }, + "DeleteUser": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "DeletionCodeTimeout": { + "properties": { + "expires_in": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "expires_in" + ], + "type": "object" + }, + "DeprecatedMatchingResult": { + "deprecated": true, + "properties": { + "auto-connects": { + "items": {}, + "type": "array" + }, + "results": { + "items": {}, + "type": "array" + } + }, + "required": [ + "results", + "auto-connects" + ], + "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" + }, + "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" + }, + "Epoch Timestamp": { + "example": "2021-05-12T10:52:02Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + }, + "Event": { + "properties": { + "conversation": { + "$ref": "#/components/schemas/UUID" + }, + "data": { + "description": "Encrypted message of a conversation", + "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" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "data": { + "description": "Extra (symmetric) data (i.e. ciphertext, Base64 in JSON) that is common with all other recipients.", + "type": "string" + }, + "email": { + "type": "string" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/Epoch Timestamp" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "has_password": { + "description": "Whether the conversation has a password", + "type": "boolean" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/ConvMembers" + }, + "message": { + "type": "string" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "protocol": { + "$ref": "#/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", + "reason", + "qualified_user_ids", + "user_ids", + "qualified_target", + "name", + "access", + "key", + "code", + "has_password", + "qualified_id", + "type", + "members", + "group_id", + "epoch", + "epoch_timestamp", + "cipher_suite", + "qualified_recipient", + "receipt_mode", + "sender", + "recipient", + "text", + "status" + ], + "type": "object" + }, + "from": { + "$ref": "#/components/schemas/UUID" + }, + "qualified_conversation": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "qualified_from": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "subconv": { + "type": "string" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "type": { + "$ref": "#/components/schemas/EventType" + } + }, + "required": [ + "type", + "data", + "qualified_conversation", + "qualified_from", + "time" + ], + "type": "object" + }, + "EventType": { + "enum": [ + "conversation.member-join", + "conversation.member-leave", + "conversation.member-update", + "conversation.rename", + "conversation.access-update", + "conversation.receipt-mode-update", + "conversation.message-timer-update", + "conversation.code-update", + "conversation.code-delete", + "conversation.create", + "conversation.delete", + "conversation.connect-request", + "conversation.typing", + "conversation.otr-message-add", + "conversation.mls-message-add", + "conversation.mls-welcome", + "conversation.protocol-update" + ], + "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" + }, + "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" + }, + "GroupId": { + "description": "A base64-encoded MLS group ID", + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "GroupInfoData": { + "description": "This object can only be parsed in TLS format. Please refer to the MLS specification for details." + }, + "GuestLinksConfig.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "GuestLinksConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "Handle": { + "type": "string" + }, + "HandleUpdate": { + "properties": { + "handle": { + "type": "string" + } + }, + "required": [ + "handle" + ], + "type": "object" + }, + "HttpsUrl": { + "example": "https://example.com", + "type": "string" + }, + "ID_*_AuthnRequest": { + "properties": { + "iD": { + "$ref": "#/components/schemas/XmlText" + } + }, + "required": [ + "iD" + ], + "type": "object" + }, + "Icon": { + "type": "string" + }, + "Id": { + "properties": { + "id": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "IdPConfig_WireIdP": { + "properties": { + "extraInfo": { + "$ref": "#/components/schemas/WireIdP" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "metadata": { + "$ref": "#/components/schemas/IdPMetadata" + } + }, + "required": [ + "id", + "metadata", + "extraInfo" + ], + "type": "object" + }, + "IdPList": { + "properties": { + "providers": { + "items": { + "$ref": "#/components/schemas/IdPConfig_WireIdP" + }, + "type": "array" + } + }, + "required": [ + "providers" + ], + "type": "object" + }, + "IdPMetadata": { + "properties": { + "certAuthnResponse": { + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "issuer": { + "type": "string" + }, + "requestURI": { + "type": "string" + } + }, + "required": [ + "issuer", + "requestURI", + "certAuthnResponse" + ], + "type": "object" + }, + "IdPMetadataInfo": { + "maxProperties": 1, + "minProperties": 1, + "properties": { + "value": { + "type": "string" + } + }, + "type": "object" + }, + "Invitation": { + "description": "An invitation to join a team on Wire", + "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": { + "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" + }, + "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" + }, + "uri": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "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" + }, + "List1": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "minItems": 1, + "type": "array" + }, + "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": { + "properties": { + "allowedCipherSuites": { + "items": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "type": "array" + }, + "defaultCipherSuite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "defaultProtocol": { + "$ref": "#/components/schemas/Protocol" + }, + "protocolToggleUsers": { + "description": "allowlist of users that may change protocols", + "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_MLSPublicKey": { + "properties": { + "conversation": { + "$ref": "#/components/schemas/Conversation" + }, + "public_keys": { + "$ref": "#/components/schemas/MLSKeysByPurpose" + } + }, + "required": [ + "conversation", + "public_keys" + ], + "type": "object" + }, + "MLSPublicKey": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "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" + }, + "ManagedBy": { + "enum": [ + "wire", + "scim" + ], + "type": "string" + }, + "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" + }, + "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": { + "properties": { + "acmeDiscoveryUrl": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "crlProxy": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "useProxyOnMobile": { + "type": "boolean" + }, + "verificationExpiration": { + "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`.", + "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": { + "$ref": "#/components/schemas/UTCTime" + }, + "startTime": { + "$ref": "#/components/schemas/UTCTime" + } + }, + "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": { + "$ref": "#/components/schemas/XmlText" + } + }, + "required": [ + "format", + "allowCreate" + ], + "type": "object" + }, + "NewAssetToken": { + "properties": { + "token": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "token" + ], + "type": "object" + }, + "NewClient": { + "properties": { + "capabilities": { + "description": "Hints provided by the client for the backend so it can behave in a backwards-compatible way.", + "items": { + "$ref": "#/components/schemas/ClientCapability" + }, + "type": "array" + }, + "class": { + "$ref": "#/components/schemas/ClientClass" + }, + "cookie": { + "description": "The cookie label, i.e. the label used when logging in.", + "type": "string" + }, + "label": { + "type": "string" + }, + "lastkey": { + "$ref": "#/components/schemas/Prekey" + }, + "mls_public_keys": { + "$ref": "#/components/schemas/MLSPublicKeys" + }, + "model": { + "type": "string" + }, + "password": { + "description": "The password of the authenticated user for verification. Note: Required for registration of the 2nd, 3rd, ... client.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "prekeys": { + "description": "Prekeys for other clients to establish OTR sessions.", + "items": { + "$ref": "#/components/schemas/Prekey" + }, + "type": "array" + }, + "type": { + "$ref": "#/components/schemas/ClientType" + }, + "verification_code": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "prekeys", + "lastkey", + "type" + ], + "type": "object" + }, + "NewConv": { + "description": "JSON object to create a new conversation. When using 'qualified_users' (preferred), you can omit 'users'", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "message_timer": { + "description": "Per-conversation message timer", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "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" + }, + "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" + }, + "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/" + }, + "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" + }, + "NewTeamMember": { + "description": "Required data when creating new team members", + "properties": { + "member": { + "description": "the team member to add (the legalhold_status field must be null or missing!)", + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "permissions": { + "$ref": "#/components/schemas/Permissions" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "permissions" + ], + "type": "object" + } + }, + "required": [ + "member" + ], + "type": "object" + }, + "NewUser": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "email_code": { + "$ref": "#/components/schemas/ASCII" + }, + "expires_in": { + "maximum": 604800, + "minimum": 1, + "type": "integer" + }, + "invitation_code": { + "$ref": "#/components/schemas/ASCII" + }, + "label": { + "type": "string" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "managed_by": { + "$ref": "#/components/schemas/ManagedBy" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict" + }, + "sso_id": { + "$ref": "#/components/schemas/UserSSOId" + }, + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/BindingNewTeamUser" + }, + "team_code": { + "$ref": "#/components/schemas/ASCII" + }, + "team_id": { + "$ref": "#/components/schemas/UUID" + }, + "uuid": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "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" + }, + "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": { + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "self": { + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "self", + "copy" + ], + "type": "object" + }, + "PhoneNumber": { + "description": "A known phone number with a pending password reset.", + "type": "string" + }, + "Pict": { + "items": {}, + "maxItems": 10, + "minItems": 0, + "type": "array" + }, + "Prekey": { + "properties": { + "id": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "key": { + "type": "string" + } + }, + "required": [ + "id", + "key" + ], + "type": "object" + }, + "PrekeyBundle": { + "properties": { + "clients": { + "items": { + "$ref": "#/components/schemas/ClientPrekey" + }, + "type": "array" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "clients" + ], + "type": "object" + }, + "Priority": { + "enum": [ + "low", + "high" + ], + "type": "string" + }, + "PropertyKeysAndValues": { + "type": "object" + }, + "PropertyValue": { + "description": "An arbitrary JSON value for a property" + }, + "Protocol": { + "enum": [ + "proteus", + "mls", + "mixed" + ], + "type": "string" + }, + "ProtocolUpdate": { + "properties": { + "protocol": { + "$ref": "#/components/schemas/Protocol" + } + }, + "type": "object" + }, + "Provider": { + "properties": { + "description": { + "type": "string" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "id", + "name", + "email", + "url", + "description" + ], + "type": "object" + }, + "ProviderActivationResponse": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "ProviderLogin": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "email", + "password" + ], + "type": "object" + }, + "PubClient": { + "properties": { + "class": { + "$ref": "#/components/schemas/ClientClass" + }, + "id": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "PublicSubConversation": { + "description": "An MLS subconversation", + "properties": { + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "members": { + "items": { + "$ref": "#/components/schemas/ClientIdentity" + }, + "type": "array" + }, + "parent_qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "subconv_id": { + "type": "string" + } + }, + "required": [ + "parent_qualified_id", + "subconv_id", + "group_id", + "epoch", + "members" + ], + "type": "object" + }, + "PushToken": { + "description": "Native Push Token", + "properties": { + "app": { + "description": "Application", + "type": "string" + }, + "client": { + "description": "Client ID", + "type": "string" + }, + "token": { + "description": "Access Token", + "type": "string" + }, + "transport": { + "$ref": "#/components/schemas/Transport" + } + }, + "required": [ + "transport", + "app", + "token", + "client" + ], + "type": "object" + }, + "PushTokenList": { + "description": "List of Native Push Tokens", + "properties": { + "tokens": { + "description": "Push tokens", + "items": { + "$ref": "#/components/schemas/PushToken" + }, + "type": "array" + } + }, + "required": [ + "tokens" + ], + "type": "object" + }, + "QualifiedNewOtrMessage": { + "description": "This object can only be parsed from Protobuf.\nThe specification for the protobuf types is here: \nhttps://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto." + }, + "QualifiedUserClientPrekeyMapV4": { + "properties": { + "failed_to_list": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + }, + "qualified_user_client_prekeys": { + "additionalProperties": { + "$ref": "#/components/schemas/UserClientPrekeyMap" + }, + "type": "object" + } + }, + "required": [ + "qualified_user_client_prekeys" + ], + "type": "object" + }, + "QualifiedUserClients": { + "additionalProperties": { + "additionalProperties": { + "items": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "type": "array" + }, + "type": "object" + }, + "description": "Map of Domain to UserClients", + "example": { + "domain1.example.com": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + "60f85e4b15ad3786", + "6e323ab31554353b" + ] + } + }, + "type": "object" + }, + "QualifiedUserIdList with EdMemberLeftReason": { + "properties": { + "qualified_user_ids": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + }, + "reason": { + "$ref": "#/components/schemas/EdMemberLeftReason" + }, + "user_ids": { + "deprecated": true, + "description": "Deprecated, use qualified_user_ids", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "reason", + "qualified_user_ids", + "user_ids" + ], + "type": "object" + }, + "QualifiedUserMap_Set_PubClient": { + "additionalProperties": { + "$ref": "#/components/schemas/UserMap_Set_PubClient" + }, + "description": "Map of Domain to (UserMap (Set_PubClient)).", + "example": { + "domain1.example.com": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + { + "class": "legalhold", + "id": "d0" + } + ] + } + }, + "type": "object" + }, + "Qualified_ConvId": { + "properties": { + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain", + "id" + ], + "type": "object" + }, + "Qualified_Handle": { + "properties": { + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + } + }, + "required": [ + "domain", + "handle" + ], + "type": "object" + }, + "Qualified_UserId": { + "properties": { + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain", + "id" + ], + "type": "object" + }, + "QueuedNotification": { + "description": "A single notification", + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "payload": { + "description": "List of events", + "items": { + "$ref": "#/components/schemas/Object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "id", + "payload" + ], + "type": "object" + }, + "QueuedNotificationList": { + "description": "Zero or more notifications", + "properties": { + "has_more": { + "description": "Whether there are still more notifications.", + "type": "boolean" + }, + "notifications": { + "description": "Notifications", + "items": { + "$ref": "#/components/schemas/QueuedNotification" + }, + "type": "array" + }, + "time": { + "$ref": "#/components/schemas/UTCTime" + } + }, + "required": [ + "notifications" + ], + "type": "object" + }, + "RTCConfiguration": { + "description": "A subset of the WebRTC 'RTCConfiguration' dictionary", + "properties": { + "ice_servers": { + "description": "Array of 'RTCIceServer' objects", + "items": { + "$ref": "#/components/schemas/RTCIceServer" + }, + "minItems": 1, + "type": "array" + }, + "is_federating": { + "description": "True if the client should connect to an SFT in the sft_servers_all and request it to federate", + "type": "boolean" + }, + "sft_servers": { + "description": "Array of 'SFTServer' objects (optional)", + "items": { + "$ref": "#/components/schemas/SftServer" + }, + "minItems": 1, + "type": "array" + }, + "sft_servers_all": { + "description": "Array of all SFT servers", + "items": { + "$ref": "#/components/schemas/SftServer" + }, + "type": "array" + }, + "ttl": { + "description": "Number of seconds after which the configuration should be refreshed (advisory)", + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "ice_servers", + "ttl" + ], + "type": "object" + }, + "RTCIceServer": { + "description": "A subset of the WebRTC 'RTCIceServer' object", + "properties": { + "credential": { + "$ref": "#/components/schemas/ASCII" + }, + "urls": { + "description": "Array of TURN server addresses of the form 'turn::'", + "items": { + "$ref": "#/components/schemas/TurnURI" + }, + "minItems": 1, + "type": "array" + }, + "username": { + "$ref": "#/components/schemas/" + } + }, + "required": [ + "urls", + "username", + "credential" + ], + "type": "object" + }, + "RedirectUrl": { + "description": "The URL must match the URL that was used to generate the authorization code.", + "type": "string" + }, + "Relation": { + "enum": [ + "accepted", + "blocked", + "pending", + "ignored", + "sent", + "cancelled", + "missing-legalhold-consent" + ], + "type": "string" + }, + "RemoveBotResponse": { + "properties": { + "event": { + "$ref": "#/components/schemas/Event" + } + }, + "required": [ + "event" + ], + "type": "object" + }, + "RemoveCookies": { + "description": "Data required to remove cookies", + "properties": { + "ids": { + "description": "A list of cookie IDs to revoke", + "items": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "type": "array" + }, + "labels": { + "description": "A list of cookie labels for which to revoke the cookies", + "items": { + "type": "string" + }, + "type": "array" + }, + "password": { + "description": "The user's password", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "RemoveLegalHoldSettingsRequest": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "RichField": { + "properties": { + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "value" + ], + "type": "object" + }, + "RichInfoAssocList": { + "description": "json object with case-insensitive fields.", + "properties": { + "fields": { + "items": { + "$ref": "#/components/schemas/RichField" + }, + "type": "array" + }, + "version": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "version", + "fields" + ], + "type": "object" + }, + "Role": { + "description": "Role of the invited user", + "enum": [ + "owner", + "admin", + "member", + "partner" + ], + "type": "string" + }, + "RoleName": { + "description": "Role name, between 2 and 128 chars, 'wire_' prefix is reserved for roles designed by Wire (i.e., no custom roles can have the same prefix)", + "type": "string" + }, + "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" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "team", + "id", + "created_at", + "description" + ], + "type": "object" + }, + "ScimTokenList": { + "properties": { + "tokens": { + "items": { + "$ref": "#/components/schemas/ScimTokenInfo" + }, + "type": "array" + } + }, + "required": [ + "tokens" + ], + "type": "object" + }, + "SearchResult": { + "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" + }, + "Service": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "auth_tokens": { + "$ref": "#/components/schemas/List1" + }, + "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": { + "$ref": "#/components/schemas/List1" + }, + "summary": { + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/" + }, + "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": { + "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" + }, + "ServiceTagList": { + "items": { + "$ref": "#/components/schemas/" + }, + "type": "array" + }, + "SftServer": { + "description": "Inspired by WebRTC 'RTCIceServer' object, contains details of SFT servers", + "properties": { + "urls": { + "description": "Array containing exactly one SFT server address of the form 'https://:'", + "items": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "type": "array" + } + }, + "required": [ + "urls" + ], + "type": "object" + }, + "SimpleMember": { + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + } + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "SimpleMembers": { + "properties": { + "user_ids": { + "deprecated": true, + "description": "deprecated", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "users": { + "items": { + "$ref": "#/components/schemas/SimpleMember" + }, + "type": "array" + } + }, + "required": [ + "users" + ], + "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": {}, + "Sso": { + "properties": { + "issuer": { + "type": "string" + }, + "nameid": { + "type": "string" + } + }, + "required": [ + "issuer", + "nameid" + ], + "type": "object" + }, + "SsoSettings": { + "properties": { + "default_sso_code": { + "$ref": "#/components/schemas/UUID" + } + }, + "type": "object" + }, + "SupportedProtocolUpdate": { + "properties": { + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array" + } + }, + "required": [ + "supported_protocols" + ], + "type": "object" + }, + "SystemSettings": { + "properties": { + "setEnableMls": { + "description": "Whether MLS is enabled or not", + "type": "boolean" + }, + "setRestrictUserCreation": { + "description": "Do not allow certain user creation flows", + "type": "boolean" + } + }, + "required": [ + "setRestrictUserCreation", + "setEnableMls" + ], + "type": "object" + }, + "SystemSettingsPublic": { + "properties": { + "setRestrictUserCreation": { + "description": "Do not allow certain user creation flows", + "type": "boolean" + } + }, + "required": [ + "setRestrictUserCreation" + ], + "type": "object" + }, + "Team": { + "description": "`binding` is deprecated, and should be ignored. The non-binding teams API is not used (and will not be supported from API version V4 onwards), and `binding` will always be `true`.", + "properties": { + "binding": { + "$ref": "#/components/schemas/TeamBinding" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "icon": { + "$ref": "#/components/schemas/Icon" + }, + "icon_key": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + }, + "splash_screen": { + "$ref": "#/components/schemas/Icon" + } + }, + "required": [ + "id", + "creator", + "name", + "icon" + ], + "type": "object" + }, + "TeamBinding": { + "deprecated": true, + "description": "Deprecated, please ignore.", + "enum": [ + true, + false + ], + "type": "boolean" + }, + "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" + }, + "sso": { + "$ref": "#/components/schemas/Sso" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "id", + "name" + ], + "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" + }, + "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" + }, + "TokenType": { + "enum": [ + "Bearer" + ], + "type": "string" + }, + "Transport": { + "description": "Transport", + "enum": [ + "GCM", + "APNS", + "APNS_SANDBOX", + "APNS_VOIP", + "APNS_VOIP_SANDBOX" + ], + "type": "string" + }, + "TurnURI": { + "type": "string" + }, + "TypingData": { + "properties": { + "status": { + "$ref": "#/components/schemas/TypingStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "TypingStatus": { + "enum": [ + "started", + "stopped" + ], + "type": "string" + }, + "URIRef Absolute": { + "description": "URL of the invitation link to be sent to the invitee", + "type": "string" + }, + "UTCTime": { + "example": "2021-05-12T10:52:02Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + }, + "UTCTimeMillis": { + "description": "The time when the session was created", + "example": "2021-05-12T10:52:02.671Z", + "format": "yyyy-mm-ddThh:MM:ss.qqqZ", + "type": "string" + }, + "UUID": { + "description": "The OAuth client's ID", + "example": "99db9768-04e3-4b5d-9268-831b6a25c4ab", + "format": "uuid", + "type": "string" + }, + "Unnamed": { + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "permissions": { + "$ref": "#/components/schemas/Permissions" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "permissions" + ], + "type": "object" + }, + "UpdateBotPrekeys": { + "properties": { + "prekeys": { + "items": { + "$ref": "#/components/schemas/Prekey" + }, + "type": "array" + } + }, + "required": [ + "prekeys" + ], + "type": "object" + }, + "UpdateClient": { + "properties": { + "capabilities": { + "description": "Hints provided by the client for the backend so it can behave in a backwards-compatible way.", + "items": { + "$ref": "#/components/schemas/ClientCapability" + }, + "type": "array" + }, + "label": { + "description": "A new name for this client.", + "type": "string" + }, + "lastkey": { + "$ref": "#/components/schemas/Prekey" + }, + "mls_public_keys": { + "$ref": "#/components/schemas/MLSPublicKeys" + }, + "prekeys": { + "description": "New prekeys for other clients to establish OTR sessions.", + "items": { + "$ref": "#/components/schemas/Prekey" + }, + "type": "array" + } + }, + "type": "object" + }, + "UpdateProvider": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "type": "object" + }, + "UpdateService": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "description": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "summary": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/" + }, + "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" + }, + "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" + }, + "expires_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "managed_by": { + "$ref": "#/components/schemas/ManagedBy" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "service": { + "$ref": "#/components/schemas/ServiceRef" + }, + "sso_id": { + "$ref": "#/components/schemas/UserSSOId" + }, + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "text_status": { + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "qualified_id", + "name", + "accent_id", + "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" + }, + "UserIdList": { + "properties": { + "user_ids": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "user_ids" + ], + "type": "object" + }, + "UserLegalHoldStatus": { + "description": "The state of Legal Hold compliance for the member", + "enum": [ + "enabled", + "pending", + "disabled", + "no_consent" + ], + "type": "string" + }, + "UserLegalHoldStatusResponse": { + "properties": { + "client": { + "$ref": "#/components/schemas/Id" + }, + "last_prekey": { + "$ref": "#/components/schemas/Prekey" + }, + "status": { + "$ref": "#/components/schemas/UserLegalHoldStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "UserMap_Set_PubClient": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/PubClient" + }, + "type": "array", + "uniqueItems": true + }, + "description": "Map of UserId to (Set PubClient)", + "example": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + { + "class": "legalhold", + "id": "d0" + } + ] + }, + "type": "object" + }, + "UserProfile": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "expires_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "legalhold_status": { + "$ref": "#/components/schemas/UserLegalHoldStatus" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "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" + } + }, + "required": [ + "qualified_id", + "name", + "accent_id", + "legalhold_status" + ], + "type": "object" + }, + "UserSSOId": { + "properties": { + "scim_external_id": { + "type": "string" + }, + "subject": { + "type": "string" + }, + "tenant": { + "type": "string" + } + }, + "type": "object" + }, + "UserUpdate": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict" + }, + "text_status": { + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "type": "object" + }, + "ValidateSAMLEmailsConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "VerificationAction": { + "enum": [ + "create_scim_token", + "login", + "delete_team" + ], + "type": "string" + }, + "VerifyDeleteUser": { + "description": "Data for verifying an account deletion.", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "VersionInfo": { + "example": { + "development": [ + 7 + ], + "domain": "example.com", + "federation": false, + "supported": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ] + }, + "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 + ], + "type": "integer" + }, + "ViewLegalHoldService": { + "properties": { + "settings": { + "$ref": "#/components/schemas/ViewLegalHoldServiceInfo" + }, + "status": { + "$ref": "#/components/schemas/LHServiceStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "ViewLegalHoldServiceInfo": { + "properties": { + "auth_token": { + "$ref": "#/components/schemas/ASCII" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "fingerprint": { + "$ref": "#/components/schemas/Fingerprint" + }, + "public_key": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "team_id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "team_id", + "base_url", + "fingerprint", + "auth_token", + "public_key" + ], + "type": "object" + }, + "WireIdP": { + "properties": { + "apiVersion": { + "$ref": "#/components/schemas/WireIdPAPIVersion" + }, + "handle": { + "type": "string" + }, + "oldIssuers": { + "items": { + "type": "string" + }, + "type": "array" + }, + "replacedBy": { + "$ref": "#/components/schemas/UUID" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "team", + "oldIssuers", + "handle" + ], + "type": "object" + }, + "WireIdPAPIVersion": { + "enum": [ + "WireIdPAPIV1", + "WireIdPAPIV2" + ], + "type": "string" + }, + "XmlText": { + "properties": { + "fromXmlText": { + "type": "string" + } + }, + "required": [ + "fromXmlText" + ], + "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: 500, label: `federation-not-implemented`): Federated behaviour for a certain endpoint is not yet implemented.\n - **Federator discovery failed** (status: 400, label: `discovery-failure`): A DNS error occurred during discovery of a remote backend. This can be transient, so clients should retry the request.\n - **Local federation error** (status: 500, label: `federation-local-error`): An error occurred in the communication between this backend and its local federator. These errors are most likely caused by bugs in the backend, and should be reported as such.\n\n### Remote federation errors\n\nErrors in this category are returned in case of communication issues between the local backend and a remote one, or if the remote side encountered an error while processing an RPC. Some errors in this category might be caused by incorrect client behaviour, wrong user input, or incorrect certificate configuration. Possibly transient errors are indicated explicitly. We use non-standard 5xx status codes for these errors.\n\n - **HTTP2 error** (status: 533, label: `federation-http2-error`): The current federator encountered an error when making an HTTP2 request to a remote one. Check the error message for more details.\n - **Connection refused** (status: 521, label: `federation-connection-refused`): The local federator could not connect to a remote one. This could be transient, so clients should retry the request.\n - **TLS failure**: (status: 525, label: `federation-tls-error`): An error occurred during the TLS handshake between the local federator and a remote one. This is most likely due to an issue with the certificate on the remote end.\n - **Remote federation error** (status: 533, label: `federation-remote-error`): The remote backend could not process a request coming from this backend. Check the error message for more details.\n - **Version negotiation error** (status: 533, label: `federation-version-error`): The remote backend returned invalid version information.\n\n### Backend compatibility errors\n\nAn error in this category will be returned when this backend makes an invalid or unsupported RPC to another backend. This can indicate some incompatibility between backends or a backend bug. These errors are unlikely to be transient, so retrying requests is *not* advised.\n\n - **Version mismatch** (status: 531, label: `federation-version-mismatch`): A remote backend is running an unsupported version of the federator.\n - **Invalid content type** (status: 533, label: `federation-invalid-content-type`): An RPC to another backend returned with an invalid content type.\n - **Unsupported content type** (status: 533, label: `federation-unsupported-content-type`): An RPC to another backend returned with an unsupported content type.\n", + "title": "Wire-Server API", + "version": "" + }, + "openapi": "3.0.0", + "paths": { + "/": { + "get": { + "description": " [internal route ID: \"get-services-tags\"]\n\n", + "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" + } + }, + "/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.Calls federation service brig on send-connection-action", + "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.", + "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", + "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" + }, + "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.
Calls federation service brig on send-connection-action", + "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.Calls federation service brig on send-connection-action", + "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", + "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 example.com that you are attempting to register a user with has been blocked for creating wire users. Please contact your IT department." + }, + "properties": { + "code": { + "enum": [ + 451 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-blocked-for-registration" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "[Customer extension] the email domain example.com that you are attempting to register a user with has been blocked for creating wire users. Please contact your IT department. (label: `domain-blocked-for-registration`)" + } + }, + "summary": "Send (or resend) an email activation code." + } + }, + "/api-version": { + "get": { + "description": " [internal route ID: \"get-version\"]\n\n", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/VersionInfo" + } + } + }, + "description": "" + } + } + } + }, + "/assets": { + "post": { + "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": "invalid-length", + "message": "Invalid content length" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-length" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `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": "**Note**: only local assets can be deleted.", + "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": "**Note**: local assets result in a redirect, while remote assets are streamed directly.Calls federation service cargohold on stream-asset
Calls federation service cargohold on get-asset", + "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": "**Note**: deleting the token makes the asset public.", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset token deleted" + } + }, + "summary": "Delete an asset token" + }, + "post": { + "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" + }, + "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": { + "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": "invalid-length", + "message": "Invalid content length" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-length" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `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": { + "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": { + "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-v6\"]\n\n", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Clientv6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Clientv6" + } + } + }, + "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", + "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", + "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", + "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/messages": { + "post": { + "description": " [internal route ID: \"post-bot-message-unqualified\"]\n\nCalls federation service brig on get-user-clients
Calls federation service galley on on-message-sent", + "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", + "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", + "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", + "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", + "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 ID}/clients": { + "get": { + "description": " [internal route ID: \"bot-get-user-clients\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "User ID", + "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.", + "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.", + "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": { + "get": { + "deprecated": true, + "description": " [internal route ID: \"get-calls-config\"]\n\n", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RTCConfiguration" + } + } + }, + "description": "" + } + }, + "summary": "Retrieve TURN server addresses and credentials for IP addresses, scheme `turn` and transport `udp` only (deprecated)" + } + }, + "/calls/config/v2": { + "get": { + "description": " [internal route ID: \"get-calls-config-v2\"]\n\n", + "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-v6\"]\n\n", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientListv6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientListv6" + } + } + }, + "description": "List of clients" + } + }, + "summary": "List the registered clients" + }, + "post": { + "description": " [internal route ID: \"add-client\"]\n\nCalls federation service brig on send-connection-action", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewClient" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Client" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Client" + } + } + }, + "description": "Client registered", + "headers": { + "Location": { + "description": "Client ID", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "bad-request", + "message": "Malformed prekeys uploaded" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "bad-request" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMalformed prekeys uploaded (label: `bad-request`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "missing-auth", + "too-many-clients" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nRe-authentication via password required (label: `missing-auth`)\n\nToo many clients (label: `too-many-clients`)" + } + }, + "summary": "Register a new client" + } + }, + "/clients/{cid}/access-token": { + "post": { + "description": " [internal route ID: \"create-access-token\"]\n\nCreate an JWT DPoP access token for the client CSR, given a JWT DPoP proof, specified in the `DPoP` header. The access token will be returned as JWT DPoP token in the `DPoP` header.", + "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", + "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-v6\"]\n\n", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Clientv6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Clientv6" + } + } + }, + "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", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateClient" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Client updated" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "bad-request", + "message": "Malformed prekeys uploaded" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "bad-request" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMalformed prekeys uploaded (label: `bad-request`)" + } + }, + "summary": "Update a registered client" + } + }, + "/clients/{client}/capabilities": { + "get": { + "description": " [internal route ID: \"get-client-capabilities\"]\n\n", + "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.", + "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.", + "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", + "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", + "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
Calls federation service brig on send-connection-action
Calls federation service brig on get-users-by-ids", + "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\nCalls federation service brig on send-connection-action
Calls federation service brig on get-users-by-ids", + "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
Calls federation service galley on on-conversation-updated
Calls federation service galley on on-conversation-created
Calls federation service brig on get-not-fully-connected-backends
Calls federation service brig on api-version", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewConv" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationV6v6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationV6v6" + } + } + }, + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateGroupConversationv6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateGroupConversationv6" + } + } + }, + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled", + "non-empty-member-list" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nAttempting to add group members outside MLS (label: `non-empty-member-list`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "operation-denied", + "no-team-member", + "not-connected", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nUsers are not connected (label: `not-connected`)\n\nConversation access denied (label: `access-denied`)" + }, + "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.", + "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", + "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.Calls federation service galley on on-conversation-updated", + "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\nCalls federation service galley on get-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.", + "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", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Conversation" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Conversation" + } + } + }, + "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/one2one": { + "post": { + "description": " [internal route ID: \"create-one-to-one-conversation\"]\n\nCalls federation service galley on on-conversation-created", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewConv" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationV3v3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationV3v3" + } + } + }, + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationV3v3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationV3v3" + } + } + }, + "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" + } + }, + "/conversations/one2one/{usr_domain}/{usr}": { + "get": { + "description": " [internal route ID: \"get-one-to-one-mls-conversation@v6\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "usr_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "usr", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MLSOne2OneConversation_MLSPublicKey" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSOne2OneConversation_MLSPublicKey" + } + } + }, + "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": "MLS 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" + } + }, + "/conversations/self": { + "post": { + "description": " [internal route ID: \"create-self-conversation\"]\n\n", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationV6v6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationV6v6" + } + } + }, + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationV6v6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationV6v6" + } + } + }, + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + } + }, + "summary": "Create a self-conversation" + } + }, + "/conversations/{Conversation ID}/bots": { + "post": { + "description": " [internal route ID: \"add-bot\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "Conversation ID", + "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" + } + }, + "/conversations/{Conversation ID}/bots/{Bot ID}": { + "delete": { + "description": " [internal route ID: \"remove-bot\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "Conversation ID", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "Bot ID", + "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" + } + }, + "/conversations/{cnv_domain}/{cnv}": { + "get": { + "description": " [internal route ID: \"get-conversation\"]\n\nCalls federation service galley on get-conversations", + "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\nCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "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}/groupinfo": { + "get": { + "description": " [internal route ID: \"get-group-info\"]\n\nCalls federation service galley on query-group-info", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "message/mls": { + "schema": { + "$ref": "#/components/schemas/GroupInfoData" + } + } + }, + "description": "The group information" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "mls-missing-group-info", + "message": "The conversation has no group information" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-missing-group-info", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nThe conversation has no group information (label: `mls-missing-group-info`)\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get MLS group information" + } + }, + "/conversations/{cnv_domain}/{cnv}/members": { + "post": { + "description": " [internal route ID: \"add-members-to-conversation\"]\n\nCalls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "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" + }, + "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." + } + }, + "/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}": { + "delete": { + "description": " [internal route ID: \"remove-member\"]\n\nCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated
Calls federation service galley on leave-conversation", + "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.Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "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\nCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "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\nCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "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.Calls federation service galley on send-message
Calls federation service galley on on-message-sent
Calls federation service brig on get-user-clients", + "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.", + "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\nCalls federation service brig on get-users-by-ids
Calls federation service galley on update-conversation
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "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": "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_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": { + "put": { + "description": " [internal route ID: \"update-conversation-self\"]\n\n**Note**: at least one field has to be provided.", + "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\nCalls federation service galley on delete-sub-conversation", + "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/DeleteSubConversationRequest" + } + } + }, + "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\nCalls federation service galley on get-sub-conversation", + "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\nCalls federation service galley on query-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\nCalls federation service galley on leave-sub-conversation
Calls federation service galley on on-mls-message-sent", + "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\nCalls federation service galley on on-typing-indicator-updated
Calls federation service galley on update-typing-indicator", + "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}": { + "put": { + "deprecated": true, + "description": " [internal route ID: \"update-conversation-name-deprecated\"]\n\nUse `/conversations/:domain/:conv/name` instead.Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "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/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 updated" + }, + "204": { + "description": "Name 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", + "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` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update conversation name (deprecated)" + } + }, + "/conversations/{cnv}/code": { + "delete": { + "description": " [internal route ID: \"remove-code-unqualified\"]\n\n", + "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", + "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`", + "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", + "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}/members/{usr}": { + "put": { + "deprecated": true, + "description": " [internal route ID: \"update-other-member-unqualified\"]\n\nUse `PUT /conversations/:cnv_domain/:cnv/members/:usr_domain/:usr` insteadCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "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` 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 (deprecated)" + } + }, + "/conversations/{cnv}/message-timer": { + "put": { + "deprecated": true, + "description": " [internal route ID: \"update-conversation-message-timer-unqualified\"]\n\nUse `/conversations/:domain/:cnv/message-timer` instead.Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "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/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` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update the message timer for a conversation (deprecated)" + } + }, + "/conversations/{cnv}/name": { + "put": { + "deprecated": true, + "description": " [internal route ID: \"update-conversation-name-unqualified\"]\n\nUse `/conversations/:domain/:conv/name` instead.Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "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/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 updated" + }, + "204": { + "description": "Name 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", + "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` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update conversation name (deprecated)" + } + }, + "/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.Calls federation service brig on get-user-clients
Calls federation service galley on on-message-sent", + "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}/receipt-mode": { + "put": { + "deprecated": true, + "description": " [internal route ID: \"update-conversation-receipt-mode-unqualified\"]\n\nUse `PUT /conversations/:domain/:cnv/receipt-mode` instead.Calls federation service brig on get-users-by-ids
Calls federation service galley on update-conversation
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "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/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": "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_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` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update receipt mode for a conversation (deprecated)" + } + }, + "/conversations/{cnv}/roles": { + "get": { + "description": " [internal route ID: \"get-conversation-roles\"]\n\n", + "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" + } + }, + "/conversations/{cnv}/self": { + "get": { + "deprecated": true, + "description": " [internal route ID: \"get-conversation-self-unqualified\"]\n\n", + "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/Member" + } + } + }, + "description": "" + } + }, + "summary": "Get self membership properties (deprecated)" + }, + "put": { + "deprecated": true, + "description": " [internal route ID: \"update-conversation-self-unqualified\"]\n\nUse `/conversations/:domain/:conv/self` instead.", + "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/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` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update self membership properties (deprecated)" + } + }, + "/cookies": { + "get": { + "description": " [internal route ID: \"list-cookies\"]\n\n", + "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", + "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", + "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\nCalls federation service brig on send-connection-action", + "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." + } + }, + "/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`", + "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" + } + }, + "/identity-providers": { + "get": { + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPList" + } + } + }, + "description": "" + } + } + }, + "post": { + "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": 1, + "minLength": 32, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPConfig_WireIdP" + } + } + }, + "description": "" + } + } + } + }, + "/identity-providers/{id}": { + "delete": { + "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": { + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPConfig_WireIdP" + } + } + }, + "description": "" + } + } + }, + "put": { + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "handle", + "required": false, + "schema": { + "maxLength": 1, + "minLength": 32, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPConfig_WireIdP" + } + } + }, + "description": "" + } + } + } + }, + "/identity-providers/{id}/raw": { + "get": { + "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.", + "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.Calls federation service brig on get-users-by-ids", + "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 discretionCalls federation service brig on send-connection-action", + "parameters": [ + { + "description": "Request a persistent cookie instead of a session cookie", + "in": "query", + "name": "persist", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Login" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessToken" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AccessToken" + } + } + }, + "description": "OK", + "headers": { + "Set-Cookie": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "pending-activation", + "suspended", + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nAccount pending activation (label: `pending-activation`)\n\nAccount suspended (label: `suspended`)\n\nAuthentication failed (label: `invalid-credentials`)" + } + }, + "summary": "Authenticate a user to obtain a cookie and first access token" + } + }, + "/mls/commit-bundles": { + "post": { + "description": " [internal route ID: \"mls-commit-bundle\"]\n\n\n\n**Note**: this endpoint can execute proposals, and therefore return all possible errors associated with adding or removing members to a conversation, in addition to the ones listed below. See the documentation of [POST /conversations/{cnv}/members/v2](#/default/post_conversations__cnv__members_v2) and [POST /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}](#/default/delete_conversations__cnv_domain___cnv__members__usr_domain___usr_) for more details on the possible error responses of each type of proposal.
Calls federation service brig on api-version
Calls federation service brig on get-users-by-ids
Calls federation service brig on get-mls-clients
Calls federation service galley on on-conversation-updated
Calls federation service galley on send-mls-commit-bundle
Calls federation service galley on mls-welcome
Calls federation service galley on on-mls-message-sent", + "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-welcome-mismatch", + "message": "The list of targets of a welcome message does not match the list of new clients in a group" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "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\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-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": { + "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\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. For backwards compatibility, the `ciphersuite` parameter is optional, defaulting to ciphersuite 0x0001 when omitted.", + "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) - default is 0x0001", + "in": "query", + "name": "ciphersuite", + "required": false, + "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", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Ciphersuite in hex format (e.g. 0xf031) - default is 0x0001", + "in": "query", + "name": "ciphersuite", + "required": false, + "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.", + "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.", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Comma-separated list of ciphersuites in hex format (e.g. 0xf031) - default is 0x0001", + "in": "query", + "name": "ciphersuites", + "required": false, + "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", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Ciphersuite in hex format (e.g. 0xf031) - default is 0x0001", + "in": "query", + "name": "ciphersuite", + "required": false, + "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.
Calls federation service brig on get-mls-clients
Calls federation service galley on on-conversation-updated
Calls federation service galley on send-mls-message
Calls federation service galley on on-mls-message-sent", + "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-self-removal-not-allowed", + "message": "Self removal from group is not allowed" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "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\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": { + "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\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.", + "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" + } + }, + "/notifications": { + "get": { + "description": " [internal route ID: \"get-notifications\"]\n\n", + "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", + "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", + "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.", + "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}": { + "delete": { + "description": " [internal route ID: \"revoke-oauth-account-access-v6\"]\n\n", + "parameters": [ + { + "description": "The ID of the OAuth client", + "in": "path", + "name": "OAuthClientId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "OAuth application access revoked" + } + }, + "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.", + "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.", + "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", + "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.", + "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.", + "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" + } + }, + "/onboarding/v3": { + "post": { + "deprecated": true, + "description": " [internal route ID: \"onboarding\"]\n\nDEPRECATED: the feature has been turned off, the end-point does nothing and always returns '{\"results\":[],\"auto-connects\":[]}'.", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Body" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeprecatedMatchingResult" + } + } + }, + "description": "" + } + }, + "summary": "Upload contacts and invoke matching." + } + }, + "/password-reset": { + "post": { + "description": " [internal route ID: \"post-password-reset\"]\n\n", + "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", + "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." + } + }, + "/password-reset/{key}": { + "post": { + "deprecated": true, + "description": " [internal route ID: \"post-password-reset-key-deprecated\"]\n\nDEPRECATED: Use 'POST /password-reset/complete'.", + "parameters": [ + { + "description": "An opaque key for a pending password reset.", + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordReset" + } + } + }, + "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", + "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`)" + }, + "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`)" + } + }, + "summary": "Complete a password reset." + } + }, + "/properties": { + "delete": { + "description": " [internal route ID: \"clear-properties\"]\n\n", + "responses": { + "200": { + "description": "Properties cleared" + } + }, + "summary": "Clear all properties" + }, + "get": { + "description": " [internal route ID: \"list-property-keys\"]\n\n", + "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", + "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", + "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", + "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", + "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", + "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", + "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", + "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", + "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": { + "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": "invalid-length", + "message": "Invalid content length" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-length" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `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": { + "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": { + "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", + "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", + "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", + "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", + "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", + "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", + "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`\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", + "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", + "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", + "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", + "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", + "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", + "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", + "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", + "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", + "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/youtube/v3": {}, + "/push/tokens": { + "get": { + "description": " [internal route ID: \"get-push-tokens\"]\n\n", + "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", + "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", + "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.Calls federation service brig on send-connection-action", + "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`" + }, + "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" + ], + "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" + ], + "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`)" + }, + "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": { + "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", + "password-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`)\n\nPassword authentication failed. (label: `password-authentication-failed`)" + } + } + }, + "get": { + "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", + "password-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`)\n\nPassword authentication failed. (label: `password-authentication-failed`)" + } + } + }, + "post": { + "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", + "password-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`)\n\nPassword authentication failed. (label: `password-authentication-failed`)" + } + } + } + }, + "/search/contacts": { + "get": { + "description": " [internal route ID: \"search-contacts\"]\n\nCalls federation service brig on search-users
Calls federation service brig on get-users-by-ids", + "parameters": [ + { + "description": "Search query", + "in": "query", + "name": "q", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Searched domain. Note: This is optional only for backwards compatibility, future versions will mandate this.", + "in": "query", + "name": "domain", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Number of results to return (min: 1, max: 500, default 15)", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 500, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchResult" + } + } + }, + "description": "" + } + }, + "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.Calls federation service brig on send-connection-action", + "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`", + "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\nCalls federation service brig on send-connection-action", + "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.Calls federation service brig on send-connection-action", + "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-password", + "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-password", + "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 password. (label: `no-password`)\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\nCalls federation service brig on send-connection-action
Calls federation service brig on send-connection-action", + "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\nCalls federation service brig on send-connection-action", + "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", + "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", + "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", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SupportedProtocolUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Supported protocols changed" + } + }, + "summary": "Change your supported protocols" + } + }, + "/services": { + "get": { + "description": " [internal route ID: \"get-services\"]\n\n", + "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/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`)" + } + }, + "summary": "List services" + } + }, + "/sso/finalize-login": { + "post": { + "deprecated": true, + "description": "DEPRECATED! use /sso/metadata/:tid instead! Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams", + "responses": { + "200": { + "content": { + "text/plain;charset=utf-8": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/finalize-login/{team}": { + "post": { + "parameters": [ + { + "in": "path", + "name": "team", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/plain;charset=utf-8": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/initiate-login/{idp}": { + "get": { + "parameters": [ + { + "in": "query", + "name": "success_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "error_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "idp", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/html": { + "schema": { + "$ref": "#/components/schemas/FormRedirect" + } + } + }, + "description": "" + } + } + }, + "head": { + "parameters": [ + { + "in": "query", + "name": "success_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "error_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "idp", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/plain;charset=utf-8": {} + }, + "description": "" + } + } + } + }, + "/sso/metadata": { + "get": { + "deprecated": true, + "description": "DEPRECATED! use /sso/metadata/:tid instead! Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams", + "responses": { + "200": { + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/metadata/{team}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "team", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/settings": { + "get": { + "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", + "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", + "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/by-email": { + "head": { + "description": " [internal route ID: \"head-team-invitations\"]\n\n", + "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", + "parameters": [ + { + "description": "Invitation code", + "in": "query", + "name": "code", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invitation" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Invitation" + } + } + }, + "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'.", + "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", + "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", + "parameters": [ + { + "in": "path", + "name": "team-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "prefix", + "required": false, + "schema": { + "maxLength": 1, + "minLength": 128, + "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/ServiceProfile" + } + } + }, + "description": "" + } + }, + "summary": "Get whitelisted services by team id" + } + }, + "/teams/{tid}": { + "delete": { + "description": " [internal route ID: \"delete-team\"]\n\n", + "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`)" + }, + "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", + "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", + "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}/conversations": { + "get": { + "description": " [internal route ID: \"get-team-conversations\"]\n\n", + "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", + "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\nCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "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", + "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.", + "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/appLock": { + "get": { + "description": " [internal route ID: (\"get\", AppLockConfig)]\n\n", + "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\", AppLockConfig)]\n\n", + "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/classifiedDomains": { + "get": { + "description": " [internal route ID: (\"get\", ClassifiedDomainsConfig)]\n\n", + "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\", ConferenceCallingConfig)]\n\n", + "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\", ConferenceCallingConfig)]\n\n", + "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/conversationGuestLinks": { + "get": { + "description": " [internal route ID: (\"get\", GuestLinksConfig)]\n\n", + "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", + "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", + "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/enforceFileDownloadLocation": { + "get": { + "description": " [internal route ID: (\"get\", EnforceFileDownloadLocationConfig)]\n\n

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

", + "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\", EnforceFileDownloadLocationConfig)]\n\n

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

", + "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", + "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", + "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", + "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", + "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", + "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", + "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", + "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/mls": { + "get": { + "description": " [internal route ID: (\"get\", MLSConfig)]\n\n", + "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\", MLSConfig)]\n\n", + "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\", MlsE2EIdConfig)]\n\n", + "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\", MlsE2EIdConfig)]\n\n", + "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\", MlsMigrationConfig)]\n\n", + "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\", MlsMigrationConfig)]\n\n", + "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", + "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", + "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", + "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", + "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", + "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", + "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\", SelfDeletingMessagesConfig)]\n\n", + "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\", SelfDeletingMessagesConfig)]\n\n", + "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/sndFactorPasswordChallenge": { + "get": { + "description": " [internal route ID: (\"get\", SndFactorPasswordChallengeConfig)]\n\n", + "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", + "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", + "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/validateSAMLemails": { + "get": { + "description": " [internal route ID: (\"get\", ValidateSAMLEmailsConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ValidateSAMLEmailsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for validateSAMLemails" + } + }, + "/teams/{tid}/get-members-by-ids-using-post": { + "post": { + "description": " [internal route ID: \"get-team-members-by-ids\"]\n\nThe `has_more` field in the response body is always `false`.", + "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", + "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.", + "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-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": "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", + "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", + "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`)" + } + }, + "summary": "Get a pending team invitation by ID." + } + }, + "/teams/{tid}/legalhold/consent": { + "post": { + "description": " [internal route ID: \"consent-to-legal-hold\"]\n\nCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "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)
Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "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`)" + }, + "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", + "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", + "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)
Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "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`)" + }, + "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", + "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)
Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "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": "legalhold-no-consent", + "message": "user has not given consent to using legal hold" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-no-consent", + "legalhold-already-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "user has not given consent to using legal hold (label: `legalhold-no-consent`)\n\nlegal hold is already enabled for this user (label: `legalhold-already-enabled`)" + }, + "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)
Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "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`)" + }, + "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", + "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", + "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.", + "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", + "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`)" + } + }, + "summary": "Remove an existing team member" + }, + "get": { + "description": " [internal route ID: \"get-team-member\"]\n\n", + "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", + "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": { + "format": "int32", + "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" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchResult" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchResult" + } + } + }, + "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", + "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", + "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.", + "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" + } + }, + "/users/handles": { + "post": { + "description": " [internal route ID: \"check-user-handles\"]\n\n", + "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" + } + }, + "/users/handles/{handle}": { + "head": { + "description": " [internal route ID: \"check-user-handle\"]\n\n", + "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" + } + }, + "/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 responseCalls federation service brig on get-user-clients", + "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.Calls federation service brig on claim-multi-prekey-bundle", + "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\nCalls federation service brig on get-users-by-ids", + "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\nCalls federation service brig on get-user-clients", + "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\nCalls federation service brig on get-user-clients", + "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\nCalls federation service brig on claim-prekey-bundle", + "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\nCalls federation service brig on claim-prekey", + "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", + "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.", + "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", + "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" + } + }, + "/verification-code/send": { + "post": { + "description": " [internal route ID: \"send-verification-code\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SendVerificationCode" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Verification code sent." + } + }, + "summary": "Send a verification code to a given email address." + } + } + }, + "security": [ + { + "ZAuth": [] + } + ], + "servers": [ + { + "url": "/v6" + } + ] +} diff --git a/services/brig/src/Brig/API/Auth.hs b/services/brig/src/Brig/API/Auth.hs index 581876fe0ce..021ca38aabc 100644 --- a/services/brig/src/Brig/API/Auth.hs +++ b/services/brig/src/Brig/API/Auth.hs @@ -22,13 +22,9 @@ import Brig.API.Handler import Brig.API.Types import Brig.API.User import Brig.App -import Brig.Data.User qualified as User -import Brig.Effects.BlacklistStore -import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Options import Brig.User.Auth qualified as Auth import Brig.ZAuth hiding (Env, settings) -import Control.Lens (view) import Control.Monad.Trans.Except import Data.CommaSeparatedList import Data.Id @@ -37,14 +33,13 @@ import Data.List1 (List1 (..)) import Data.Qualified import Data.Text qualified as T import Data.Text.Lazy qualified as LT -import Data.Time.Clock (UTCTime) import Data.ZAuth.Token qualified as ZAuth import Imports import Network.HTTP.Types import Network.Wai.Utilities ((!>>)) import Network.Wai.Utilities.Error qualified as Wai import Polysemy -import Polysemy.Input (Input) +import Polysemy.Input import Polysemy.TinyLog (TinyLog) import Wire.API.Error import Wire.API.Error.Brig qualified as E @@ -53,11 +48,13 @@ import Wire.API.User.Auth hiding (access) import Wire.API.User.Auth.LegalHold import Wire.API.User.Auth.ReAuth import Wire.API.User.Auth.Sso +import Wire.ActivationCodeStore (ActivationCodeStore) +import Wire.AuthenticationSubsystem +import Wire.AuthenticationSubsystem qualified as Authentication +import Wire.BlockListStore import Wire.EmailSubsystem (EmailSubsystem) +import Wire.Events (Events) import Wire.GalleyAPIAccess -import Wire.NotificationSubsystem -import Wire.PasswordStore (PasswordStore) -import Wire.Sem.Paging.Cassandra (InternalPaging) import Wire.UserKeyStore import Wire.UserStore import Wire.UserSubsystem @@ -65,11 +62,8 @@ import Wire.VerificationCodeSubsystem (VerificationCodeSubsystem) accessH :: ( Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r ) => Maybe ClientId -> [Either Text SomeUserToken] -> @@ -84,11 +78,8 @@ accessH mcid ut' mat' = do access :: ( TokenPair u a, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r ) => Maybe ClientId -> NonEmpty (Token u) -> @@ -106,15 +97,14 @@ sendLoginCode _ = login :: ( Member GalleyAPIAccess r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, - Member PasswordStore r, Member UserKeyStore r, Member UserStore r, - Member VerificationCodeSubsystem r + Member Events r, + Member (Input (Local ())) r, + Member UserSubsystem r, + Member ActivationCodeStore r, + Member VerificationCodeSubsystem r, + Member AuthenticationSubsystem r ) => Login -> Maybe Bool -> @@ -139,9 +129,10 @@ logout _ Nothing = throwStd authMissingToken logout uts (Just at) = Auth.logout (List1 uts) at !>> zauthError changeSelfEmailH :: - ( Member BlacklistStore r, + ( Member BlockListStore r, Member UserKeyStore r, - Member EmailSubsystem r + Member EmailSubsystem r, + Member UserSubsystem r ) => [Either Text SomeUserToken] -> Maybe (Either Text SomeAccessToken) -> @@ -169,18 +160,23 @@ listCookies lusr (fold -> labels) = CookieList <$> wrapClientE (Auth.listCookies (tUnqualified lusr) (toList labels)) -removeCookies :: (Member TinyLog r, Member PasswordStore r) => Local UserId -> RemoveCookies -> Handler r () +removeCookies :: + ( Member TinyLog r, + Member UserSubsystem r, + Member AuthenticationSubsystem r + ) => + Local UserId -> + RemoveCookies -> + Handler r () removeCookies lusr (RemoveCookies pw lls ids) = - Auth.revokeAccess (tUnqualified lusr) pw ids lls !>> authError + Auth.revokeAccess lusr pw ids lls !>> authError legalHoldLogin :: ( Member GalleyAPIAccess r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r, + Member AuthenticationSubsystem r ) => LegalHoldLogin -> Handler r SomeAccess @@ -191,11 +187,9 @@ legalHoldLogin lhl = do ssoLogin :: ( Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member AuthenticationSubsystem r, + Member UserSubsystem r, + Member Events r ) => SsoLogin -> Maybe Bool -> @@ -208,12 +202,21 @@ ssoLogin l (fromMaybe False -> persist) = do getLoginCode :: Phone -> Handler r PendingLoginCode getLoginCode _ = throwStd loginCodeNotFound -reauthenticate :: (Member GalleyAPIAccess r, Member VerificationCodeSubsystem r) => UserId -> ReAuthUser -> Handler r () -reauthenticate uid body = do - wrapClientE (User.reauthenticate uid (reAuthPassword body)) !>> reauthError +reauthenticate :: + ( Member GalleyAPIAccess r, + Member VerificationCodeSubsystem r, + Member AuthenticationSubsystem r, + Member UserSubsystem r + ) => + Local UserId -> + ReAuthUser -> + Handler r () +reauthenticate luid@(tUnqualified -> uid) body = do + (lift . liftSem $ Authentication.reauthenticateEither uid body.reAuthPassword) + >>= either (throwE . reauthError) (const $ pure ()) case reAuthCodeAction body of Just action -> - Auth.verifyCode (reAuthCode body) action uid + Auth.verifyCode (reAuthCode body) action luid `catchE` \case VerificationCodeRequired -> throwE $ reauthError ReAuthCodeVerificationRequired VerificationCodeNoPendingCode -> throwE $ reauthError ReAuthCodeVerificationNoPendingCode @@ -228,14 +231,14 @@ mkUserTokenCookie :: Cookie (Token u) -> m UserTokenCookie mkUserTokenCookie c = do - s <- view settings + s <- asks (.settings) pure UserTokenCookie { utcExpires = - guard (cookieType c == PersistentCookie) - $> cookieExpires c, - utcToken = mkSomeToken (cookieValue c), - utcSecure = not (setCookieInsecure s) + guard (c.cookieType == PersistentCookie) + $> c.cookieExpires, + utcToken = mkSomeToken c.cookieValue, + utcSecure = not s.cookieInsecure } partitionTokens :: diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index e4511fae9a1..9a94f880659 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -53,7 +53,6 @@ import Brig.App import Brig.Data.Client qualified as Data import Brig.Data.Nonce as Nonce import Brig.Data.User qualified as Data -import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.JwtTools (JwtTools) import Brig.Effects.JwtTools qualified as JwtTools import Brig.Effects.PublicKeyBundle (PublicKeyBundle) @@ -69,13 +68,12 @@ import Brig.User.Auth qualified as UserAuth import Brig.User.Auth.Cookie qualified as Auth import Cassandra (MonadClient) import Control.Error -import Control.Lens (view) import Control.Monad.Trans.Except (except) import Data.ByteString (toStrict) import Data.ByteString.Conversion import Data.Code as Code import Data.Domain -import Data.Either.Extra (mapLeft) +import Data.HavePendingInvitations import Data.Id (ClientId, ConnId, UserId) import Data.List.Split (chunksOf) import Data.Map.Strict qualified as Map @@ -84,13 +82,10 @@ import Data.Qualified import Data.Set qualified as Set import Data.Text.Encoding qualified as T import Data.Text.Encoding.Error -import Data.Time.Clock (UTCTime) import Imports import Network.HTTP.Types.Method (StdMethod) import Network.Wai.Utilities import Polysemy -import Polysemy.Input (Input) -import Polysemy.TinyLog import Servant (Link, ToHttpApiData (toUrlPiece)) import System.Logger.Class (field, msg, val, (~~)) import System.Logger.Class qualified as Log @@ -107,15 +102,20 @@ import Wire.API.User.Client.DPoPAccessToken import Wire.API.User.Client.Prekey import Wire.API.UserEvent import Wire.API.UserMap (QualifiedUserMap (QualifiedUserMap, qualifiedUserMap), UserMap (userMap)) +import Wire.AuthenticationSubsystem (AuthenticationSubsystem) +import Wire.AuthenticationSubsystem qualified as Authentication import Wire.DeleteQueue import Wire.EmailSubsystem (EmailSubsystem, sendNewClientEmail) +import Wire.Events (Events) +import Wire.Events qualified as Events import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.NotificationSubsystem import Wire.Sem.Concurrency import Wire.Sem.FromUTC (FromUTC (fromUTCTime)) import Wire.Sem.Now as Now -import Wire.Sem.Paging.Cassandra (InternalPaging) +import Wire.UserSubsystem (UserSubsystem) +import Wire.UserSubsystem qualified as User import Wire.VerificationCodeSubsystem (VerificationCodeSubsystem) lookupLocalClient :: UserId -> ClientId -> (AppT r) (Maybe Client) @@ -163,17 +163,15 @@ lookupLocalPubClientsBulk = lift . wrapClient . Data.lookupPubClientsBulk addClient :: ( Member GalleyAPIAccess r, - Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r, + Member UserSubsystem r, Member DeleteQueue r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, - Member VerificationCodeSubsystem r + Member AuthenticationSubsystem r, + Member VerificationCodeSubsystem r, + Member Events r ) => - UserId -> + Local UserId -> Maybe ConnId -> NewClient -> ExceptT ClientError (AppT r) Client @@ -184,44 +182,43 @@ addClient = addClientWithReAuthPolicy Data.reAuthForNewClients addClientWithReAuthPolicy :: forall r. ( Member GalleyAPIAccess r, - Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, Member DeleteQueue r, - Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, + Member Events r, + Member UserSubsystem r, + Member AuthenticationSubsystem r, Member VerificationCodeSubsystem r ) => Data.ReAuthPolicy -> - UserId -> + Local UserId -> Maybe ConnId -> NewClient -> ExceptT ClientError (AppT r) Client -addClientWithReAuthPolicy policy u con new = do - acc <- lift (wrapClient $ Data.lookupAccount u) >>= maybe (throwE (ClientUserNotFound u)) pure - verifyCode (newClientVerificationCode new) (userId . accountUser $ acc) - maxPermClients <- fromMaybe Opt.defUserMaxPermClients . Opt.setUserMaxPermClients <$> view settings - let caps :: Maybe (Set ClientCapability) +addClientWithReAuthPolicy policy luid@(tUnqualified -> u) con new = do + usr <- + (lift . liftSem $ User.getAccountNoFilter luid) + >>= maybe (throwE (ClientUserNotFound u)) pure + verifyCode (newClientVerificationCode new) luid + maxPermClients <- fromMaybe Opt.defUserMaxPermClients <$> asks (.settings.userMaxPermClients) + let caps :: Maybe ClientCapabilityList caps = updlhdev $ newClientCapabilities new where + updlhdev :: Maybe ClientCapabilityList -> Maybe ClientCapabilityList updlhdev = if newClientType new == LegalHoldClientType - then Just . maybe (Set.singleton lhcaps) (Set.insert lhcaps) + then Just . ClientCapabilityList . maybe (Set.singleton lhcaps) (Set.insert lhcaps . fromClientCapabilityList) else id lhcaps = ClientSupportsLegalholdImplicitConsent (clt0, old, count) <- - wrapClientE - (Data.addClientWithReAuthPolicy policy u clientId' new maxPermClients caps) + Data.addClientWithReAuthPolicy policy luid clientId' new maxPermClients caps !>> ClientDataError let clt = clt0 {clientMLSPublicKeys = newClientMLSPublicKeys new} - let usr = accountUser acc lift $ do for_ old $ execDelete u con liftSem $ GalleyAPIAccess.newClient u (clientId clt) liftSem $ Intra.onClientEvent u con (ClientAdded clt) - when (clientType clt == LegalHoldClientType) $ liftSem $ Intra.onUserEvent u con (UserLegalHoldEnabled u) + when (clientType clt == LegalHoldClientType) $ liftSem $ Events.generateUserEvent u con (UserLegalHoldEnabled u) when (count > 1) $ for_ (userEmail usr) $ \email -> @@ -232,12 +229,12 @@ addClientWithReAuthPolicy policy u con new = do verifyCode :: Maybe Code.Value -> - UserId -> + Local UserId -> ExceptT ClientError (AppT r) () - verifyCode mbCode uid = + verifyCode mbCode luid1 = -- this only happens inside the login flow (in particular, when logging in from a new device) -- the code obtained for logging in is used a second time for adding the device - UserAuth.verifyCode mbCode Code.Login uid `catchE` \case + UserAuth.verifyCode mbCode Code.Login luid1 `catchE` \case VerificationCodeRequired -> throwE ClientCodeAuthenticationRequired VerificationCodeNoPendingCode -> throwE ClientCodeAuthenticationFailed VerificationCodeNoEmail -> throwE ClientCodeAuthenticationFailed @@ -247,8 +244,7 @@ updateClient u c r = do client <- lift (Data.lookupClient u c) >>= maybe (throwE ClientNotFound) pure for_ (updateClientLabel r) $ lift . Data.updateClientLabel u c . Just for_ (updateClientCapabilities r) $ \caps' -> do - let ClientCapabilityList caps = clientCapabilities client - if caps `Set.isSubsetOf` caps' + if client.clientCapabilities.fromClientCapabilityList `Set.isSubsetOf` caps'.fromClientCapabilityList then lift . Data.updateClientCapabilities u c . Just $ caps' else throwE ClientCapabilitiesCannotBeRemoved let lk = maybeToList (unpackLastPrekey <$> updateClientLastKey r) @@ -258,7 +254,9 @@ updateClient u c r = do -- nb. We must ensure that the set of clients known to brig is always -- a superset of the clients known to galley. rmClient :: - (Member DeleteQueue r) => + ( Member DeleteQueue r, + Member AuthenticationSubsystem r + ) => UserId -> ConnId -> ClientId -> @@ -274,7 +272,9 @@ rmClient u con clt pw = -- Temporary clients don't need to re-auth TemporaryClientType -> pure () -- All other clients must authenticate - _ -> wrapClientE (Data.reauthenticate u pw) !>> ClientDataError . ClientReAuthError + _ -> + (lift . liftSem $ Authentication.reauthenticateEither u pw) + >>= either (throwE . ClientDataError . ClientReAuthError) (const $ pure ()) lift $ execDelete u (Just con) client claimPrekey :: @@ -514,19 +514,9 @@ pubClient c = pubClientClass = clientClass c } -legalHoldClientRequested :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r - ) => - UserId -> - LegalHoldClientRequest -> - AppT r () +legalHoldClientRequested :: (Member Events r) => UserId -> LegalHoldClientRequest -> AppT r () legalHoldClientRequested targetUser (LegalHoldClientRequest _requester lastPrekey') = - liftSem $ Intra.onUserEvent targetUser Nothing lhClientEvent + liftSem $ Events.generateUserEvent targetUser Nothing lhClientEvent where clientId :: ClientId clientId = clientIdFromPrekey $ unpackLastPrekey lastPrekey' @@ -535,24 +525,14 @@ legalHoldClientRequested targetUser (LegalHoldClientRequest _requester lastPreke lhClientEvent :: UserEvent lhClientEvent = LegalHoldClientRequested eventData -removeLegalHoldClient :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member DeleteQueue r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r - ) => - UserId -> - AppT r () +removeLegalHoldClient :: (Member DeleteQueue r, Member Events r) => UserId -> AppT r () removeLegalHoldClient uid = do clients <- wrapClient $ Data.lookupClients uid -- Should only be one; but just in case we'll treat it as a list let legalHoldClients = filter ((== LegalHoldClientType) . clientType) clients -- maybe log if this isn't the case forM_ legalHoldClients (execDelete uid Nothing) - liftSem $ Intra.onUserEvent uid Nothing (UserLegalHoldDisabled uid) + liftSem $ Events.generateUserEvent uid Nothing (UserLegalHoldDisabled uid) createAccessToken :: (Member JwtTools r, Member Now r, Member PublicKeyBundle r) => @@ -585,12 +565,12 @@ createAccessToken luid cid method link proof = do note MisconfiguredRequestUrl $ fromByteString $ "https://" <> toByteString' domain <> "/" <> T.encodeUtf8 (toUrlPiece link) - maxSkewSeconds <- Opt.setDpopMaxSkewSecs <$> view settings - expiresIn <- Opt.setDpopTokenExpirationTimeSecs <$> view settings + maxSkewSeconds <- Opt.setDpopMaxSkewSecs <$> asks (.settings) + expiresIn <- Opt.dpopTokenExpirationTimeSecs <$> asks (.settings) now <- fromUTCTime <$> lift (liftSem Now.get) let expiresAt = now & addToEpoch expiresIn pubKeyBundle <- do - pathToKeys <- ExceptT $ note KeyBundleError . Opt.setPublicKeyBundle <$> view settings + pathToKeys <- ExceptT (note KeyBundleError <$> asks (.settings.publicKeyBundle)) ExceptT $ note KeyBundleError <$> liftSem (PublicKeyBundle.get pathToKeys) token <- ExceptT $ diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index f718cd465d1..7dbbc0b86a6 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -41,13 +41,11 @@ import Brig.App import Brig.Data.Connection qualified as Data import Brig.Data.Types (resultHasMore, resultList) import Brig.Data.User qualified as Data -import Brig.Effects.FederationConfigStore import Brig.IO.Intra qualified as Intra import Brig.IO.Logging import Brig.Options import Brig.Types.Connection import Control.Error -import Control.Lens (view) import Control.Monad.Catch (throwM) import Data.Id as Id import Data.LegalHold qualified as LH @@ -68,10 +66,12 @@ import Wire.API.Error.Brig qualified as E import Wire.API.Routes.Public.Util (ResponseForExistedCreated (..)) import Wire.API.User import Wire.API.UserEvent +import Wire.FederationConfigStore import Wire.GalleyAPIAccess import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.NotificationSubsystem import Wire.UserStore +import Wire.UserSubsystem ensureNotSameTeam :: (Member GalleyAPIAccess r) => Local UserId -> Local UserId -> (ConnectionM r) () ensureNotSameTeam self target = do @@ -86,6 +86,7 @@ createConnection :: Member NotificationSubsystem r, Member TinyLog r, Member UserStore r, + Member UserSubsystem r, Member (Embed HttpClientIO) r ) => Local UserId -> @@ -106,6 +107,7 @@ createConnectionToLocalUser :: Member NotificationSubsystem r, Member TinyLog r, Member UserStore r, + Member UserSubsystem r, Member (Embed HttpClientIO) r ) => Local UserId -> @@ -116,7 +118,7 @@ createConnectionToLocalUser self conn target = do ensureNotSameAndActivated self (tUntagged target) noteT (InvalidUser (tUntagged target)) $ ensureIsActivated target - checkLegalholdPolicyConflict (tUnqualified self) (tUnqualified target) + checkLegalholdPolicyConflict self target ensureNotSameTeam self target s2o <- lift . wrapClient $ Data.lookupConnection self (tUntagged target) o2s <- lift . wrapClient $ Data.lookupConnection target (tUntagged self) @@ -194,9 +196,9 @@ createConnectionToLocalUser self conn target = do -- FUTUREWORK: we may want to move this to the LH application logic, so we can recycle it for -- group conv creation and possibly other situations. checkLegalholdPolicyConflict :: - (Member GalleyAPIAccess r) => - UserId -> - UserId -> + (Member GalleyAPIAccess r, Member UserSubsystem r) => + Local UserId -> + Local UserId -> ExceptT ConnectionError (AppT r) () checkLegalholdPolicyConflict uid1 uid2 = do let catchProfileNotFound = @@ -339,7 +341,7 @@ updateConnectionToLocalUser self other newStatus conn = do logLocalConnection (tUnqualified self) (qUnqualified (ucTo s2o)) . msg (val "Blocking connection") traverse_ (liftSem . Intra.blockConv self) (ucConvId s2o) - mlsEnabled <- view (settings . enableMLS) + mlsEnabled <- asks (.settings.enableMLS) liftSem $ when (fromMaybe False mlsEnabled) $ do let mlsConvId = one2OneConvId BaseProtocolMLSTag (tUntagged self) (tUntagged other) isEstablished <- isMLSOne2OneEstablished self (tUntagged other) @@ -355,7 +357,7 @@ updateConnectionToLocalUser self other newStatus conn = do logLocalConnection (tUnqualified self) (qUnqualified (ucTo s2o)) . msg (val "Unblocking connection") cnv <- lift . liftSem $ traverse (unblockConversation self conn) (ucConvId s2o) - mlsEnabled <- view (settings . enableMLS) + mlsEnabled <- asks (.settings.enableMLS) lift . liftSem $ when (fromMaybe False mlsEnabled) $ do let mlsConvId = one2OneConvId BaseProtocolMLSTag (tUntagged self) (tUntagged other) isEstablished <- isMLSOne2OneEstablished self (tUntagged other) diff --git a/services/brig/src/Brig/API/Connection/Remote.hs b/services/brig/src/Brig/API/Connection/Remote.hs index 03b650731c8..0bf4c398d5f 100644 --- a/services/brig/src/Brig/API/Connection/Remote.hs +++ b/services/brig/src/Brig/API/Connection/Remote.hs @@ -28,13 +28,11 @@ import Brig.API.Types (ConnectionError (..)) import Brig.App import Brig.Data.Connection qualified as Data import Brig.Data.User qualified as Data -import Brig.Effects.FederationConfigStore import Brig.Federation.Client as Federation import Brig.IO.Intra qualified as Intra import Brig.Options import Control.Comonad import Control.Error.Util ((??)) -import Control.Lens (view) import Control.Monad.Trans.Except import Data.Id as Id import Data.Qualified @@ -51,6 +49,7 @@ import Wire.API.Routes.Internal.Galley.ConversationsIntra import Wire.API.Routes.Public.Util (ResponseForExistedCreated (..)) import Wire.API.User import Wire.API.UserEvent +import Wire.FederationConfigStore import Wire.GalleyAPIAccess import Wire.NotificationSubsystem import Wire.UserStore @@ -195,7 +194,7 @@ transitionTo self mzcon other (Just connection) (Just rel) actor = do $ ucConvId connection desiredMem = desiredMembership actor rel lift $ updateOne2OneConv self Nothing other proteusConvId desiredMem actor - mlsEnabled <- view (settings . enableMLS) + mlsEnabled <- asks (.settings.enableMLS) when (fromMaybe False mlsEnabled) $ do let mlsConvId = one2OneConvId BaseProtocolMLSTag (tUntagged self) (tUntagged other) isEstablished <- lift . liftSem $ isMLSOne2OneEstablished self (tUntagged other) diff --git a/services/brig/src/Brig/API/Connection/Util.hs b/services/brig/src/Brig/API/Connection/Util.hs index 118c03bcc03..e256ff373f4 100644 --- a/services/brig/src/Brig/API/Connection/Util.hs +++ b/services/brig/src/Brig/API/Connection/Util.hs @@ -26,9 +26,8 @@ where import Brig.API.Types import Brig.App import Brig.Data.Connection qualified as Data -import Brig.Options (Settings (setUserMaxConnections)) +import Brig.Options (Settings (userMaxConnections)) import Control.Error (MaybeT, noteT) -import Control.Lens (view) import Control.Monad.Trans.Except import Data.Id (UserId) import Data.Qualified @@ -44,7 +43,7 @@ type ConnectionM r = ExceptT ConnectionError (AppT r) checkLimit :: Local UserId -> ExceptT ConnectionError (AppT r) () checkLimit u = noteT (TooManyConnections (tUnqualified u)) $ do n <- lift . wrapClient $ Data.countConnections u [Accepted, Sent] - l <- setUserMaxConnections <$> view settings + l <- asks (.settings.userMaxConnections) guard (n < l) ensureNotSameAndActivated :: (Member UserStore r) => Local UserId -> Qualified UserId -> ConnectionM r () diff --git a/services/brig/src/Brig/API/Error.hs b/services/brig/src/Brig/API/Error.hs index 9c120e6d5b5..019e3786c1b 100644 --- a/services/brig/src/Brig/API/Error.hs +++ b/services/brig/src/Brig/API/Error.hs @@ -19,14 +19,12 @@ module Brig.API.Error where import Brig.API.Types import Control.Monad.Error.Class -import Data.Aeson import Data.ByteString.Conversion import Data.Domain (Domain) import Data.Jwt.Tools (DPoPTokenGenerationError (..)) import Data.Text.Lazy as LT import Data.ZAuth.Validation qualified as ZAuth import Imports -import Network.HTTP.Types.Header import Network.HTTP.Types.Status import Network.Wai.Utilities.Error qualified as Wai import Wire.API.Error @@ -38,9 +36,6 @@ import Wire.Error throwStd :: (MonadError HttpError m) => Wai.Error -> m a throwStd = throwError . StdError -throwRich :: (MonadError HttpError m, ToJSON x) => Wai.Error -> x -> [Header] -> m a -throwRich e x h = throwError (RichError e x h) - -- Error Mapping ---------------------------------------------------------- connError :: ConnectionError -> HttpError @@ -87,12 +82,6 @@ changeEmailError (EmailExists _) = StdError (errorToWai @'E.UserKeyExists) changeEmailError (ChangeBlacklistedEmail _) = StdError blacklistedEmail changeEmailError EmailManagedByScim = StdError $ propertyManagedByScim "email" -changeHandleError :: ChangeHandleError -> HttpError -changeHandleError ChangeHandleNoIdentity = StdError (errorToWai @'E.NoIdentity) -changeHandleError ChangeHandleExists = StdError (errorToWai @'E.HandleExists) -changeHandleError ChangeHandleInvalid = StdError (errorToWai @'E.InvalidHandle) -changeHandleError ChangeHandleManagedByScim = StdError (errorToWai @'E.HandleManagedByScim) - legalHoldLoginError :: LegalHoldLoginError -> HttpError legalHoldLoginError LegalHoldLoginNoBindingTeam = StdError noBindingTeam legalHoldLoginError LegalHoldLoginLegalHoldNotEnabled = StdError legalHoldNotEnabled @@ -144,6 +133,7 @@ clientError (ClientDataError e) = clientDataError e clientError (ClientUserNotFound _) = StdError (errorToWai @'E.InvalidUser) clientError ClientLegalHoldCannotBeRemoved = StdError can'tDeleteLegalHoldClient clientError ClientLegalHoldCannotBeAdded = StdError can'tAddLegalHoldClient +clientError ClientLegalHoldIncompatible = StdError $ Wai.mkError status409 "mls-legal-hold-not-allowed" "A user who is under legal-hold may not participate in MLS conversations" clientError (ClientFederationError e) = fedError e clientError ClientCapabilitiesCannotBeRemoved = StdError clientCapabilitiesCannotBeRemoved clientError ClientMissingLegalholdConsentOldClients = StdError (errorToWai @'E.MissingLegalholdConsentOldClients) @@ -210,9 +200,6 @@ certEnrollmentError MissingName = StdError $ Wai.mkError status400 "missing-name fedError :: FederationError -> HttpError fedError = StdError . federationErrorToWai -propDataError :: PropertiesDataError -> HttpError -propDataError TooManyProperties = StdError tooManyProperties - clientDataError :: ClientDataError -> HttpError clientDataError TooManyClients = StdError (errorToWai @'E.TooManyClients) clientDataError (ClientReAuthError e) = reauthError e @@ -237,10 +224,6 @@ accountStatusError :: AccountStatusError -> HttpError accountStatusError InvalidAccountStatus = StdError invalidAccountStatus accountStatusError AccountNotFound = StdError (notFound "Account not found") -updateProfileError :: UpdateProfileError -> HttpError -updateProfileError DisplayNameManagedByScim = StdError (propertyManagedByScim "name") -updateProfileError ProfileNotFound = StdError (errorToWai @'E.UserNotFound) - verificationCodeThrottledError :: VerificationCodeThrottledError -> HttpError verificationCodeThrottledError (VerificationCodeThrottled t) = RichError @@ -250,30 +233,18 @@ verificationCodeThrottledError (VerificationCodeThrottled t) = -- WAI Errors ----------------------------------------------------------------- -tooManyProperties :: Wai.Error -tooManyProperties = Wai.mkError status403 "too-many-properties" "Too many properties" - -propertyKeyTooLarge :: Wai.Error -propertyKeyTooLarge = Wai.mkError status403 "property-key-too-large" "The property key is too large." - -propertyValueTooLarge :: Wai.Error -propertyValueTooLarge = Wai.mkError status403 "property-value-too-large" "The property value is too large" - clientCapabilitiesCannotBeRemoved :: Wai.Error clientCapabilitiesCannotBeRemoved = Wai.mkError status409 "client-capabilities-cannot-be-removed" "You can only add capabilities to a client, not remove them." +-- One of two cases: +-- (1) the email is in use by any other account or invitation; +-- (2) (when posting an invitation) the email is in use by a member of another team (and we can't steal away those, invitee has to be personal user). emailExists :: Wai.Error emailExists = Wai.mkError status409 "email-exists" "The given e-mail address is in use." -phoneExists :: Wai.Error -phoneExists = Wai.mkError status409 "phone-exists" "The given phone number is in use." - badRequest :: LText -> Wai.Error badRequest = Wai.mkError status400 "bad-request" -loginCodePending :: Wai.Error -loginCodePending = Wai.mkError status403 "pending-login" "A login code is still pending." - loginCodeNotFound :: Wai.Error loginCodeNotFound = Wai.mkError status404 "no-pending-login" "No login code was found." @@ -286,12 +257,6 @@ invalidAccountStatus = Wai.mkError status400 "invalid-status" "The specified acc activationKeyNotFound :: Wai.Error activationKeyNotFound = notFound "Activation key not found." -invalidActivationCode :: LText -> Wai.Error -invalidActivationCode = Wai.mkError status404 "invalid-code" - -activationCodeNotFound :: Wai.Error -activationCodeNotFound = invalidActivationCode "Activation key/code not found or invalid." - deletionCodePending :: Wai.Error deletionCodePending = Wai.mkError status403 "pending-delete" "A verification code for account deletion is still pending." @@ -306,38 +271,15 @@ blacklistedEmail = "The given e-mail address has been blacklisted due to a permanent bounce \ \or a complaint." -passwordExists :: Wai.Error -passwordExists = - Wai.mkError - status403 - "password-exists" - "The operation is not permitted because the user has a password set." - -phoneBudgetExhausted :: Wai.Error -phoneBudgetExhausted = - Wai.mkError - status403 - "phone-budget-exhausted" - "The SMS or voice call budget for the given phone number has been \ - \exhausted. Please try again later. Repeated exhaustion of the SMS or \ - \voice call budget is considered abuse of the API and may result in \ - \permanent blacklisting of the phone number." - authMissingCookie :: Wai.Error authMissingCookie = Wai.mkError status403 "invalid-credentials" "Missing cookie" -authInvalidCookie :: Wai.Error -authInvalidCookie = Wai.mkError status403 "invalid-credentials" "Invalid cookie" - authMissingToken :: Wai.Error authMissingToken = Wai.mkError status403 "invalid-credentials" "Missing token" authMissingCookieAndToken :: Wai.Error authMissingCookieAndToken = Wai.mkError status403 "invalid-credentials" "Missing cookie and token" -invalidAccessToken :: Wai.Error -invalidAccessToken = Wai.mkError status403 "invalid-credentials" "Invalid access token" - missingAccessToken :: Wai.Error missingAccessToken = Wai.mkError status403 "invalid-credentials" "Missing access token" diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index 58af99451bf..02e1c040317 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -31,14 +31,11 @@ import Brig.API.User qualified as API import Brig.App import Brig.Data.Connection qualified as Data import Brig.Data.User qualified as Data -import Brig.Effects.FederationConfigStore (FederationConfigStore) -import Brig.Effects.FederationConfigStore qualified as E import Brig.IO.Intra (notify) import Brig.Options import Brig.User.API.Handle import Brig.User.Search.SearchIndex qualified as Q import Control.Error.Util -import Control.Lens ((^.)) import Control.Monad.Trans.Except import Data.Domain import Data.Handle (Handle (..)) @@ -48,7 +45,6 @@ import Data.List.NonEmpty (nonEmpty) import Data.Qualified import Data.Range import Data.Set (fromList, (\\)) -import Gundeck.Types.Push qualified as Push import Imports hiding ((\\)) import Network.Wai.Utilities.Error ((!>>)) import Polysemy @@ -60,6 +56,7 @@ import Wire.API.Federation.API.Common import Wire.API.Federation.Endpoint import Wire.API.Federation.Version import Wire.API.MLS.KeyPackage +import Wire.API.Push.V2 qualified as Push import Wire.API.Routes.FederationDomainConfig as FD import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Named @@ -72,11 +69,14 @@ import Wire.API.UserEvent import Wire.API.UserMap (UserMap) import Wire.DeleteQueue import Wire.Error +import Wire.FederationConfigStore (FederationConfigStore) +import Wire.FederationConfigStore qualified as E import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.NotificationSubsystem import Wire.Sem.Concurrency import Wire.UserStore -import Wire.UserSubsystem +import Wire.UserSubsystem (UserSubsystem) +import Wire.UserSubsystem qualified as UserSubsystem type FederationAPI = "federation" :> BrigApi @@ -111,7 +111,7 @@ federationSitemap = getFederationStatus :: (Member FederationConfigStore r) => Domain -> DomainSet -> Handler r NonConnectedBackends getFederationStatus _ request = do cfg <- ask - case setFederationStrategy (cfg ^. settings) of + case cfg.settings.federationStrategy of Just AllowAll -> pure $ NonConnectedBackends mempty _ -> do fedDomains <- fromList . fmap (.domain) . (.remotes) <$> lift (liftSem $ E.getFederationConfigs) @@ -167,7 +167,7 @@ getUserByHandle domain handle = do pure Nothing Just ownerId -> do localOwnerId <- qualifyLocal ownerId - liftSem $ getLocalUserProfile localOwnerId + liftSem $ UserSubsystem.getLocalUserProfile localOwnerId getUsersByIds :: (Member UserSubsystem r) => @@ -176,7 +176,7 @@ getUsersByIds :: ExceptT HttpError (AppT r) [UserProfile] getUsersByIds _ uids = do luids <- qualifyLocal uids - lift $ liftSem $ getLocalUserProfiles luids + lift $ liftSem $ UserSubsystem.getLocalUserProfiles luids claimPrekey :: (Member DeleteQueue r) => Domain -> (UserId, ClientId) -> (Handler r) (Maybe ClientPrekey) claimPrekey _ (user, client) = do @@ -193,7 +193,13 @@ claimMultiPrekeyBundle :: Handler r UserClientPrekeyMap claimMultiPrekeyBundle _ uc = API.claimLocalMultiPrekeyBundles LegalholdPlusFederationNotImplemented uc !>> clientError -fedClaimKeyPackages :: Domain -> ClaimKeyPackageRequest -> Handler r (Maybe KeyPackageBundle) +fedClaimKeyPackages :: + ( Member GalleyAPIAccess r, + Member UserStore r + ) => + Domain -> + ClaimKeyPackageRequest -> + Handler r (Maybe KeyPackageBundle) fedClaimKeyPackages domain ckpr = isMLSEnabled >>= \case True -> do @@ -252,7 +258,7 @@ searchUsers domain (SearchRequest searchTerm mTeam mOnlyInTeams) = do mFoundUserTeamId <- lift $ wrapClient $ Data.lookupUserTeam foundUser localFoundUser <- qualifyLocal foundUser if isTeamAllowed mOnlyInTeams mFoundUserTeamId - then lift $ liftSem $ (fmap contactFromProfile . maybeToList) <$> getLocalUserProfile localFoundUser + then lift $ liftSem $ (fmap contactFromProfile . maybeToList) <$> UserSubsystem.getLocalUserProfile localFoundUser else pure [] | otherwise = pure [] @@ -281,7 +287,7 @@ onUserDeleted :: onUserDeleted origDomain udcn = lift $ do let deletedUser = toRemoteUnsafe origDomain udcn.user connections = udcn.connections - event = pure . UserEvent $ UserDeleted (tUntagged deletedUser) + event = UserEvent $ UserDeleted (tUntagged deletedUser) acceptedLocals <- map csv2From . filter (\x -> csv2Status x == Accepted) diff --git a/services/brig/src/Brig/API/Handler.hs b/services/brig/src/Brig/API/Handler.hs index 2971f28e4e9..c664cd3f0e0 100644 --- a/services/brig/src/Brig/API/Handler.hs +++ b/services/brig/src/Brig/API/Handler.hs @@ -21,7 +21,6 @@ module Brig.API.Handler toServantHandler, -- * Utilities - parseJsonBody, checkAllowlist, checkAllowlistWithError, isAllowlisted, @@ -34,23 +33,19 @@ import Brig.API.Error import Brig.AWS qualified as AWS import Brig.App import Brig.CanonicalInterpreter (BrigCanonicalEffects, runBrigToIO) -import Brig.Options (setAllowlistEmailDomains) +import Brig.Options (allowlistEmailDomains) import Control.Error import Control.Exception (throwIO) -import Control.Lens (view) import Control.Monad.Catch (catches, throwM) import Control.Monad.Catch qualified as Catch import Control.Monad.Except (MonadError, throwError) -import Data.Aeson (FromJSON) import Data.Aeson qualified as Aeson import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import Data.ZAuth.Validation qualified as ZV import Imports import Network.HTTP.Types (Status (statusCode, statusMessage)) -import Network.Wai.Utilities.Error ((!>>)) import Network.Wai.Utilities.Error qualified as WaiError -import Network.Wai.Utilities.Request (JsonRequest, parseBody) import Network.Wai.Utilities.Server qualified as Server import Servant qualified import System.Logger qualified as Log @@ -58,7 +53,7 @@ import System.Logger.Class (Logger) import Wire.API.Allowlists qualified as Allowlists import Wire.API.Error import Wire.API.Error.Brig -import Wire.API.User (Email) +import Wire.API.User import Wire.Error ------------------------------------------------------------------------------- @@ -68,8 +63,8 @@ type Handler r = ExceptT HttpError (AppT r) toServantHandler :: Env -> (Handler BrigCanonicalEffects) a -> Servant.Handler a toServantHandler env action = do - let logger = view applog env - reqId = unRequestId $ view requestId env + let logger = env.appLogger + reqId = unRequestId $ env.requestId a <- liftIO $ (runBrigToIO env (runExceptT action)) @@ -122,22 +117,16 @@ brigErrorHandlers logger reqId = ------------------------------------------------------------------------------- -- Utilities --- This could go to libs/wai-utilities. There is a `parseJson'` in --- "Network.Wai.Utilities.Request", but adding `parseJsonBody` there would require to move --- more code out of brig. -parseJsonBody :: (FromJSON a, MonadIO m) => JsonRequest a -> ExceptT HttpError m a -parseJsonBody req = parseBody req !>> StdError . badRequest - -- | If an Allowlist is configured, consult it, otherwise a no-op. {#RefActivationAllowlist} -checkAllowlist :: Email -> Handler r () +checkAllowlist :: EmailAddress -> Handler r () checkAllowlist = wrapHttpClientE . checkAllowlistWithError (StdError allowlistError) -checkAllowlistWithError :: (MonadReader Env m, MonadError e m) => e -> Email -> m () +checkAllowlistWithError :: (MonadReader Env m, MonadError e m) => e -> EmailAddress -> m () checkAllowlistWithError e key = do ok <- isAllowlisted key unless ok (throwError e) -isAllowlisted :: (MonadReader Env m) => Email -> m Bool +isAllowlisted :: (MonadReader Env m) => EmailAddress -> m Bool isAllowlisted key = do - env <- view settings - pure $ Allowlists.verify (setAllowlistEmailDomains env) key + env <- asks (.settings) + pure $ Allowlists.verify env.allowlistEmailDomains key diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 1e05d13e6cf..ea9c2f26f20 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -31,53 +31,42 @@ import Brig.API.MLS.KeyPackages.Validation import Brig.API.OAuth (internalOauthAPI) import Brig.API.Types import Brig.API.User qualified as API -import Brig.App +import Brig.App as App import Brig.Data.Activation import Brig.Data.Client qualified as Data import Brig.Data.Connection qualified as Data import Brig.Data.MLS.KeyPackage qualified as Data import Brig.Data.User qualified as Data -import Brig.Effects.BlacklistStore (BlacklistStore) -import Brig.Effects.ConnectionStore (ConnectionStore) -import Brig.Effects.FederationConfigStore - ( AddFederationRemoteResult (..), - AddFederationRemoteTeamResult (..), - FederationConfigStore, - UpdateFederationResult (..), - ) -import Brig.Effects.FederationConfigStore qualified as E import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) -import Brig.IO.Intra qualified as Intra import Brig.Options hiding (internalEvents) import Brig.Provider.API qualified as Provider import Brig.Team.API qualified as Team -import Brig.Team.DB (lookupInvitationByEmail) import Brig.Types.Connection import Brig.Types.Intra import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) import Brig.Types.User -import Brig.User.API.Search qualified as Search import Brig.User.EJPD qualified -import Brig.User.Search.Index qualified as Index +import Brig.User.Search.Index qualified as Search import Control.Error hiding (bool) -import Control.Lens (view) +import Control.Lens (preview, to, _Just) import Data.ByteString.Conversion (toByteString) import Data.Code qualified as Code import Data.CommaSeparatedList import Data.Default import Data.Domain (Domain) import Data.Handle +import Data.HavePendingInvitations import Data.Id as Id import Data.Map.Strict qualified as Map import Data.Qualified import Data.Set qualified as Set import Data.Text qualified as T -import Data.Time.Clock (UTCTime) import Data.Time.Clock.System import Imports hiding (head) import Network.Wai.Utilities as Utilities import Polysemy -import Polysemy.Input (Input) +import Polysemy.Error qualified as Polysemy +import Polysemy.Input (Input, input) import Polysemy.TinyLog (TinyLog) import Servant hiding (Handler, JSON, addHeader, respond) import Servant.OpenApi.Internal.Orphans () @@ -86,58 +75,81 @@ import UnliftIO.Async (pooledMapConcurrentlyN) import Wire.API.Connection import Wire.API.Error import Wire.API.Error.Brig qualified as E -import Wire.API.Federation.API import Wire.API.Federation.Error (FederationError (..)) import Wire.API.MLS.CipherSuite import Wire.API.Routes.FederationDomainConfig import Wire.API.Routes.Internal.Brig qualified as BrigIRoutes import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Named -import Wire.API.Team.Feature qualified as ApiFt +import Wire.API.Team.Export +import Wire.API.Team.Feature import Wire.API.User import Wire.API.User.Activation import Wire.API.User.Client import Wire.API.User.RichInfo import Wire.API.UserEvent +import Wire.ActivationCodeStore (ActivationCodeStore) import Wire.AuthenticationSubsystem (AuthenticationSubsystem) -import Wire.DeleteQueue -import Wire.EmailSending (EmailSending) +import Wire.BlockListStore (BlockListStore) +import Wire.DeleteQueue (DeleteQueue) import Wire.EmailSubsystem (EmailSubsystem) -import Wire.GalleyAPIAccess (GalleyAPIAccess, ShowOrHideInvitationUrl (..)) +import Wire.Events (Events) +import Wire.Events qualified as Events +import Wire.FederationConfigStore + ( AddFederationRemoteResult (..), + AddFederationRemoteTeamResult (..), + FederationConfigStore, + UpdateFederationResult (..), + ) +import Wire.FederationConfigStore qualified as E +import Wire.GalleyAPIAccess (GalleyAPIAccess) +import Wire.HashPassword (HashPassword) +import Wire.IndexedUserStore (IndexedUserStore, getTeamSize) +import Wire.InvitationStore import Wire.NotificationSubsystem +import Wire.PasswordResetCodeStore (PasswordResetCodeStore) +import Wire.PropertySubsystem import Wire.Rpc import Wire.Sem.Concurrency -import Wire.Sem.Paging.Cassandra (InternalPaging) +import Wire.TeamInvitationSubsystem import Wire.UserKeyStore -import Wire.UserStore +import Wire.UserStore as UserStore import Wire.UserSubsystem import Wire.UserSubsystem qualified as UserSubsystem +import Wire.UserSubsystem.Error import Wire.VerificationCode import Wire.VerificationCodeGen import Wire.VerificationCodeSubsystem servantSitemap :: forall r p. - ( Member BlacklistStore r, + ( Member BlockListStore r, Member DeleteQueue r, Member (Concurrency 'Unsafe) r, - Member (ConnectionStore InternalPaging) r, Member (Embed HttpClientIO) r, Member FederationConfigStore r, Member AuthenticationSubsystem r, Member GalleyAPIAccess r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, Member NotificationSubsystem r, Member UserSubsystem r, + Member TeamInvitationSubsystem r, Member UserStore r, + Member InvitationStore r, Member UserKeyStore r, Member Rpc r, Member TinyLog r, Member (UserPendingActivationStore p) r, - Member EmailSending r, Member EmailSubsystem r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member Events r, + Member PasswordResetCodeStore r, + Member PropertySubsystem r, + Member (Input (Local ())) r, + Member IndexedUserStore r, + Member (Polysemy.Error UserSubsystemError) r, + Member HashPassword r, + Member (Embed IO) r, + Member ActivationCodeStore r ) => ServerT BrigIRoutes.API (Handler r) servantSitemap = @@ -145,7 +157,7 @@ servantSitemap = :<|> ejpdAPI :<|> accountAPI :<|> mlsAPI - :<|> getVerificationCode + :<|> Named @"get-verification-code" getVerificationCode :<|> teamsAPI :<|> userAPI :<|> clientAPI @@ -165,14 +177,13 @@ ejpdAPI :: Member Rpc r ) => ServerT BrigIRoutes.EJPDRequest (Handler r) -ejpdAPI = - Brig.User.EJPD.ejpdRequest +ejpdAPI = Named @"ejpd-request" Brig.User.EJPD.ejpdRequest mlsAPI :: ServerT BrigIRoutes.MLSAPI (Handler r) -mlsAPI = getMLSClients +mlsAPI = Named @"get-mls-clients" getMLSClients accountAPI :: - ( Member BlacklistStore r, + ( Member BlockListStore r, Member GalleyAPIAccess r, Member AuthenticationSubsystem r, Member DeleteQueue r, @@ -181,23 +192,28 @@ accountAPI :: Member NotificationSubsystem r, Member UserSubsystem r, Member UserKeyStore r, + Member (Input (Local ())) r, Member UserStore r, Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member PropertySubsystem r, + Member Events r, + Member PasswordResetCodeStore r, + Member HashPassword r, + Member InvitationStore r, + Member (Embed IO) r, + Member ActivationCodeStore r ) => ServerT BrigIRoutes.AccountAPI (Handler r) accountAPI = Named @"get-account-conference-calling-config" getAccountConferenceCallingConfig - :<|> putAccountConferenceCallingConfig - :<|> deleteAccountConferenceCallingConfig - :<|> getConnectionsStatusUnqualified - :<|> getConnectionsStatus - :<|> Named @"createUserNoVerify" (callsFed (exposeAnnotations createUserNoVerify)) - :<|> Named @"createUserNoVerifySpar" (callsFed (exposeAnnotations createUserNoVerifySpar)) + :<|> Named @"i-put-account-conference-calling-config" putAccountConferenceCallingConfig + :<|> Named @"i-delete-account-conference-calling-config" deleteAccountConferenceCallingConfig + :<|> Named @"i-get-all-connections-unqualified" getConnectionsStatusUnqualified + :<|> Named @"i-get-all-connections" getConnectionsStatus + :<|> Named @"createUserNoVerify" createUserNoVerify + :<|> Named @"createUserNoVerifySpar" createUserNoVerifySpar :<|> Named @"putSelfEmail" changeSelfEmailMaybeSendH :<|> Named @"iDeleteUser" deleteUserNoAuthH :<|> Named @"iPutUserStatus" changeAccountStatusH @@ -229,32 +245,35 @@ accountAPI = teamsAPI :: ( Member GalleyAPIAccess r, Member (UserPendingActivationStore p) r, - Member BlacklistStore r, + Member BlockListStore r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member UserKeyStore r, Member (Concurrency 'Unsafe) r, Member TinyLog r, + Member InvitationStore r, + Member TeamInvitationSubsystem r, + Member UserSubsystem r, + Member (Polysemy.Error UserSubsystemError) r, + Member Events r, Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, - Member EmailSending r + Member IndexedUserStore r ) => ServerT BrigIRoutes.TeamsAPI (Handler r) teamsAPI = - Named @"updateSearchVisibilityInbound" Index.updateSearchVisibilityInbound + Named @"updateSearchVisibilityInbound" (lift . liftSem . updateTeamSearchVisibilityInbound) :<|> Named @"get-invitation-by-email" Team.getInvitationByEmail - :<|> Named @"get-invitation-code" Team.getInvitationCode + :<|> Named @"get-invitation-code" (\tid iid -> lift . liftSem $ Team.getInvitationCode tid iid) :<|> Named @"suspend-team" Team.suspendTeam :<|> Named @"unsuspend-team" Team.unsuspendTeam - :<|> Named @"team-size" Team.teamSize + :<|> Named @"team-size" (lift . liftSem . getTeamSize) :<|> Named @"create-invitations-via-scim" Team.createInvitationViaScim userAPI :: (Member UserSubsystem r) => ServerT BrigIRoutes.UserAPI (Handler r) userAPI = - updateLocale - :<|> deleteLocale - :<|> getDefaultUserLocale + Named @"i-update-user-locale" updateLocale + :<|> Named @"i-delete-user-locale" deleteLocale + :<|> Named @"i-get-default-locale" getDefaultUserLocale + :<|> Named @"get-user-export-data" getUserExportDataH clientAPI :: ServerT BrigIRoutes.ClientAPI (Handler r) clientAPI = Named @"update-client-last-active" updateClientLastActive @@ -262,21 +281,28 @@ clientAPI = Named @"update-client-last-active" updateClientLastActive authAPI :: ( Member GalleyAPIAccess r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, - Member VerificationCodeSubsystem r + Member Events r, + Member UserSubsystem r, + Member VerificationCodeSubsystem r, + Member AuthenticationSubsystem r ) => ServerT BrigIRoutes.AuthAPI (Handler r) authAPI = - Named @"legalhold-login" (callsFed (exposeAnnotations legalHoldLogin)) - :<|> Named @"sso-login" (callsFed (exposeAnnotations ssoLogin)) + Named @"legalhold-login" legalHoldLogin + :<|> Named @"sso-login" ssoLogin :<|> Named @"login-code" getLoginCode - :<|> Named @"reauthenticate" reauthenticate - -federationRemotesAPI :: (Member FederationConfigStore r) => ServerT BrigIRoutes.FederationRemotesAPI (Handler r) + :<|> Named @"reauthenticate" + ( \uid reauth -> + -- changing this end-point would involve providing a `Local` type from a user id that is + -- captured from the path, not pulled from the http header. this is certainly feasible, + -- but running qualifyLocal here is easier. + qualifyLocal uid >>= \luid -> reauthenticate luid reauth + ) + +federationRemotesAPI :: + ( Member FederationConfigStore r + ) => + ServerT BrigIRoutes.FederationRemotesAPI (Handler r) federationRemotesAPI = Named @"add-federation-remotes" addFederationRemote :<|> Named @"get-federation-remotes" getFederationRemotes @@ -293,7 +319,12 @@ getFederationRemoteTeams :: (Member FederationConfigStore r) => Domain -> (Handl getFederationRemoteTeams domain = lift $ liftSem $ E.getFederationRemoteTeams domain -addFederationRemoteTeam :: (Member FederationConfigStore r) => Domain -> FederationRemoteTeam -> (Handler r) () +addFederationRemoteTeam :: + ( Member FederationConfigStore r + ) => + Domain -> + FederationRemoteTeam -> + (Handler r) () addFederationRemoteTeam domain rt = lift (liftSem $ E.addFederationRemoteTeam domain rt.teamId) >>= \case AddFederationRemoteTeamSuccess -> pure () @@ -308,7 +339,11 @@ addFederationRemoteTeam domain rt = getFederationRemotes :: (Member FederationConfigStore r) => (Handler r) FederationDomainConfigs getFederationRemotes = lift $ liftSem $ E.getFederationConfigs -addFederationRemote :: (Member FederationConfigStore r) => FederationDomainConfig -> (Handler r) () +addFederationRemote :: + ( Member FederationConfigStore r + ) => + FederationDomainConfig -> + (Handler r) () addFederationRemote fedDomConf = do lift (liftSem $ E.addFederationConfig fedDomConf) >>= \case AddFederationRemoteSuccess -> pure () @@ -345,17 +380,17 @@ updateFederationRemote dom fedcfg = do "keeping track of remote domains in the brig config file is deprecated, but as long as we \ \do that, removing or updating items listed in the config file is not allowed." --- | Responds with 'Nothing' if field is NULL in existing user or user does not exist. -getAccountConferenceCallingConfig :: UserId -> (Handler r) (ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig) -getAccountConferenceCallingConfig uid = - lift (wrapClient $ Data.lookupFeatureConferenceCalling uid) - >>= maybe (ApiFt.forgetLock <$> view (settings . getAfcConferenceCallingDefNull)) pure +getAccountConferenceCallingConfig :: UserId -> Handler r (Feature ConferenceCallingConfig) +getAccountConferenceCallingConfig uid = do + mStatus <- lift $ wrapClient $ Data.lookupFeatureConferenceCalling uid + mDefStatus <- preview (App.settingsLens . featureFlagsLens . _Just . to conferenceCalling . to forNull) + pure $ def {status = mStatus <|> mDefStatus ?: (def :: LockableFeature ConferenceCallingConfig).status} -putAccountConferenceCallingConfig :: UserId -> ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig -> (Handler r) NoContent -putAccountConferenceCallingConfig uid status = - lift $ wrapClient $ Data.updateFeatureConferenceCalling uid (Just status) $> NoContent +putAccountConferenceCallingConfig :: UserId -> Feature ConferenceCallingConfig -> Handler r NoContent +putAccountConferenceCallingConfig uid feat = do + lift $ wrapClient $ Data.updateFeatureConferenceCalling uid (Just feat.status) $> NoContent -deleteAccountConferenceCallingConfig :: UserId -> (Handler r) NoContent +deleteAccountConferenceCallingConfig :: UserId -> Handler r NoContent deleteAccountConferenceCallingConfig uid = lift $ wrapClient $ Data.updateFeatureConferenceCalling uid Nothing $> NoContent @@ -387,8 +422,6 @@ getVerificationCode uid action = runMaybeT do internalSearchIndexAPI :: forall r. ServerT BrigIRoutes.ISearchIndexAPI (Handler r) internalSearchIndexAPI = Named @"indexRefresh" (NoContent <$ lift (wrapClient Search.refreshIndex)) - :<|> Named @"indexReindex" (NoContent <$ lift (wrapClient Search.reindexAll)) - :<|> Named @"indexReindexIfSameOrNewer" (NoContent <$ lift (wrapClient Search.reindexAllIfSameOrNewer)) --------------------------------------------------------------------------- -- Handlers @@ -396,15 +429,13 @@ internalSearchIndexAPI = -- | Add a client without authentication checks addClientInternalH :: ( Member GalleyAPIAccess r, - Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member DeleteQueue r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, - Member VerificationCodeSubsystem r + Member Events r, + Member UserSubsystem r, + Member VerificationCodeSubsystem r, + Member AuthenticationSubsystem r ) => UserId -> Maybe Bool -> @@ -415,33 +446,14 @@ addClientInternalH usr mSkipReAuth new connId = do let policy | mSkipReAuth == Just True = \_ _ -> False | otherwise = Data.reAuthForNewClients - API.addClientWithReAuthPolicy policy usr connId new !>> clientError + lusr <- qualifyLocal usr + API.addClientWithReAuthPolicy policy lusr connId new !>> clientError -legalHoldClientRequestedH :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r - ) => - UserId -> - LegalHoldClientRequest -> - (Handler r) NoContent +legalHoldClientRequestedH :: (Member Events r) => UserId -> LegalHoldClientRequest -> (Handler r) NoContent legalHoldClientRequestedH targetUser clientRequest = do lift $ NoContent <$ API.legalHoldClientRequested targetUser clientRequest -removeLegalHoldClientH :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member DeleteQueue r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r - ) => - UserId -> - (Handler r) NoContent +removeLegalHoldClientH :: (Member DeleteQueue r, Member Events r) => UserId -> (Handler r) NoContent removeLegalHoldClientH uid = do lift $ NoContent <$ API.removeLegalHoldClient uid @@ -455,40 +467,38 @@ internalListFullClientsH (UserSet usrs) = lift $ do UserClientsFull <$> wrapClient (Data.lookupClientsBulk (Set.toList usrs)) createUserNoVerify :: - ( Member BlacklistStore r, + ( Member BlockListStore r, Member GalleyAPIAccess r, Member (UserPendingActivationStore p) r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, + Member Events r, + Member InvitationStore r, Member UserKeyStore r, + Member UserSubsystem r, Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member HashPassword r, + Member PasswordResetCodeStore r ) => NewUser -> (Handler r) (Either RegisterError SelfProfile) createUserNoVerify uData = lift . runExceptT $ do result <- API.createUser uData let acc = createdAccount result - let usr = accountUser acc - let uid = userId usr + let uid = userId acc let eac = createdEmailActivation result for_ eac $ \adata -> let key = ActivateKey $ activationKey adata code = activationCode adata in API.activate key code (Just uid) !>> activationErrorToRegisterError - pure . SelfProfile $ usr + pure . SelfProfile $ acc createUserNoVerifySpar :: ( Member GalleyAPIAccess r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member UserSubsystem r, Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r, + Member HashPassword r, + Member PasswordResetCodeStore r ) => NewUserSpar -> (Handler r) (Either CreateUserSparError SelfProfile) @@ -496,14 +506,13 @@ createUserNoVerifySpar uData = lift . runExceptT $ do result <- API.createUserSpar uData let acc = createdAccount result - let usr = accountUser acc - let uid = userId usr + let uid = userId acc let eac = createdEmailActivation result for_ eac $ \adata -> let key = ActivateKey $ activationKey adata code = activationCode adata in API.activate key code (Just uid) !>> CreateUserSparRegistrationError . activationErrorToRegisterError - pure . SelfProfile $ usr + pure . SelfProfile $ acc deleteUserNoAuthH :: ( Member (Embed HttpClientIO) r, @@ -511,27 +520,47 @@ deleteUserNoAuthH :: Member UserStore r, Member TinyLog r, Member UserKeyStore r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member Events r, + Member UserSubsystem r, + Member PropertySubsystem r ) => UserId -> (Handler r) DeleteUserResponse deleteUserNoAuthH uid = do - r <- lift $ API.ensureAccountDeleted uid + luid <- qualifyLocal uid + r <- lift $ API.ensureAccountDeleted luid case r of NoUser -> throwStd (errorToWai @'E.UserNotFound) AccountAlreadyDeleted -> pure UserResponseAccountAlreadyDeleted AccountDeleted -> pure UserResponseAccountDeleted -changeSelfEmailMaybeSendH :: (Member BlacklistStore r, Member UserKeyStore r, Member EmailSubsystem r) => UserId -> EmailUpdate -> Maybe Bool -> (Handler r) ChangeEmailResponse +changeSelfEmailMaybeSendH :: + ( Member BlockListStore r, + Member UserKeyStore r, + Member EmailSubsystem r, + Member UserSubsystem r + ) => + UserId -> + EmailUpdate -> + Maybe Bool -> + (Handler r) ChangeEmailResponse changeSelfEmailMaybeSendH u body (fromMaybe False -> validate) = do let email = euEmail body changeSelfEmailMaybeSend u (if validate then ActuallySendEmail else DoNotSendEmail) email UpdateOriginScim data MaybeSendEmail = ActuallySendEmail | DoNotSendEmail -changeSelfEmailMaybeSend :: (Member BlacklistStore r, Member UserKeyStore r, Member EmailSubsystem r) => UserId -> MaybeSendEmail -> Email -> UpdateOriginType -> (Handler r) ChangeEmailResponse +changeSelfEmailMaybeSend :: + ( Member BlockListStore r, + Member UserKeyStore r, + Member EmailSubsystem r, + Member UserSubsystem r + ) => + UserId -> + MaybeSendEmail -> + EmailAddress -> + UpdateOriginType -> + (Handler r) ChangeEmailResponse changeSelfEmailMaybeSend u ActuallySendEmail email allowScim = do API.changeSelfEmail u email allowScim changeSelfEmailMaybeSend u DoNotSendEmail email allowScim = do @@ -544,92 +573,63 @@ changeSelfEmailMaybeSend u DoNotSendEmail email allowScim = do -- handler allows up to 4 lists of various user keys, and returns the union of the lookups. -- Empty list is forbidden for backwards compatibility. listActivatedAccountsH :: - ( Member DeleteQueue r, - Member UserKeyStore r, - Member UserStore r + ( Member (Input (Local ())) r, + Member UserSubsystem r ) => Maybe (CommaSeparatedList UserId) -> Maybe (CommaSeparatedList Handle) -> - Maybe (CommaSeparatedList Email) -> + Maybe (CommaSeparatedList EmailAddress) -> Maybe Bool -> - Handler r [UserAccount] + Handler r [User] listActivatedAccountsH (maybe [] fromCommaSeparatedList -> uids) (maybe [] fromCommaSeparatedList -> handles) (maybe [] fromCommaSeparatedList -> emails) - (fromMaybe False -> includePendingInvitations) = do + (maybe NoPendingInvitations fromBool -> include) = do when (length uids + length handles + length emails == 0) $ do throwStd (notFound "no user keys") - lift $ do - u1 <- listActivatedAccounts (Left uids) includePendingInvitations - u2 <- listActivatedAccounts (Right handles) includePendingInvitations - u3 <- (\email -> API.lookupAccountsByIdentity email includePendingInvitations) `mapM` emails - pure $ u1 <> u2 <> join u3 - --- FUTUREWORK: this should use UserStore only through UserSubsystem. -listActivatedAccounts :: - (Member DeleteQueue r, Member UserStore r) => - Either [UserId] [Handle] -> - Bool -> - AppT r [UserAccount] -listActivatedAccounts elh includePendingInvitations = do - Log.debug (Log.msg $ "listActivatedAccounts: " <> show (elh, includePendingInvitations)) - case elh of - Left us -> byIds us - Right hs -> do - us <- liftSem $ mapM API.lookupHandle hs - byIds (catMaybes us) - where - byIds :: (Member DeleteQueue r) => [UserId] -> (AppT r) [UserAccount] - byIds uids = wrapClient (API.lookupAccounts uids) >>= filterM accountValid - - accountValid :: (Member DeleteQueue r) => UserAccount -> (AppT r) Bool - accountValid account = case userIdentity . accountUser $ account of - Nothing -> pure False - Just ident -> - case (accountStatus account, includePendingInvitations, emailIdentity ident) of - (PendingInvitation, False, _) -> pure False - (PendingInvitation, True, Just email) -> do - hasInvitation <- isJust <$> wrapClient (lookupInvitationByEmail HideInvitationUrl email) - unless hasInvitation $ do - -- user invited via scim should expire together with its invitation - liftSem $ API.deleteUserNoVerify (userId . accountUser $ account) - pure hasInvitation - (PendingInvitation, True, Nothing) -> - pure True -- cannot happen, user invited via scim always has an email - (Active, _, _) -> pure True - (Suspended, _, _) -> pure True - (Deleted, _, _) -> pure True - (Ephemeral, _, _) -> pure True - -getActivationCode :: Email -> Handler r GetActivationCodeResp + lift $ liftSem do + loc <- input + byEmails <- getAccountsByEmailNoFilter $ loc $> emails + others <- + getAccountsBy $ + loc + $> def + { includePendingInvitations = include, + getByUserId = uids, + getByHandle = handles + } + pure $ others <> byEmails + +getActivationCode :: + ( Member ActivationCodeStore r, + Member (Embed IO) r + ) => + EmailAddress -> + Handler r GetActivationCodeResp getActivationCode email = do - apair <- lift . wrapClient $ API.lookupActivationCode email + apair <- lift . liftSem $ API.lookupActivationCode email maybe (throwStd activationKeyNotFound) (pure . GetActivationCodeResp) apair getPasswordResetCodeH :: ( Member AuthenticationSubsystem r ) => - Email -> + EmailAddress -> Handler r GetPasswordResetCodeResp getPasswordResetCodeH email = getPasswordResetCode email getPasswordResetCode :: ( Member AuthenticationSubsystem r ) => - Email -> + EmailAddress -> Handler r GetPasswordResetCodeResp getPasswordResetCode email = (GetPasswordResetCodeResp <$$> lift (API.lookupPasswordResetCode email)) >>= maybe (throwStd (errorToWai @'E.InvalidPasswordResetKey)) pure changeAccountStatusH :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + ( Member UserSubsystem r, + Member Events r ) => UserId -> AccountStatusUpdate -> @@ -675,7 +675,7 @@ revokeIdentityH :: ( Member UserSubsystem r, Member UserKeyStore r ) => - Email -> + EmailAddress -> Handler r NoContent revokeIdentityH email = lift $ NoContent <$ API.revokeIdentity email @@ -691,49 +691,44 @@ updateConnectionInternalH updateConn = do API.updateConnectionInternal updateConn !>> connError pure NoContent -checkBlacklist :: (Member BlacklistStore r) => Email -> Handler r CheckBlacklistResponse +checkBlacklist :: (Member BlockListStore r) => EmailAddress -> Handler r CheckBlacklistResponse checkBlacklist email = lift $ bool NotBlacklisted YesBlacklisted <$> API.isBlacklisted email -deleteFromBlacklist :: (Member BlacklistStore r) => Email -> Handler r NoContent +deleteFromBlacklist :: (Member BlockListStore r) => EmailAddress -> Handler r NoContent deleteFromBlacklist email = lift $ NoContent <$ API.blacklistDelete email -addBlacklist :: (Member BlacklistStore r) => Email -> Handler r NoContent +addBlacklist :: (Member BlockListStore r) => EmailAddress -> Handler r NoContent addBlacklist email = lift $ NoContent <$ API.blacklistInsert email updateSSOIdH :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + ( Member UserSubsystem r, + Member Events r ) => UserId -> UserSSOId -> (Handler r) UpdateSSOIdResponse -updateSSOIdH uid ssoid = do - success <- lift $ wrapClient $ Data.updateSSOId uid (Just ssoid) - if success - then do - lift $ liftSem $ Intra.onUserEvent uid Nothing (UserUpdated ((emptyUserUpdatedData uid) {eupSSOId = Just ssoid})) - pure UpdateSSOIdSuccess - else pure UpdateSSOIdNotFound +updateSSOIdH uid ssoid = lift $ do + success <- wrapClient $ Data.updateSSOId uid (Just ssoid) + liftSem $ + if success + then do + UserSubsystem.internalUpdateSearchIndex uid + Events.generateUserEvent uid Nothing (UserUpdated ((emptyUserUpdatedData uid) {eupSSOId = Just ssoid})) + pure UpdateSSOIdSuccess + else pure UpdateSSOIdNotFound deleteSSOIdH :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + ( Member UserSubsystem r, + Member Events r ) => UserId -> (Handler r) UpdateSSOIdResponse -deleteSSOIdH uid = do - success <- lift $ wrapClient $ Data.updateSSOId uid Nothing +deleteSSOIdH uid = lift $ do + success <- wrapClient $ Data.updateSSOId uid Nothing if success - then do - lift $ liftSem $ Intra.onUserEvent uid Nothing (UserUpdated ((emptyUserUpdatedData uid) {eupSSOIdRemoved = True})) + then liftSem $ do + UserSubsystem.internalUpdateSearchIndex uid + Events.generateUserEvent uid Nothing (UserUpdated ((emptyUserUpdatedData uid) {eupSSOIdRemoved = True})) pure UpdateSSOIdSuccess else pure UpdateSSOIdNotFound @@ -745,7 +740,7 @@ updateRichInfoH :: UserId -> RichInfoUpdate -> (Handler r) NoContent updateRichInfoH uid rup = NoContent <$ do let (unRichInfoAssocList -> richInfo) = normalizeRichInfoAssocList . riuRichInfo $ rup - maxSize <- setRichInfoLimit <$> view settings + maxSize <- asks (.settings.richInfoLimit) when (richInfoSize (RichInfo (mkRichInfoAssocList richInfo)) > maxSize) $ throwStd tooLargeRichInfo -- FUTUREWORK: send an event -- Intra.onUserEvent uid (Just conn) (richInfoUpdate uid ri) @@ -759,14 +754,14 @@ updateLocale uid upd@(LocaleUpdate locale) = do deleteLocale :: (Member UserSubsystem r) => UserId -> (Handler r) NoContent deleteLocale uid = do - defLoc <- setDefaultUserLocale <$> view settings + defLoc <- defaultUserLocale <$> asks (.settings) qUid <- qualifyLocal uid lift . liftSem $ updateUserProfile qUid Nothing UpdateOriginScim def {locale = Just defLoc} pure NoContent getDefaultUserLocale :: (Handler r) LocaleUpdate getDefaultUserLocale = do - defLocale <- setDefaultUserLocale <$> view settings + defLocale <- defaultUserLocale <$> asks (.settings) pure $ LocaleUpdate defLocale updateClientLastActive :: UserId -> ClientId -> Handler r () @@ -782,12 +777,14 @@ updateClientLastActive u c = do } lift . wrapClient $ Data.updateClientLastActive u c now -getRichInfoH :: UserId -> (Handler r) RichInfo -getRichInfoH uid = RichInfo . fromMaybe mempty <$> lift (wrapClient $ API.lookupRichInfo uid) +getRichInfoH :: (Member UserStore r) => UserId -> Handler r RichInfo +getRichInfoH uid = + RichInfo . fromMaybe mempty + <$> lift (liftSem $ UserStore.getRichInfo uid) -getRichInfoMultiH :: Maybe (CommaSeparatedList UserId) -> (Handler r) [(UserId, RichInfo)] +getRichInfoMultiH :: Maybe (CommaSeparatedList UserId) -> Handler r BrigIRoutes.GetRichInfoMultiResponse getRichInfoMultiH (maybe [] fromCommaSeparatedList -> uids) = - lift $ wrapClient $ API.lookupRichInfoMultiUsers uids + lift $ wrapClient $ BrigIRoutes.GetRichInfoMultiResponse <$> API.lookupRichInfoMultiUsers uids updateHandleH :: (Member UserSubsystem r) => @@ -820,3 +817,9 @@ checkHandleInternalH h = lift $ liftSem do getContactListH :: UserId -> (Handler r) UserIds getContactListH uid = lift . wrapClient $ UserIds <$> API.lookupContactList uid + +getUserExportDataH :: + (Member UserSubsystem r) => + UserId -> + Handler r (Maybe TeamExportUser) +getUserExportDataH = lift . liftSem . getUserExportData diff --git a/services/brig/src/Brig/API/MLS/CipherSuite.hs b/services/brig/src/Brig/API/MLS/CipherSuite.hs index da8182c0a41..c47bd0fedaa 100644 --- a/services/brig/src/Brig/API/MLS/CipherSuite.hs +++ b/services/brig/src/Brig/API/MLS/CipherSuite.hs @@ -15,12 +15,15 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Brig.API.MLS.CipherSuite (getCipherSuite, getCipherSuites) where +module Brig.API.MLS.CipherSuite (getCipherSuite, validateCipherSuites) where import Brig.API.Handler import Brig.API.MLS.KeyPackages.Validation +import Data.Set qualified as Set import Imports import Wire.API.MLS.CipherSuite +import Wire.API.MLS.KeyPackage +import Wire.API.MLS.Serialisation getOneCipherSuite :: CipherSuite -> Handler r CipherSuiteTag getOneCipherSuite s = @@ -32,5 +35,15 @@ getOneCipherSuite s = getCipherSuite :: Maybe CipherSuite -> Handler r CipherSuiteTag getCipherSuite = maybe (pure defCipherSuite) getOneCipherSuite -getCipherSuites :: Maybe [CipherSuite] -> Handler r [CipherSuiteTag] -getCipherSuites = maybe (pure [defCipherSuite]) (traverse getOneCipherSuite) +validateCipherSuites :: + Maybe [CipherSuite] -> + KeyPackageUpload -> + Handler r (Set CipherSuiteTag) +validateCipherSuites suites upload = do + suitesQuery <- Set.fromList <$> maybe (pure [defCipherSuite]) (traverse getOneCipherSuite) suites + when (any isNothing suitesKPM) . void $ mlsProtocolError "uploaded key packages contains unsupported cipher suite" + unless (suitesQuery == suitesKP) . void $ mlsProtocolError "uploaded key packages for unannounced cipher suites" + pure suitesQuery + where + suitesKPM = map (cipherSuiteTag . (.cipherSuite) . value) upload.keyPackages + suitesKP = Set.fromList $ catMaybes suitesKPM diff --git a/services/brig/src/Brig/API/MLS/KeyPackages.hs b/services/brig/src/Brig/API/MLS/KeyPackages.hs index 9226a4db7be..f0e96bc1576 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages.hs @@ -40,9 +40,11 @@ import Control.Monad.Trans.Except import Control.Monad.Trans.Maybe import Data.CommaSeparatedList import Data.Id +import Data.LegalHold import Data.Qualified import Data.Set qualified as Set import Imports +import Polysemy (Member) import Wire.API.Federation.API import Wire.API.Federation.API.Brig import Wire.API.MLS.CipherSuite @@ -51,6 +53,9 @@ import Wire.API.MLS.KeyPackage import Wire.API.MLS.Serialisation import Wire.API.Team.LegalHold import Wire.API.User.Client +import Wire.GalleyAPIAccess (GalleyAPIAccess, getUserLegalholdStatus) +import Wire.StoredUser +import Wire.UserStore (UserStore, getUser) uploadKeyPackages :: Local UserId -> ClientId -> KeyPackageUpload -> Handler r () uploadKeyPackages lusr cid kps = do @@ -60,6 +65,9 @@ uploadKeyPackages lusr cid kps = do lift . wrapClient $ Data.insertKeyPackages (tUnqualified lusr) cid kps' claimKeyPackages :: + ( Member GalleyAPIAccess r, + Member UserStore r + ) => Local UserId -> Maybe ClientId -> Qualified UserId -> @@ -67,6 +75,7 @@ claimKeyPackages :: Handler r KeyPackageBundle claimKeyPackages lusr mClient target mSuite = do assertMLSEnabled + suite <- getCipherSuite mSuite foldQualified lusr @@ -75,12 +84,24 @@ claimKeyPackages lusr mClient target mSuite = do target claimLocalKeyPackages :: + forall r. + ( Member GalleyAPIAccess r, + Member UserStore r + ) => Qualified UserId -> Maybe ClientId -> CipherSuiteTag -> Local UserId -> ExceptT ClientError (AppT r) KeyPackageBundle claimLocalKeyPackages qusr skipOwn suite target = do + -- 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 + -- skip own client when the target is the requesting user itself let own = guard (qusr == tUntagged target) *> skipOwn clients <- map clientId <$> wrapClientE (Data.lookupClients (tUnqualified target)) @@ -103,6 +124,24 @@ claimLocalKeyPackages qusr skipOwn suite target = do uncurry (KeyPackageBundleEntry (tUntagged target) c) <$> wrapClientM (Data.claimKeyPackage target c suite) + 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 () + claimRemoteKeyPackages :: Local UserId -> CipherSuite -> @@ -168,6 +207,6 @@ replaceKeyPackages :: Handler r () replaceKeyPackages lusr c (fmap toList -> mSuites) upload = do assertMLSEnabled - suites <- getCipherSuites mSuites + suites <- validateCipherSuites mSuites upload lift $ wrapClient (Data.deleteAllKeyPackages (tUnqualified lusr) c suites) uploadKeyPackages lusr c upload diff --git a/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs b/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs index 16707b15ff4..6a2d428748c 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs @@ -29,7 +29,6 @@ import Brig.App import Brig.Data.Client qualified as Data import Brig.Options import Control.Applicative -import Control.Lens import Data.ByteString qualified as LBS import Data.Qualified import Data.Time.Clock @@ -82,7 +81,7 @@ validateUploadedKeyPackage identity kp = do validateLifetime :: Lifetime -> Handler r () validateLifetime lt = do now <- liftIO getPOSIXTime - mMaxLifetime <- setKeyPackageMaximumLifetime <$> view settings + mMaxLifetime <- asks (.settings.keyPackageMaximumLifetime) either mlsProtocolError pure $ validateLifetime' now mMaxLifetime lt diff --git a/services/brig/src/Brig/API/MLS/Util.hs b/services/brig/src/Brig/API/MLS/Util.hs index e36f83babe1..b23308d55b8 100644 --- a/services/brig/src/Brig/API/MLS/Util.hs +++ b/services/brig/src/Brig/API/MLS/Util.hs @@ -23,11 +23,10 @@ import Brig.App import Brig.Data.Client import Brig.Options import Control.Error -import Control.Lens (view) import Imports isMLSEnabled :: Handler r Bool -isMLSEnabled = fromMaybe False . setEnableMLS <$> view settings +isMLSEnabled = fromMaybe False <$> asks (.settings.enableMLS) assertMLSEnabled :: Handler r () assertMLSEnabled = diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index 5ff461bf652..9176f14553e 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -29,14 +29,16 @@ import Brig.App import Brig.Options qualified as Opt import Cassandra hiding (Set) import Cassandra qualified as C -import Control.Error (assertMay, failWith, failWithM) -import Control.Lens (view, (?~), (^?)) -import Control.Monad.Except +import Control.Error +import Control.Lens ((?~), (^?)) import Crypto.JWT hiding (params, uri) import Data.ByteString.Conversion import Data.Domain import Data.Id +import Data.Json.Util (toUTCTimeMillis) +import Data.Map qualified as Map import Data.Misc +import Data.Qualified import Data.Set qualified as Set import Data.Text.Ascii import Data.Text.Encoding qualified as T @@ -46,11 +48,18 @@ import OpenSSL.Random (randBytes) import Polysemy (Member) import Servant hiding (Handler, Tagged) import Wire.API.Error +import Wire.API.Error.Brig (BrigError (AccessDenied)) import Wire.API.OAuth as OAuth -import Wire.API.Password (Password, mkSafePasswordScrypt) +import Wire.API.Password import Wire.API.Routes.Internal.Brig.OAuth qualified as I import Wire.API.Routes.Named (Named (Named)) import Wire.API.Routes.Public.Brig.OAuth +import Wire.AuthenticationSubsystem (AuthenticationSubsystem) +import Wire.AuthenticationSubsystem qualified as Authentication +import Wire.AuthenticationSubsystem.Error +import Wire.Error +import Wire.HashPassword (HashPassword) +import Wire.HashPassword qualified as HashPassword import Wire.Sem.Jwk import Wire.Sem.Jwk qualified as Jwk import Wire.Sem.Now (Now) @@ -59,48 +68,61 @@ import Wire.Sem.Now qualified as Now -------------------------------------------------------------------------------- -- API Internal -internalOauthAPI :: ServerT I.OAuthAPI (Handler r) +internalOauthAPI :: (Member HashPassword r) => ServerT I.OAuthAPI (Handler r) internalOauthAPI = Named @"create-oauth-client" registerOAuthClient - :<|> Named @"get-oauth-client" getOAuthClientById + :<|> Named @"i-get-oauth-client" getOAuthClientById :<|> Named @"update-oauth-client" updateOAuthClient :<|> Named @"delete-oauth-client" deleteOAuthClient -------------------------------------------------------------------------------- -- API Public -oauthAPI :: (Member Now r, Member Jwk r) => ServerT OAuthAPI (Handler r) +oauthAPI :: + ( Member Now r, + Member Jwk r, + Member AuthenticationSubsystem r + ) => + ServerT OAuthAPI (Handler r) oauthAPI = Named @"get-oauth-client" getOAuthClient :<|> Named @"create-oauth-auth-code" createNewOAuthAuthorizationCode :<|> Named @"create-oauth-access-token" createAccessTokenWith :<|> Named @"revoke-oauth-refresh-token" revokeRefreshToken :<|> Named @"get-oauth-applications" getOAuthApplications + :<|> Named @"revoke-oauth-account-access-v6" revokeOAuthAccountAccessV6 :<|> Named @"revoke-oauth-account-access" revokeOAuthAccountAccess + :<|> Named @"delete-oauth-refresh-token" deleteOAuthRefreshTokenById -------------------------------------------------------------------------------- -- Handlers -registerOAuthClient :: OAuthClientConfig -> (Handler r) OAuthClientCredentials +registerOAuthClient :: (Member HashPassword r) => OAuthClientConfig -> (Handler r) OAuthClientCredentials registerOAuthClient (OAuthClientConfig name uri) = do - unlessM (Opt.setOAuthEnabled <$> view settings) $ throwStd $ errorToWai @'OAuthFeatureDisabled + guardOAuthEnabled credentials@(OAuthClientCredentials cid secret) <- OAuthClientCredentials <$> randomId <*> createSecret - safeSecret <- liftIO $ hashClientSecret secret + safeSecret <- hashClientSecret secret lift $ wrapClient $ insertOAuthClient cid name uri safeSecret pure credentials where createSecret :: (MonadIO m) => m OAuthClientPlainTextSecret createSecret = OAuthClientPlainTextSecret <$> rand32Bytes - hashClientSecret :: (MonadIO m) => OAuthClientPlainTextSecret -> m Password - hashClientSecret = mkSafePasswordScrypt . plainTextPassword8Unsafe . toText . unOAuthClientPlainTextSecret + hashClientSecret :: (Member HashPassword r) => OAuthClientPlainTextSecret -> (Handler r) Password + hashClientSecret = + lift + . liftSem + . HashPassword.hashPassword8 + . plainTextPassword8Unsafe + . toText + . unOAuthClientPlainTextSecret rand32Bytes :: (MonadIO m) => m AsciiBase16 rand32Bytes = liftIO . fmap encodeBase16 $ randBytes 32 getOAuthClientById :: OAuthClientId -> (Handler r) OAuthClient getOAuthClientById cid = do - unlessM (Opt.setOAuthEnabled <$> view settings) $ throwStd $ errorToWai @'OAuthFeatureDisabled + guardOAuthEnabled mClient <- lift $ wrapClient $ lookupOauthClient cid maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure mClient @@ -115,16 +137,20 @@ deleteOAuthClient cid = do void $ getOAuthClientById cid lift $ wrapClient $ deleteOAuthClient' cid --------------------------------------------------------------------------------- - -getOAuthClient :: UserId -> OAuthClientId -> (Handler r) (Maybe OAuthClient) +getOAuthClient :: Local UserId -> OAuthClientId -> (Handler r) (Maybe OAuthClient) getOAuthClient _ cid = do - unlessM (Opt.setOAuthEnabled <$> view settings) $ throwStd $ errorToWai @'OAuthFeatureDisabled + guardOAuthEnabled lift $ wrapClient $ lookupOauthClient cid -createNewOAuthAuthorizationCode :: UserId -> CreateOAuthAuthorizationCodeRequest -> (Handler r) CreateOAuthCodeResponse -createNewOAuthAuthorizationCode uid code = do - runExceptT (validateAndCreateAuthorizationCode uid code) >>= \case +guardOAuthEnabled :: Handler r () +guardOAuthEnabled = do + settings <- asks (.settings) + unless (Opt.oAuthEnabled settings) do + throwStd (errorToWai @'OAuthFeatureDisabled) + +createNewOAuthAuthorizationCode :: Local UserId -> CreateOAuthAuthorizationCodeRequest -> (Handler r) CreateOAuthCodeResponse +createNewOAuthAuthorizationCode luid code = do + runExceptT (validateAndCreateAuthorizationCode luid code) >>= \case Right oauthCode -> pure $ CreateOAuthCodeSuccess $ @@ -169,26 +195,24 @@ data CreateNewOAuthCodeError | CreateNewOAuthCodeErrorUnsupportedResponseType | CreateNewOAuthCodeErrorRedirectUrlMissMatch -validateAndCreateAuthorizationCode :: UserId -> CreateOAuthAuthorizationCodeRequest -> ExceptT CreateNewOAuthCodeError (Handler r) OAuthAuthorizationCode -validateAndCreateAuthorizationCode uid (CreateOAuthAuthorizationCodeRequest cid scope responseType redirectUrl _state _ chal) = do - failWithM CreateNewOAuthCodeErrorFeatureDisabled (assertMay . Opt.setOAuthEnabled <$> view settings) +validateAndCreateAuthorizationCode :: Local UserId -> CreateOAuthAuthorizationCodeRequest -> ExceptT CreateNewOAuthCodeError (Handler r) OAuthAuthorizationCode +validateAndCreateAuthorizationCode luid@(tUnqualified -> uid) (CreateOAuthAuthorizationCodeRequest cid scope responseType redirectUrl _state _ chal) = do + failWithM CreateNewOAuthCodeErrorFeatureDisabled (assertMay . Opt.oAuthEnabled <$> asks (.settings)) failWith CreateNewOAuthCodeErrorUnsupportedResponseType (assertMay $ responseType == OAuthResponseTypeCode) - client <- failWithM CreateNewOAuthCodeErrorClientNotFound $ getOAuthClient uid cid + client <- failWithM CreateNewOAuthCodeErrorClientNotFound $ getOAuthClient luid cid failWith CreateNewOAuthCodeErrorRedirectUrlMissMatch (assertMay $ client.redirectUrl == redirectUrl) lift mkAuthorizationCode where mkAuthorizationCode :: (Handler r) OAuthAuthorizationCode mkAuthorizationCode = do oauthCode <- OAuthAuthorizationCode <$> rand32Bytes - ttl <- Opt.setOAuthAuthorizationCodeExpirationTimeSecs <$> view settings + ttl <- Opt.oAuthAuthorizationCodeExpirationTimeSecs <$> asks (.settings) lift $ wrapClient $ insertOAuthAuthorizationCode ttl oauthCode cid uid scope redirectUrl chal pure oauthCode --------------------------------------------------------------------------------- - createAccessTokenWith :: (Member Now r, Member Jwk r) => Either OAuthAccessTokenRequest OAuthRefreshAccessTokenRequest -> (Handler r) OAuthAccessTokenResponse createAccessTokenWith req = do - unlessM (Opt.setOAuthEnabled <$> view settings) $ throwStd $ errorToWai @'OAuthFeatureDisabled + guardOAuthEnabled case req of Left reqAC -> createAccessTokenWithAuthorizationCode reqAC Right reqRT -> createAccessTokenWithRefreshToken reqRT @@ -198,7 +222,8 @@ createAccessTokenWithRefreshToken req = do unless (req.grantType == OAuthGrantTypeRefreshToken) $ throwStd $ errorToWai @'OAuthInvalidGrantType key <- signingKey (OAuthRefreshTokenInfo _ cid uid scope _) <- lookupVerifyAndDeleteToken key req.refreshToken - void $ getOAuthClient uid cid >>= maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure + luid <- qualifyLocal uid + void $ getOAuthClient luid cid >>= maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure unless (cid == req.clientId) $ throwStd $ errorToWai @'OAuthInvalidClientCredentials createAccessToken key uid cid scope @@ -223,7 +248,8 @@ createAccessTokenWithAuthorizationCode req = do (cid, uid, scope, uri, mChal) <- lift (wrapClient $ lookupAndDeleteByOAuthAuthorizationCode req.code) >>= maybe (throwStd $ errorToWai @'OAuthAuthorizationCodeNotFound) pure - oauthClient <- getOAuthClient uid req.clientId >>= maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure + luid <- qualifyLocal uid + oauthClient <- getOAuthClient luid req.clientId >>= maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure unless (uri == req.redirectUri) $ throwStd $ errorToWai @'OAuthRedirectUrlMissMatch unless (oauthClient.redirectUrl == req.redirectUri) $ throwStd $ errorToWai @'OAuthRedirectUrlMissMatch @@ -234,18 +260,18 @@ createAccessTokenWithAuthorizationCode req = do signingKey :: (Member Jwk r) => (Handler r) JWK signingKey = do - fp <- view settings >>= maybe (throwStd $ errorToWai @'OAuthJwtError) pure . Opt.setOAuthJwkKeyPair + fp <- maybe (throwStd $ errorToWai @'OAuthJwtError) pure =<< asks (.settings.oAuthJwkKeyPair) lift (liftSem $ Jwk.get fp) >>= maybe (throwStd $ errorToWai @'OAuthJwtError) pure createAccessToken :: (Member Now r) => JWK -> UserId -> OAuthClientId -> OAuthScopes -> (Handler r) OAuthAccessTokenResponse createAccessToken key uid cid scope = do - exp <- fromIntegral . Opt.setOAuthAccessTokenExpirationTimeSecs <$> view settings + exp <- fromIntegral . Opt.oAuthAccessTokenExpirationTimeSecs <$> asks (.settings) accessToken <- mkAccessToken (rid, refreshToken) <- mkRefreshToken now <- lift (liftSem Now.get) let refreshTokenInfo = OAuthRefreshTokenInfo rid cid uid scope now - refreshTokenExpiration <- Opt.setOAuthRefreshTokenExpirationTimeSecs <$> view settings - maxActiveTokens <- Opt.setOAuthMaxActiveRefreshTokens <$> view settings + refreshTokenExpiration <- Opt.oAuthRefreshTokenExpirationTimeSecs <$> asks (.settings) + maxActiveTokens <- Opt.oAuthMaxActiveRefreshTokens <$> asks (.settings) lift $ wrapClient $ insertOAuthRefreshToken maxActiveTokens refreshTokenExpiration refreshTokenInfo pure $ OAuthAccessTokenResponse accessToken OAuthAccessTokenTypeBearer exp refreshToken where @@ -258,8 +284,8 @@ createAccessToken key uid cid scope = do mkAccessToken :: (Member Now r) => (Handler r) OAuthAccessToken mkAccessToken = do - domain <- Opt.setFederationDomain <$> view settings - exp <- fromIntegral . Opt.setOAuthAccessTokenExpirationTimeSecs <$> view settings + domain <- asks (.settings.federationDomain) + exp <- fromIntegral . Opt.oAuthAccessTokenExpirationTimeSecs <$> asks (.settings) claims <- mkAccessTokenClaims uid domain scope exp OAuthToken <$> signAccessToken claims @@ -298,14 +324,13 @@ createAccessToken key uid cid scope = do algo <- bestJWSAlg key signClaims key (newJWSHeader ((), algo)) claims --------------------------------------------------------------------------------- - revokeRefreshToken :: (Member Jwk r) => OAuthRevokeRefreshTokenRequest -> (Handler r) () revokeRefreshToken req = do key <- signingKey info <- lookupAndVerifyToken key req.refreshToken - void $ getOAuthClient info.userId info.clientId >>= maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure - lift $ wrapClient $ deleteOAuthRefreshToken info + luid <- qualifyLocal info.userId + void $ getOAuthClient luid info.clientId >>= maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure + lift $ wrapClient $ deleteOAuthRefreshToken info.userId info.refreshTokenId lookupAndVerifyToken :: JWK -> OAuthRefreshToken -> (Handler r) OAuthRefreshTokenInfo lookupAndVerifyToken key = @@ -315,22 +340,58 @@ lookupAndVerifyToken key = . lookupOAuthRefreshTokenInfo >=> maybe (throwStd $ errorToWai @'OAuthInvalidRefreshToken) pure --------------------------------------------------------------------------------- - -getOAuthApplications :: UserId -> (Handler r) [OAuthApplication] -getOAuthApplications uid = do +getOAuthApplications :: Local UserId -> (Handler r) [OAuthApplication] +getOAuthApplications (tUnqualified -> uid) = do activeRefreshTokens <- lift $ wrapClient $ lookupOAuthRefreshTokens uid - nub . catMaybes <$> for activeRefreshTokens oauthApp + toApplications activeRefreshTokens where - oauthApp :: OAuthRefreshTokenInfo -> (Handler r) (Maybe OAuthApplication) - oauthApp info = (OAuthApplication info.clientId . (.name)) <$$> getOAuthClient info.userId info.clientId - --------------------------------------------------------------------------------- - -revokeOAuthAccountAccess :: UserId -> OAuthClientId -> (Handler r) () -revokeOAuthAccountAccess uid cid = do + toApplications :: [OAuthRefreshTokenInfo] -> (Handler r) [OAuthApplication] + toApplications infos = do + let grouped = Map.fromListWith (<>) $ (\info -> (info.clientId, [info])) <$> infos + mApps <- for (Map.toList grouped) $ \(cid, tokens) -> do + let luid = undefined uid + mClient <- getOAuthClient luid cid + pure $ (\client -> OAuthApplication cid client.name ((\i -> OAuthSession i.refreshTokenId (toUTCTimeMillis i.createdAt)) <$> tokens)) <$> mClient + pure $ catMaybes mApps + +revokeOAuthAccountAccessV6 :: Local UserId -> OAuthClientId -> (Handler r) () +revokeOAuthAccountAccessV6 (tUnqualified -> uid) cid = do rts <- lift $ wrapClient $ lookupOAuthRefreshTokens uid - for_ rts $ \rt -> when (rt.clientId == cid) $ lift $ wrapClient $ deleteOAuthRefreshToken rt + for_ rts $ \rt -> when (rt.clientId == cid) $ lift $ wrapClient $ deleteOAuthRefreshToken uid rt.refreshTokenId + +revokeOAuthAccountAccess :: + (Member AuthenticationSubsystem r) => + Local UserId -> + OAuthClientId -> + PasswordReqBody -> + (Handler r) () +revokeOAuthAccountAccess luid@(tUnqualified -> uid) cid req = do + (lift . liftSem $ Authentication.reauthenticateEither uid req.fromPasswordReqBody) + >>= either (throwE . toAccessDenied) (const $ pure ()) + revokeOAuthAccountAccessV6 luid cid + where + toAccessDenied :: ReAuthError -> HttpError + toAccessDenied _ = StdError $ errorToWai @'AccessDenied + +deleteOAuthRefreshTokenById :: + (Member AuthenticationSubsystem r) => + Local UserId -> + OAuthClientId -> + OAuthRefreshTokenId -> + PasswordReqBody -> + (Handler r) () +deleteOAuthRefreshTokenById (tUnqualified -> uid) cid tokenId req = do + (lift . liftSem $ Authentication.reauthenticateEither uid req.fromPasswordReqBody) + >>= either (throwE . toAccessDenied) (const $ pure ()) + mInfo <- lift $ wrapClient $ lookupOAuthRefreshTokenInfo tokenId + case mInfo of + Nothing -> pure () + Just info -> do + when (info.clientId /= cid) $ throwStd $ errorToWai @'OAuthClientNotFound + lift $ wrapClient $ deleteOAuthRefreshToken uid tokenId + where + toAccessDenied :: ReAuthError -> HttpError + toAccessDenied _ = StdError $ errorToWai @'AccessDenied -------------------------------------------------------------------------------- -- DB @@ -390,7 +451,7 @@ insertOAuthRefreshToken :: (MonadClient m) => Word32 -> Word64 -> OAuthRefreshTo insertOAuthRefreshToken maxActiveTokens ttl info = do let rid = info.refreshTokenId oldTokes <- determineOldestTokensToBeDeleted <$> lookupOAuthRefreshTokens info.userId - for_ oldTokes deleteOAuthRefreshToken + for_ oldTokes (\t -> deleteOAuthRefreshToken t.userId t.refreshTokenId) retry x5 . write qInsertId $ params LocalQuorum (info.userId, rid, fromIntegral ttl) retry x5 . write qInsertInfo $ params LocalQuorum (rid, info.clientId, info.userId, C.Set (Set.toList (unOAuthScopes info.scopes)), info.createdAt, fromIntegral ttl) where @@ -404,7 +465,7 @@ insertOAuthRefreshToken maxActiveTokens ttl info = do determineOldestTokensToBeDeleted tokens = take (length sorted - fromIntegral maxActiveTokens + 1) sorted where - sorted = sortOn createdAt tokens + sorted = sortOn (.createdAt) tokens lookupOAuthRefreshTokens :: (MonadClient m) => UserId -> m [OAuthRefreshTokenInfo] lookupOAuthRefreshTokens uid = do @@ -422,10 +483,9 @@ lookupOAuthRefreshTokenInfo rid = do q :: PrepQuery R (Identity OAuthRefreshTokenId) (OAuthClientId, UserId, C.Set OAuthScope, UTCTime) q = "SELECT client, user, scope, created_at FROM oauth_refresh_token WHERE id = ?" -deleteOAuthRefreshToken :: (MonadClient m) => OAuthRefreshTokenInfo -> m () -deleteOAuthRefreshToken info = do - let rid = info.refreshTokenId - retry x5 . write qDeleteId $ params LocalQuorum (info.userId, rid) +deleteOAuthRefreshToken :: (MonadClient m) => UserId -> OAuthRefreshTokenId -> m () +deleteOAuthRefreshToken uid rid = do + retry x5 . write qDeleteId $ params LocalQuorum (uid, rid) retry x5 . write qDeleteInfo $ params LocalQuorum (Identity rid) where qDeleteId :: PrepQuery W (UserId, OAuthRefreshTokenId) () @@ -437,4 +497,4 @@ deleteOAuthRefreshToken info = do lookupAndDeleteOAuthRefreshToken :: (MonadClient m) => OAuthRefreshTokenId -> m (Maybe OAuthRefreshTokenInfo) lookupAndDeleteOAuthRefreshToken rid = do mInfo <- lookupOAuthRefreshTokenInfo rid - for_ mInfo deleteOAuthRefreshToken $> mInfo + for_ mInfo (\info -> deleteOAuthRefreshToken info.userId info.refreshTokenId) $> mInfo diff --git a/services/brig/src/Brig/API/Properties.hs b/services/brig/src/Brig/API/Properties.hs deleted file mode 100644 index 814b899962a..00000000000 --- a/services/brig/src/Brig/API/Properties.hs +++ /dev/null @@ -1,54 +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 Brig.API.Properties - ( PropertiesDataError (..), - setProperty, - deleteProperty, - clearProperties, - Data.lookupProperty, - Data.lookupPropertyKeys, - Data.lookupPropertyKeysAndValues, - ) -where - -import Brig.App -import Brig.Data.Properties (PropertiesDataError) -import Brig.Data.Properties qualified as Data -import Brig.IO.Intra qualified as Intra -import Control.Error -import Data.Id -import Imports -import Polysemy -import Wire.API.Properties -import Wire.API.UserEvent -import Wire.NotificationSubsystem - -setProperty :: (Member NotificationSubsystem r) => UserId -> ConnId -> PropertyKey -> PropertyValue -> ExceptT PropertiesDataError (AppT r) () -setProperty u c k v = do - wrapClientE $ Data.insertProperty u k (propertyRaw v) - lift $ liftSem $ Intra.onPropertyEvent u c (PropertySet k (propertyValue v)) - -deleteProperty :: (Member NotificationSubsystem r) => UserId -> ConnId -> PropertyKey -> AppT r () -deleteProperty u c k = do - wrapClient $ Data.deleteProperty u k - liftSem $ Intra.onPropertyEvent u c (PropertyDeleted k) - -clearProperties :: (Member NotificationSubsystem r) => UserId -> ConnId -> AppT r () -clearProperties u c = do - wrapClient $ Data.clearProperties u - liftSem $ Intra.onPropertyEvent u c PropertiesCleared diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index e468afda8af..6580a413959 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -32,7 +32,6 @@ import Brig.API.Error import Brig.API.Handler import Brig.API.MLS.KeyPackages import Brig.API.OAuth (oauthAPI) -import Brig.API.Properties qualified as API import Brig.API.Public.Swagger import Brig.API.Types import Brig.API.User qualified as API @@ -42,9 +41,7 @@ import Brig.Calling.API qualified as Calling import Brig.Data.Connection qualified as Data import Brig.Data.Nonce as Nonce import Brig.Data.User qualified as Data -import Brig.Effects.BlacklistStore (BlacklistStore) -import Brig.Effects.ConnectionStore (ConnectionStore) -import Brig.Effects.FederationConfigStore (FederationConfigStore) +import Brig.Effects.ConnectionStore import Brig.Effects.JwtTools (JwtTools) import Brig.Effects.PublicKeyBundle (PublicKeyBundle) import Brig.Effects.SFT @@ -54,22 +51,17 @@ import Brig.Provider.API import Brig.Team.API qualified as Team import Brig.Team.Email qualified as Team import Brig.Types.Activation (ActivationPair) -import Brig.Types.Intra (UserAccount (UserAccount, accountUser)) -import Brig.Types.User (HavePendingInvitations (..)) +import Brig.Types.Intra import Brig.User.API.Handle qualified as Handle -import Brig.User.API.Search (teamUserSearch) -import Brig.User.API.Search qualified as Search import Brig.User.Auth.Cookie qualified as Auth import Cassandra qualified as C import Cassandra qualified as Data import Control.Error hiding (bool, note) -import Control.Lens (view, (.~), (?~), (^.)) +import Control.Lens ((.~), (?~)) import Control.Monad.Catch (throwM) import Control.Monad.Except import Data.Aeson hiding (json) -import Data.Bifunctor import Data.ByteString (fromStrict, toStrict) -import Data.ByteString.Lazy qualified as Lazy import Data.ByteString.Lazy.Char8 qualified as LBS import Data.ByteString.UTF8 qualified as UTF8 import Data.Code qualified as Code @@ -79,6 +71,7 @@ import Data.Domain import Data.FileEmbed import Data.Handle (Handle) import Data.Handle qualified as Handle +import Data.HavePendingInvitations import Data.Id import Data.Id qualified as Id import Data.List.NonEmpty (nonEmpty) @@ -88,17 +81,16 @@ import Data.OpenApi qualified as S import Data.Qualified import Data.Range import Data.Schema () -import Data.Text qualified as Text -import Data.Text.Ascii qualified as Ascii import Data.Text.Encoding qualified as Text -import Data.Text.Lazy (pack) -import Data.Time.Clock (UTCTime) +import Data.Time.Clock import Data.ZAuth.Token qualified as ZAuth import FileEmbedLzma import Imports hiding (head) import Network.Socket (PortNumber) -import Network.Wai.Utilities as Utilities +import Network.Wai.Utilities (CacheControl (..), (!>>)) +import Network.Wai.Utilities qualified as Utilities import Polysemy +import Polysemy.Error import Polysemy.Input (Input) import Polysemy.TinyLog (TinyLog) import Servant hiding (Handler, JSON, addHeader, respond) @@ -110,17 +102,18 @@ import Util.Logging (logFunction, logHandle, logTeam, logUser) import Wire.API.Connection qualified as Public import Wire.API.Error import Wire.API.Error.Brig qualified as E -import Wire.API.Federation.API import Wire.API.Federation.API.Brig qualified as BrigFederationAPI import Wire.API.Federation.API.Cargohold qualified as CargoholdFederationAPI import Wire.API.Federation.API.Galley qualified as GalleyFederationAPI import Wire.API.Federation.Error +import Wire.API.Federation.Version qualified as Fed import Wire.API.Properties qualified as Public import Wire.API.Routes.API import Wire.API.Routes.Internal.Brig qualified as BrigInternalAPI import Wire.API.Routes.Internal.Cannon qualified as CannonInternalAPI import Wire.API.Routes.Internal.Cargohold qualified as CargoholdInternalAPI import Wire.API.Routes.Internal.Galley qualified as GalleyInternalAPI +import Wire.API.Routes.Internal.Gundeck qualified as GundeckInternalAPI import Wire.API.Routes.Internal.Spar qualified as SparInternalAPI import Wire.API.Routes.MultiTablePaging qualified as Public import Wire.API.Routes.Named (Named (Named)) @@ -149,25 +142,40 @@ import Wire.API.User.Client.Prekey qualified as Public import Wire.API.User.Handle qualified as Public import Wire.API.User.Password qualified as Public import Wire.API.User.RichInfo qualified as Public +import Wire.API.User.Search qualified as Public import Wire.API.UserMap qualified as Public import Wire.API.Wrapped qualified as Public +import Wire.ActivationCodeStore (ActivationCodeStore) import Wire.AuthenticationSubsystem (AuthenticationSubsystem, createPasswordResetCode, resetPassword) +import Wire.BlockListStore (BlockListStore) import Wire.DeleteQueue import Wire.EmailSending (EmailSending) import Wire.EmailSubsystem +import Wire.EmailSubsystem.Template import Wire.Error +import Wire.Events (Events) +import Wire.FederationConfigStore (FederationConfigStore) import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess +import Wire.HashPassword (HashPassword) +import Wire.IndexedUserStore (IndexedUserStore) +import Wire.InvitationStore import Wire.NotificationSubsystem +import Wire.PasswordResetCodeStore (PasswordResetCodeStore) import Wire.PasswordStore (PasswordStore, lookupHashedPassword) +import Wire.PropertySubsystem import Wire.Sem.Concurrency import Wire.Sem.Jwk (Jwk) import Wire.Sem.Now (Now) -import Wire.Sem.Paging.Cassandra (InternalPaging) +import Wire.Sem.Paging.Cassandra +import Wire.TeamInvitationSubsystem import Wire.UserKeyStore +import Wire.UserSearch.Types import Wire.UserStore (UserStore) +import Wire.UserStore qualified as UserStore import Wire.UserSubsystem hiding (checkHandle, checkHandles) -import Wire.UserSubsystem qualified as UserSubsystem +import Wire.UserSubsystem qualified as User +import Wire.UserSubsystem.Error import Wire.VerificationCode import Wire.VerificationCodeGen import Wire.VerificationCodeSubsystem @@ -190,31 +198,33 @@ federatedEndpointsSwaggerDocsAPIs = internalEndpointsSwaggerDocsAPIs :: Servant.Server InternalEndpointsSwaggerDocsAPI internalEndpointsSwaggerDocsAPIs = internalEndpointsSwaggerDocsAPI @"brig" "brig" 9082 BrigInternalAPI.swaggerDoc - :<|> internalEndpointsSwaggerDocsAPI @"cannon" "cannon" 9093 CannonInternalAPI.swaggerDoc - :<|> internalEndpointsSwaggerDocsAPI @"cargohold" "cargohold" 9094 CargoholdInternalAPI.swaggerDoc - :<|> internalEndpointsSwaggerDocsAPI @"galley" "galley" 9095 GalleyInternalAPI.swaggerDoc - :<|> internalEndpointsSwaggerDocsAPI @"spar" "spar" 9098 SparInternalAPI.swaggerDoc + :<|> internalEndpointsSwaggerDocsAPI @"cannon" "cannon" 9083 CannonInternalAPI.swaggerDoc + :<|> internalEndpointsSwaggerDocsAPI @"cargohold" "cargohold" 9084 CargoholdInternalAPI.swaggerDoc + :<|> internalEndpointsSwaggerDocsAPI @"galley" "galley" 9085 GalleyInternalAPI.swaggerDoc + :<|> internalEndpointsSwaggerDocsAPI @"spar" "spar" 9088 SparInternalAPI.swaggerDoc + :<|> internalEndpointsSwaggerDocsAPI @"gundeck" "gundeck" 9086 GundeckInternalAPI.swaggerDoc -- | Serves Swagger docs for public endpoints -- -- Dual to `internalEndpointsSwaggerDocsAPI`. versionedSwaggerDocsAPI :: Servant.Server VersionedSwaggerDocsAPI -versionedSwaggerDocsAPI (Just (VersionNumber V6)) = +versionedSwaggerDocsAPI (Just (VersionNumber V7)) = swaggerSchemaUIServer $ - ( serviceSwagger @VersionAPITag @'V6 - <> serviceSwagger @BrigAPITag @'V6 - <> serviceSwagger @GalleyAPITag @'V6 - <> serviceSwagger @SparAPITag @'V6 - <> serviceSwagger @CargoholdAPITag @'V6 - <> serviceSwagger @CannonAPITag @'V6 - <> serviceSwagger @GundeckAPITag @'V6 - <> serviceSwagger @ProxyAPITag @'V6 - <> serviceSwagger @OAuthAPITag @'V6 + ( serviceSwagger @VersionAPITag @'V7 + <> serviceSwagger @BrigAPITag @'V7 + <> serviceSwagger @GalleyAPITag @'V7 + <> serviceSwagger @SparAPITag @'V7 + <> serviceSwagger @CargoholdAPITag @'V7 + <> serviceSwagger @CannonAPITag @'V7 + <> serviceSwagger @GundeckAPITag @'V7 + <> serviceSwagger @ProxyAPITag @'V7 + <> serviceSwagger @OAuthAPITag @'V7 ) & S.info . S.title .~ "Wire-Server API" & S.info . S.description ?~ $(embedText =<< makeRelativeToProject "docs/swagger.md") - & S.servers .~ [S.Server ("/" <> toUrlPiece V6) Nothing mempty] + & S.servers .~ [S.Server ("/" <> toUrlPiece V7) Nothing mempty] & cleanupSwagger +versionedSwaggerDocsAPI (Just (VersionNumber V6)) = swaggerPregenUIServer $(pregenSwagger V6) versionedSwaggerDocsAPI (Just (VersionNumber V5)) = swaggerPregenUIServer $(pregenSwagger V5) versionedSwaggerDocsAPI (Just (VersionNumber V4)) = swaggerPregenUIServer $(pregenSwagger V4) versionedSwaggerDocsAPI (Just (VersionNumber V3)) = swaggerPregenUIServer $(pregenSwagger V3) @@ -235,79 +245,125 @@ versionedSwaggerDocsAPI Nothing = allroutes (throwError listAllVersionsResp) listAllVersionsHTML :: LByteString listAllVersionsHTML = - "

please pick an api version

" - <> mconcat - [ let url = "/" <> toQueryParam v <> "/api/swagger-ui/" - in " (fromStrict . Text.encodeUtf8 $ url) - <> "\">" - <> (fromStrict . Text.encodeUtf8 $ url) - <> "
" - | v <- [minBound :: Version ..] + LBS.unlines $ + [ "

OpenAPI 3.0 docs for all Wire APIs

", + intro, + LBS.unlines public, + LBS.unlines internal, + LBS.unlines federated, + "" + ] + where + intro = + "

This wire-server system provides OpenAPI 3.0 \ + \documentation of our HTTP REST API.

\ + \

The openapi docs are correct by construction (compiled from the server code), and more or less \ + \complete.

\ + \

Some endpoints are version-controlled. Show all supported versions. \ + \find out more." + + public :: [LByteString] + public = + ["

Public (all available versions)

"] + <> mconcat + [ [ v <> ": ", + renderLink "swagger-ui" ("/" <> v <> "/api/swagger-ui") <> "; ", + renderLink "swagger.json" ("/" <> v <> "/api/swagger.json"), + "
" + ] + | v <- versionToLByteString <$> [minBound :: Version ..] + ] + + internal :: [LByteString] + internal = + [ "

Internal (not versioned)

", + "

Openapi docs for internal endpoints are served per service. I.e. there's one for `brig`, one for `cannon`, \ + \etc.. This is because Openapi doesn't play well with multiple actions having the same combination of HTTP \ + \method and URL path.

" ] - <> "" - --- | Serves Swagger docs for internal endpoints --- --- Dual to `versionedSwaggerDocsAPI`. Swagger docs for old versions are (almost) --- empty. It would have been too tedious to create them. Please add --- pre-generated docs on version increase as it's done in --- `versionedSwaggerDocsAPI`. --- --- If you're having issues with this function not typechecking when it should, --- be sure to supply the type argument explicitly + <> mconcat + [ [ s <> ":
", + renderLink "swagger-ui" ("/api-internal/swagger-ui/" <> s) <> "; ", + renderLink "swagger.json" ("/api-internal/swagger-ui/" <> s <> "-swagger.json"), + "
" + ] + | s <- ["brig", "galley", "spar", "cargohold", "gundeck", "cannon", "proxy"] + ] + + federated :: [LByteString] + federated = + ["

Federated API (backend-to-backend)

"] + <> [ mconcat + [ mconcat + [ s <> " (" <> v <> "):
", + renderLink "swagger-ui" ("/" <> v <> "/api-federation/swagger-ui/" <> s) <> "; ", + renderLink "swagger.json" ("/" <> v <> "/api-federation/swagger-ui/" <> s <> "-swagger.json"), + "
" + ] + | v <- versionToLByteString <$> [minBound :: Fed.Version ..] + ] + <> "
" + | s <- ["brig", "galley", "cargohold"] + ] + + versionToLByteString :: (ToHttpApiData v) => v -> LByteString + versionToLByteString = fromStrict . Text.encodeUtf8 . toQueryParam + + renderLink :: LByteString -> LByteString -> LByteString + renderLink caption url = " url <> "\">" <> caption <> "" + +-- | Serves Swagger docs for internal endpoints. internalEndpointsSwaggerDocsAPI :: forall service. String -> PortNumber -> S.OpenApi -> Servant.Server (VersionedSwaggerDocsAPIBase service) -internalEndpointsSwaggerDocsAPI service examplePort swagger (Just (VersionNumber V6)) = - swaggerSchemaUIServer $ - swagger - & adjustSwaggerForInternalEndpoint service examplePort - & cleanupSwagger -internalEndpointsSwaggerDocsAPI service examplePort swagger (Just (VersionNumber V5)) = +internalEndpointsSwaggerDocsAPI _ _ _ (Just _) = emptySwagger "Internal APIs are not versioned!" +internalEndpointsSwaggerDocsAPI service examplePort swagger Nothing = swaggerSchemaUIServer $ swagger & adjustSwaggerForInternalEndpoint service examplePort & cleanupSwagger -internalEndpointsSwaggerDocsAPI _ _ _ (Just (VersionNumber V4)) = emptySwagger -internalEndpointsSwaggerDocsAPI _ _ _ (Just (VersionNumber V3)) = emptySwagger -internalEndpointsSwaggerDocsAPI _ _ _ (Just (VersionNumber V2)) = emptySwagger -internalEndpointsSwaggerDocsAPI _ _ _ (Just (VersionNumber V1)) = emptySwagger -internalEndpointsSwaggerDocsAPI _ _ _ (Just (VersionNumber V0)) = emptySwagger -internalEndpointsSwaggerDocsAPI service examplePort swagger Nothing = - internalEndpointsSwaggerDocsAPI service examplePort swagger (Just maxBound) servantSitemap :: forall r p. - ( Member BlacklistStore r, - Member DeleteQueue r, - Member (Concurrency 'Unsafe) r, - Member (ConnectionStore InternalPaging) r, - Member (Embed HttpClientIO) r, + ( Member (Embed HttpClientIO) r, Member (Embed IO) r, - Member FederationConfigStore r, + Member (Error UserSubsystemError) r, Member (Input (Local ())) r, - Member AuthenticationSubsystem r, Member (Input UTCTime) r, - Member Jwk r, + Member (UserPendingActivationStore p) r, + Member AuthenticationSubsystem r, + Member DeleteQueue r, + Member EmailSending r, + Member EmailSubsystem r, + Member Events r, + Member FederationConfigStore r, Member GalleyAPIAccess r, + Member InvitationStore r, + Member Jwk r, Member JwtTools r, Member NotificationSubsystem r, - Member UserSubsystem r, - Member UserStore r, - Member PasswordStore r, - Member UserKeyStore r, Member Now r, + Member PasswordResetCodeStore r, + Member PasswordStore r, + Member PropertySubsystem r, Member PublicKeyBundle r, Member SFT r, Member TinyLog r, - Member (UserPendingActivationStore p) r, - Member EmailSubsystem r, - Member EmailSending r, - Member VerificationCodeSubsystem r + Member UserKeyStore r, + Member ActivationCodeStore r, + Member UserStore r, + Member (Input TeamTemplates) r, + Member UserSubsystem r, + Member TeamInvitationSubsystem r, + Member VerificationCodeSubsystem r, + Member (Concurrency 'Unsafe) r, + Member BlockListStore r, + Member (ConnectionStore InternalPaging) r, + Member IndexedUserStore r, + Member HashPassword r ) => ServerT BrigAPI (Handler r) servantSitemap = @@ -333,14 +389,14 @@ servantSitemap = where userAPI :: ServerT UserAPI (Handler r) userAPI = - Named @"get-user-unqualified" (callsFed (exposeAnnotations getUserUnqualifiedH)) - :<|> Named @"get-user-qualified" (callsFed (exposeAnnotations getUserProfileH)) + Named @"get-user-unqualified" getUserUnqualifiedH + :<|> Named @"get-user-qualified" getUserProfileH :<|> Named @"update-user-email" updateUserEmail - :<|> Named @"get-handle-info-unqualified" (callsFed (exposeAnnotations getHandleInfoUnqualifiedH)) - :<|> Named @"get-user-by-handle-qualified" (callsFed (exposeAnnotations Handle.getHandleInfo)) - :<|> Named @"list-users-by-unqualified-ids-or-handles" (callsFed (exposeAnnotations listUsersByUnqualifiedIdsOrHandles)) - :<|> Named @"list-users-by-ids-or-handles" (callsFed (exposeAnnotations listUsersByIdsOrHandles)) - :<|> Named @"list-users-by-ids-or-handles@V3" (callsFed (exposeAnnotations listUsersByIdsOrHandlesV3)) + :<|> Named @"get-handle-info-unqualified" getHandleInfoUnqualifiedH + :<|> Named @"get-user-by-handle-qualified" Handle.getHandleInfo + :<|> Named @"list-users-by-unqualified-ids-or-handles" listUsersByUnqualifiedIdsOrHandles + :<|> Named @"list-users-by-ids-or-handles" listUsersByIdsOrHandles + :<|> Named @"list-users-by-ids-or-handles@V3" listUsersByIdsOrHandlesV3 :<|> Named @"send-verification-code" sendVerificationCode :<|> Named @"get-rich-info" getRichInfo :<|> Named @"get-supported-protocols" getSupportedProtocols @@ -348,23 +404,24 @@ servantSitemap = selfAPI :: ServerT SelfAPI (Handler r) selfAPI = Named @"get-self" getSelf - :<|> Named @"delete-self" (callsFed (exposeAnnotations deleteSelfUser)) - :<|> Named @"put-self" (callsFed (exposeAnnotations updateUser)) + :<|> Named @"delete-self" deleteSelfUser + :<|> Named @"put-self" updateUser :<|> Named @"change-phone" changePhone - :<|> Named @"remove-phone" (callsFed (exposeAnnotations removePhone)) - :<|> Named @"remove-email" (callsFed (exposeAnnotations removeEmail)) + :<|> Named @"remove-phone" removePhone + :<|> Named @"remove-email" removeEmail :<|> Named @"check-password-exists" checkPasswordExists :<|> Named @"change-password" changePassword - :<|> Named @"change-locale" (callsFed (exposeAnnotations changeLocale)) - :<|> Named @"change-handle" (callsFed (exposeAnnotations changeHandle)) + :<|> Named @"change-locale" changeLocale + :<|> Named @"change-handle" changeHandle :<|> Named @"change-supported-protocols" changeSupportedProtocols accountAPI :: ServerT AccountAPI (Handler r) accountAPI = - Named @"register" (callsFed (exposeAnnotations createUser)) - :<|> Named @"verify-delete" (callsFed (exposeAnnotations verifyDeleteUser)) - :<|> Named @"get-activate" (callsFed (exposeAnnotations activate)) - :<|> Named @"post-activate" (callsFed (exposeAnnotations activateKey)) + Named @"upgrade-personal-to-team" upgradePersonalToTeam + :<|> Named @"register" createUser + :<|> Named @"verify-delete" verifyDeleteUser + :<|> Named @"get-activate" activate + :<|> Named @"post-activate" activateKey :<|> Named @"post-activate-send" sendActivationCode :<|> Named @"post-password-reset" beginPasswordReset :<|> Named @"post-password-reset-complete" completePasswordReset @@ -373,33 +430,33 @@ servantSitemap = clientAPI :: ServerT ClientAPI (Handler r) clientAPI = - Named @"get-user-clients-unqualified" (callsFed (exposeAnnotations getUserClientsUnqualified)) - :<|> Named @"get-user-clients-qualified" (callsFed (exposeAnnotations getUserClientsQualified)) - :<|> Named @"get-user-client-unqualified" (callsFed (exposeAnnotations getUserClientUnqualified)) - :<|> Named @"get-user-client-qualified" (callsFed (exposeAnnotations getUserClientQualified)) - :<|> Named @"list-clients-bulk" (callsFed (exposeAnnotations listClientsBulk)) - :<|> Named @"list-clients-bulk-v2" (callsFed (exposeAnnotations listClientsBulkV2)) - :<|> Named @"list-clients-bulk@v2" (callsFed (exposeAnnotations listClientsBulkV2)) + Named @"get-user-clients-unqualified" getUserClientsUnqualified + :<|> Named @"get-user-clients-qualified" getUserClientsQualified + :<|> Named @"get-user-client-unqualified" getUserClientUnqualified + :<|> Named @"get-user-client-qualified" getUserClientQualified + :<|> Named @"list-clients-bulk" listClientsBulk + :<|> Named @"list-clients-bulk-v2" listClientsBulkV2 + :<|> Named @"list-clients-bulk@v2" listClientsBulkV2 prekeyAPI :: ServerT PrekeyAPI (Handler r) prekeyAPI = - Named @"get-users-prekeys-client-unqualified" (callsFed (exposeAnnotations getPrekeyUnqualifiedH)) - :<|> Named @"get-users-prekeys-client-qualified" (callsFed (exposeAnnotations getPrekeyH)) - :<|> Named @"get-users-prekey-bundle-unqualified" (callsFed (exposeAnnotations getPrekeyBundleUnqualifiedH)) - :<|> Named @"get-users-prekey-bundle-qualified" (callsFed (exposeAnnotations getPrekeyBundleH)) + Named @"get-users-prekeys-client-unqualified" getPrekeyUnqualifiedH + :<|> Named @"get-users-prekeys-client-qualified" getPrekeyH + :<|> Named @"get-users-prekey-bundle-unqualified" getPrekeyBundleUnqualifiedH + :<|> Named @"get-users-prekey-bundle-qualified" getPrekeyBundleH :<|> Named @"get-multi-user-prekey-bundle-unqualified" getMultiUserPrekeyBundleUnqualifiedH - :<|> Named @"get-multi-user-prekey-bundle-qualified@v3" (callsFed (exposeAnnotations getMultiUserPrekeyBundleHV3)) - :<|> Named @"get-multi-user-prekey-bundle-qualified" (callsFed (exposeAnnotations getMultiUserPrekeyBundleH)) + :<|> Named @"get-multi-user-prekey-bundle-qualified@v3" getMultiUserPrekeyBundleHV3 + :<|> Named @"get-multi-user-prekey-bundle-qualified" getMultiUserPrekeyBundleH userClientAPI :: ServerT UserClientAPI (Handler r) userClientAPI = - Named @"add-client-v5" (callsFed (exposeAnnotations addClient)) - :<|> Named @"add-client" (callsFed (exposeAnnotations addClient)) + Named @"add-client-v6" addClient + :<|> Named @"add-client" addClient :<|> Named @"update-client" updateClient :<|> Named @"delete-client" deleteClient - :<|> Named @"list-clients-v5" listClients + :<|> Named @"list-clients-v6" listClients :<|> Named @"list-clients" listClients - :<|> Named @"get-client-v5" getClient + :<|> Named @"get-client-v6" getClient :<|> Named @"get-client" getClient :<|> Named @"get-client-capabilities" getClientCapabilities :<|> Named @"get-client-prekeys" getClientPrekeys @@ -409,25 +466,25 @@ servantSitemap = connectionAPI :: ServerT ConnectionAPI (Handler r) connectionAPI = - Named @"create-connection-unqualified" (callsFed (exposeAnnotations createConnectionUnqualified)) - :<|> Named @"create-connection" (callsFed (exposeAnnotations createConnection)) + Named @"create-connection-unqualified" createConnectionUnqualified + :<|> Named @"create-connection" createConnection :<|> Named @"list-local-connections" listLocalConnections :<|> Named @"list-connections" listConnections :<|> Named @"get-connection-unqualified" getLocalConnection :<|> Named @"get-connection" getConnection - :<|> Named @"update-connection-unqualified" (callsFed (exposeAnnotations updateLocalConnection)) - :<|> Named @"update-connection" (callsFed (exposeAnnotations updateConnection)) - :<|> Named @"search-contacts" (callsFed (exposeAnnotations Search.search)) + :<|> Named @"update-connection-unqualified" updateLocalConnection + :<|> Named @"update-connection" updateConnection + :<|> Named @"search-contacts" searchUsersHandler propertiesAPI :: ServerT PropertiesAPI (Handler r) propertiesAPI = - ( Named @"set-property" setProperty - :<|> Named @"delete-property" deleteProperty - :<|> Named @"clear-properties" clearProperties - :<|> Named @"get-property" getProperty - :<|> Named @"list-property-keys" listPropertyKeys + ( Named @"set-property" setPropertyH + :<|> Named @"delete-property" deletePropertyH + :<|> Named @"clear-properties" clearPropertiesH + :<|> Named @"get-property" getPropertyH + :<|> Named @"list-property-keys" listPropertyKeysH ) - :<|> Named @"list-properties" listPropertyKeysAndValues + :<|> Named @"list-properties" listPropertyKeysAndValuesH mlsAPI :: ServerT MLSAPI (Handler r) mlsAPI = @@ -439,18 +496,20 @@ servantSitemap = userHandleAPI :: ServerT UserHandleAPI (Handler r) userHandleAPI = - Named @"check-user-handles" checkHandles + Named @"check-user-handles@v6" checkHandles + :<|> Named @"check-user-handles" checkHandles + :<|> Named @"check-user-handle@v6" checkHandle :<|> Named @"check-user-handle" checkHandle searchAPI :: ServerT SearchAPI (Handler r) searchAPI = - Named @"browse-team" teamUserSearch + Named @"browse-team" browseTeamHandler authAPI :: ServerT AuthAPI (Handler r) authAPI = - Named @"access" (callsFed (exposeAnnotations accessH)) + Named @"access" accessH :<|> Named @"send-login-code" sendLoginCode - :<|> Named @"login" (callsFed (exposeAnnotations login)) + :<|> Named @"login" login :<|> Named @"logout" logoutH :<|> Named @"change-self-email" changeSelfEmailH :<|> Named @"list-cookies" listCookies @@ -476,61 +535,38 @@ servantSitemap = --------------------------------------------------------------------------- -- Handlers -setProperty :: (Member NotificationSubsystem r) => UserId -> ConnId -> Public.PropertyKey -> Public.RawPropertyValue -> Handler r () -setProperty u c key raw = do - checkPropertyKey key - val <- safeParsePropertyValue raw - API.setProperty u c key val !>> propDataError - -checkPropertyKey :: Public.PropertyKey -> Handler r () -checkPropertyKey k = do - maxKeyLen <- fromMaybe defMaxKeyLen <$> view (settings . propertyMaxKeyLen) - let keyText = Ascii.toText (Public.propertyKeyName k) - when (Text.compareLength keyText (fromIntegral maxKeyLen) == GT) $ - throwStd propertyKeyTooLarge - --- | Parse a 'PropertyValue' from a bytestring. This is different from 'FromJSON' in that --- checks the byte size of the input, and fails *without consuming all of it* if that size --- exceeds the settings. -safeParsePropertyValue :: Public.RawPropertyValue -> Handler r Public.PropertyValue -safeParsePropertyValue raw = do - maxValueLen <- fromMaybe defMaxValueLen <$> view (settings . propertyMaxValueLen) - let lbs = Lazy.take (maxValueLen + 1) (Public.rawPropertyBytes raw) - unless (Lazy.length lbs <= maxValueLen) $ - throwStd propertyValueTooLarge - hoistEither $ first (StdError . badRequest . pack) (propertyValueFromRaw raw) - -propertyValueFromRaw :: Public.RawPropertyValue -> Either String Public.PropertyValue -propertyValueFromRaw raw = - Public.PropertyValue raw - <$> eitherDecode (Public.rawPropertyBytes raw) - -parseStoredPropertyValue :: Public.RawPropertyValue -> Handler r Public.PropertyValue -parseStoredPropertyValue raw = case propertyValueFromRaw raw of - Right value -> pure value - Left e -> do - Log.err $ - Log.msg (Log.val "Failed to parse a stored property value") - . Log.field "raw_value" (Public.rawPropertyBytes raw) - . Log.field "parse_error" e - throwStd internalServerError - -deleteProperty :: (Member NotificationSubsystem r) => UserId -> ConnId -> Public.PropertyKey -> Handler r () -deleteProperty u c k = lift (API.deleteProperty u c k) - -clearProperties :: (Member NotificationSubsystem r) => UserId -> ConnId -> Handler r () -clearProperties u c = lift (API.clearProperties u c) - -getProperty :: UserId -> Public.PropertyKey -> Handler r (Maybe Public.RawPropertyValue) -getProperty u k = lift . wrapClient $ API.lookupProperty u k - -listPropertyKeys :: UserId -> Handler r [Public.PropertyKey] -listPropertyKeys u = lift $ wrapClient (API.lookupPropertyKeys u) - -listPropertyKeysAndValues :: UserId -> Handler r Public.PropertyKeysAndValues -listPropertyKeysAndValues u = do - keysAndVals <- fmap Map.fromList . lift $ wrapClient (API.lookupPropertyKeysAndValues u) - Public.PropertyKeysAndValues <$> traverse parseStoredPropertyValue keysAndVals +browseTeamHandler :: + (Member UserSubsystem r) => + UserId -> + TeamId -> + Maybe Text -> + Maybe Public.RoleFilter -> + Maybe Public.TeamUserSearchSortBy -> + Maybe Public.TeamUserSearchSortOrder -> + Maybe (Range 1 500 Int) -> + Maybe Public.PagingState -> + Handler r (Public.SearchResult Public.TeamContact) +browseTeamHandler uid tid mQuery mRoleFilter mTeamUserSearchSortBy mTeamUserSearchSortOrder mMaxResults mPagingState = do + let browseTeamFilters = BrowseTeamFilters tid mQuery mRoleFilter mTeamUserSearchSortBy mTeamUserSearchSortOrder + lift . liftSem $ User.browseTeam uid browseTeamFilters mMaxResults mPagingState + +setPropertyH :: (Member PropertySubsystem r) => UserId -> ConnId -> Public.PropertyKey -> Public.RawPropertyValue -> Handler r () +setPropertyH u c key raw = lift . liftSem $ setProperty u c key raw + +deletePropertyH :: (Member PropertySubsystem r) => UserId -> ConnId -> Public.PropertyKey -> Handler r () +deletePropertyH u c k = lift . liftSem $ deleteProperty u c k + +clearPropertiesH :: (Member PropertySubsystem r) => UserId -> ConnId -> Handler r () +clearPropertiesH u c = lift . liftSem $ clearProperties u c + +getPropertyH :: (Member PropertySubsystem r) => UserId -> Public.PropertyKey -> Handler r (Maybe Public.RawPropertyValue) +getPropertyH u k = lift . liftSem $ lookupProperty u k + +listPropertyKeysH :: (Member PropertySubsystem r) => UserId -> Handler r [Public.PropertyKey] +listPropertyKeysH u = lift . liftSem $ getPropertyKeys u + +listPropertyKeysAndValuesH :: (Member PropertySubsystem r) => UserId -> Handler r Public.PropertyKeysAndValues +listPropertyKeysAndValuesH u = lift . liftSem $ getAllProperties u getPrekeyUnqualifiedH :: (Member DeleteQueue r) => @@ -569,7 +605,7 @@ getMultiUserPrekeyBundleUnqualifiedH :: Public.UserClients -> Handler r Public.UserClientPrekeyMap getMultiUserPrekeyBundleUnqualifiedH zusr userClients = do - maxSize <- fromIntegral . setMaxConvSize <$> view settings + maxSize <- fromIntegral <$> asks (.settings.maxConvSize) when (Map.size (Public.userClients userClients) > maxSize) $ throwStd (errorToWai @'E.TooManyClients) API.claimLocalMultiPrekeyBundles (ProtectedUser zusr) userClients !>> clientError @@ -579,7 +615,7 @@ getMultiUserPrekeyBundleHInternal :: Public.QualifiedUserClients -> m () getMultiUserPrekeyBundleHInternal qualUserClients = do - maxSize <- fromIntegral . setMaxConvSize <$> view settings + maxSize <- fromIntegral <$> asks (.settings.maxConvSize) let Sum (size :: Int) = Map.foldMapWithKey (\_ v -> Sum . Map.size $ v) @@ -611,29 +647,29 @@ getMultiUserPrekeyBundleH zusr qualUserClients = do addClient :: ( Member GalleyAPIAccess r, - Member (Embed HttpClientIO) r, Member DeleteQueue r, Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, - Member VerificationCodeSubsystem r + Member AuthenticationSubsystem r, + Member VerificationCodeSubsystem r, + Member Events r, + Member UserSubsystem r ) => - UserId -> + Local UserId -> ConnId -> Public.NewClient -> Handler r Public.Client -addClient usr con new = do +addClient lusr con new = do -- Users can't add legal hold clients when (Public.newClientType new == Public.LegalHoldClientType) $ throwE (clientError ClientLegalHoldCannotBeAdded) - API.addClient usr (Just con) new + API.addClient lusr (Just con) new !>> clientError deleteClient :: - (Member DeleteQueue r) => + ( Member AuthenticationSubsystem r, + Member DeleteQueue r + ) => UserId -> ConnId -> ClientId -> @@ -683,21 +719,27 @@ getClientCapabilities uid cid = do mclient <- lift (API.lookupLocalClient uid cid) maybe (throwStd (errorToWai @'E.ClientNotFound)) (pure . Public.clientCapabilities) mclient -getRichInfo :: UserId -> UserId -> Handler r Public.RichInfoAssocList -getRichInfo self user = do +getRichInfo :: + ( Member UserSubsystem r, + Member UserStore r + ) => + Local UserId -> + UserId -> + Handler r Public.RichInfoAssocList +getRichInfo lself user = do + let luser = qualifyAs lself user -- Check that both users exist and the requesting user is allowed to see rich info of the -- other user - selfUser <- - ifNothing (errorToWai @'E.UserNotFound) - =<< lift (wrapClient $ Data.lookupUser NoPendingInvitations self) - otherUser <- - ifNothing (errorToWai @'E.UserNotFound) - =<< lift (wrapClient $ Data.lookupUser NoPendingInvitations user) + let fetch luid = + ifNothing (errorToWai @'E.UserNotFound) + =<< lift (liftSem $ User.getLocalAccountBy NoPendingInvitations luid) + selfUser <- fetch lself + otherUser <- fetch luser case (Public.userTeam selfUser, Public.userTeam otherUser) of (Just t1, Just t2) | t1 == t2 -> pure () _ -> throwStd insufficientTeamPermissions -- Query rich info - wrapClientE $ fromMaybe mempty <$> API.lookupRichInfo user + lift $ liftSem $ fold <$> UserStore.getRichInfo (tUnqualified luser) getSupportedProtocols :: (Member UserSubsystem r) => @@ -714,7 +756,7 @@ getClientPrekeys usr clt = lift (wrapClient $ API.lookupPrekeyIds usr clt) newNonce :: UserId -> ClientId -> (Handler r) (Nonce, CacheControl) newNonce uid cid = do - ttl <- setNonceTtlSecs <$> view settings + ttl <- nonceTtlSecs <$> asks (.settings) nonce <- randomNonce lift $ wrapClient $ Nonce.insertNonce ttl uid (Id.clientToText cid) nonce pure (nonce, NoStore) @@ -737,19 +779,38 @@ createAccessToken method luid cid proof = do let link = safeLink (Proxy @api) (Proxy @endpoint) cid API.createAccessToken luid cid method link proof !>> certEnrollmentError +upgradePersonalToTeam :: + ( Member (ConnectionStore InternalPaging) r, + Member (Embed HttpClientIO) r, + Member GalleyAPIAccess r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member NotificationSubsystem r, + Member TinyLog r, + Member UserSubsystem r, + Member UserStore r + ) => + Local UserId -> + Public.BindingNewTeamUser -> + Handler r (Either Public.UpgradePersonalToTeamError Public.CreateUserTeam) +upgradePersonalToTeam luid bNewTeam = + lift . runExceptT $ + API.upgradePersonalToTeam luid bNewTeam + -- | docs/reference/user/registration.md {#RefRegistration} createUser :: - ( Member BlacklistStore r, + ( Member BlockListStore r, Member GalleyAPIAccess r, + Member InvitationStore r, Member (UserPendingActivationStore p) r, + Member (Input (Local ())) r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member UserKeyStore r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, + Member Events r, + Member UserSubsystem r, + Member PasswordResetCodeStore r, + Member HashPassword r, Member EmailSending r ) => Public.NewUserPublic -> @@ -759,10 +820,6 @@ createUser (Public.NewUserPublic new) = lift . runExceptT $ do for_ (Public.newUserEmail new) $ mapExceptT wrapHttp . checkAllowlistWithError RegisterErrorAllowlistError - -- prevent registration with a phone number - when (isJust (Public.newUserPhone new)) $ - throwE Public.RegisterErrorInvalidPhone - result <- API.createUser new let acc = createdAccount result @@ -770,52 +827,51 @@ createUser (Public.NewUserPublic new) = lift . runExceptT $ do let epair = (,) <$> (activationKey <$> eac) <*> (activationCode <$> eac) let newUserLabel = Public.newUserLabel new let newUserTeam = Public.newUserTeam new - let usr = accountUser acc let context = let invitationCode = case Public.newUserTeam new of (Just (Public.NewTeamMember code)) -> Just code _ -> Nothing in ( logFunction "Brig.API.Public.createUser" - . logUser (Public.userId usr) - . maybe id logHandle (Public.userHandle usr) - . maybe id logTeam (Public.userTeam usr) - . maybe id logEmail (Public.userEmail usr) + . logUser (Public.userId acc) + . maybe id logHandle (Public.userHandle acc) + . maybe id logTeam (Public.userTeam acc) + . maybe id logEmail (Public.userEmail acc) . maybe id logInvitationCode invitationCode ) lift . Log.info $ context . Log.msg @Text "Sucessfully created user" - let Public.User {userLocale, userDisplayName} = usr - userEmail = Public.userEmail usr - userId = Public.userId usr + let Public.User {userLocale, userDisplayName} = acc + userEmail = Public.userEmail acc + userId = Public.userId acc lift $ do for_ (liftM2 (,) userEmail epair) $ \(e, p) -> sendActivationEmail e userDisplayName p (Just userLocale) newUserTeam for_ (liftM3 (,,) userEmail (createdUserTeam result) newUserTeam) $ \(e, ct, ut) -> sendWelcomeEmail e ct ut (Just userLocale) cok <- - Auth.toWebCookie =<< case acc of - UserAccount _ Public.Ephemeral -> + Auth.toWebCookie =<< case userStatus acc of + Public.Ephemeral -> lift . wrapHttpClient $ Auth.newCookie @ZAuth.User userId Nothing Public.SessionCookie newUserLabel - UserAccount _ _ -> + _ -> lift . wrapHttpClient $ Auth.newCookie @ZAuth.User userId Nothing Public.PersistentCookie newUserLabel - -- pure $ CreateUserResponse cok userId (Public.SelfProfile usr) - pure $ Public.RegisterSuccess cok (Public.SelfProfile usr) + -- pure $ CreateUserResponse cok userId (Public.SelfProfile acc) + pure $ Public.RegisterSuccess cok (Public.SelfProfile acc) where - sendActivationEmail :: (Member EmailSubsystem r) => Public.Email -> Public.Name -> ActivationPair -> Maybe Public.Locale -> Maybe Public.NewTeamUser -> (AppT r) () + sendActivationEmail :: (Member EmailSubsystem r) => Public.EmailAddress -> Public.Name -> ActivationPair -> Maybe Public.Locale -> Maybe Public.NewTeamUser -> (AppT r) () sendActivationEmail email name (key, code) locale mTeamUser | Just teamUser <- mTeamUser, Public.NewTeamCreator creator <- teamUser, - let Public.BindingNewTeamUser (Public.BindingNewTeam team) _ = creator = - liftSem $ sendTeamActivationMail email name key code locale (fromRange $ team ^. Public.newTeamName) + let Public.BindingNewTeamUser team _ = creator = + liftSem $ sendTeamActivationMail email name key code locale (fromRange $ team.newTeamName) | otherwise = liftSem $ sendActivationMail email name key code locale - sendWelcomeEmail :: (Member EmailSending r) => Public.Email -> CreateUserTeam -> Public.NewTeamUser -> Maybe Public.Locale -> (AppT r) () + sendWelcomeEmail :: (Member EmailSending r) => Public.EmailAddress -> Public.CreateUserTeam -> Public.NewTeamUser -> Maybe Public.Locale -> (AppT r) () -- NOTE: Welcome e-mails for the team creator are not dealt by brig anymore - sendWelcomeEmail e (CreateUserTeam t n) newUser l = case newUser of + sendWelcomeEmail e (Public.CreateUserTeam t n) newUser l = case newUser of Public.NewTeamCreator _ -> pure () Public.NewTeamMember _ -> @@ -948,6 +1004,7 @@ updateUser uid conn uu = do def { name = uu.uupName, pict = uu.uupPict, + textStatus = uu.uupTextStatus, assets = uu.uupAssets, accentId = uu.uupAccentId } @@ -964,17 +1021,12 @@ changePhone :: changePhone _ _ _ = pure . Just $ Public.InvalidNewPhone removePhone :: UserId -> Handler r (Maybe Public.RemoveIdentityError) -removePhone self = lift . exceptTToMaybe $ API.removePhone self +removePhone _ = (lift . pure) Nothing removeEmail :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member UserKeyStore r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, - Member UserSubsystem r + ( Member UserKeyStore r, + Member UserSubsystem r, + Member Events r ) => UserId -> Handler r (Maybe Public.RemoveIdentityError) @@ -983,7 +1035,14 @@ removeEmail self = lift . exceptTToMaybe $ API.removeEmail self checkPasswordExists :: (Member PasswordStore r) => UserId -> (Handler r) Bool checkPasswordExists = fmap isJust . lift . liftSem . lookupHashedPassword -changePassword :: (Member PasswordStore r, Member UserStore r) => UserId -> Public.PasswordChange -> (Handler r) (Maybe Public.ChangePasswordError) +changePassword :: + ( Member PasswordStore r, + Member UserStore r, + Member HashPassword r + ) => + UserId -> + Public.PasswordChange -> + (Handler r) (Maybe Public.ChangePasswordError) changePassword u cp = lift . exceptTToMaybe $ API.changePassword u cp changeLocale :: @@ -997,7 +1056,7 @@ changeLocale lusr conn l = updateUserProfile lusr (Just conn) - UserSubsystem.UpdateOriginWireClient + User.UpdateOriginWireClient def {locale = Just l.luLocale} changeSupportedProtocols :: @@ -1007,7 +1066,7 @@ changeSupportedProtocols :: Public.SupportedProtocolUpdate -> Handler r () changeSupportedProtocols u conn (Public.SupportedProtocolUpdate prots) = - lift . liftSem $ UserSubsystem.updateUserProfile u (Just conn) UpdateOriginWireClient upd + lift . liftSem $ User.updateUserProfile u (Just conn) UpdateOriginWireClient upd where upd = def {supportedProtocols = Just prots} @@ -1015,7 +1074,7 @@ changeSupportedProtocols u conn (Public.SupportedProtocolUpdate prots) = -- *any* account.) checkHandle :: (Member UserSubsystem r) => UserId -> Text -> Handler r () checkHandle _uid hndl = - lift (liftSem $ UserSubsystem.checkHandle hndl) >>= \case + lift (liftSem $ User.checkHandle hndl) >>= \case API.CheckHandleFound -> pure () API.CheckHandleNotFound -> throwStd (errorToWai @'E.HandleNotFound) @@ -1044,7 +1103,7 @@ getHandleInfoUnqualifiedH self handle = do changeHandle :: (Member UserSubsystem r) => Local UserId -> ConnId -> Public.HandleUpdate -> Handler r () changeHandle u conn (Public.HandleUpdate h) = lift $ liftSem do - UserSubsystem.updateHandle u (Just conn) UpdateOriginWireClient h + User.updateHandle u (Just conn) UpdateOriginWireClient h beginPasswordReset :: (Member AuthenticationSubsystem r) => @@ -1070,30 +1129,39 @@ completePasswordReset req = do -- docs/reference/user/activation.md {#RefActivationRequest} -- docs/reference/user/registration.md {#RefRegistration} sendActivationCode :: - ( Member BlacklistStore r, + ( Member BlockListStore r, Member EmailSubsystem r, Member GalleyAPIAccess r, - Member UserKeyStore r + Member UserKeyStore r, + Member ActivationCodeStore r ) => Public.SendActivationCode -> Handler r () -sendActivationCode Public.SendActivationCode {..} = do - email <- case saUserKey of - Left email -> pure email - Right _ -> throwStd (errorToWai @'E.InvalidPhone) +sendActivationCode ac = do + let email = ac.emailKey customerExtensionCheckBlockedDomains email checkAllowlist email - API.sendActivationCode email saLocale saCall !>> sendActCodeError + API.sendActivationCode email (ac.locale) !>> sendActCodeError + +searchUsersHandler :: + (Member UserSubsystem r) => + Local UserId -> + Text -> + Maybe Domain -> + Maybe (Range 1 500 Int32) -> + Handler r (Public.SearchResult Public.Contact) +searchUsersHandler luid term mDomain mMaxResults = + lift . liftSem $ User.searchUsers luid term mDomain mMaxResults -- | If the user presents an email address from a blocked domain, throw an error. -- -- The tautological constraint in the type signature is added so that once we remove the -- feature, ghc will guide us here. -customerExtensionCheckBlockedDomains :: Public.Email -> (Handler r) () +customerExtensionCheckBlockedDomains :: Public.EmailAddress -> (Handler r) () customerExtensionCheckBlockedDomains email = do - mBlockedDomains <- asks (fmap domainsBlockedForRegistration . setCustomerExtensions . view settings) + mBlockedDomains <- fmap (.domainsBlockedForRegistration) <$> asks (.settings.customerExtensions) for_ mBlockedDomains $ \(DomainsBlockedForRegistration blockedDomains) -> do - case mkDomain (Public.emailDomain email) of + case mkDomain (Text.decodeUtf8 $ Public.domainPart email) of Left _ -> pure () -- if it doesn't fit the syntax of blocked domains, it is not blocked Right domain -> @@ -1106,6 +1174,7 @@ createConnectionUnqualified :: Member NotificationSubsystem r, Member TinyLog r, Member UserStore r, + Member UserSubsystem r, Member (Embed HttpClientIO) r ) => UserId -> @@ -1122,6 +1191,7 @@ createConnection :: Member GalleyAPIAccess r, Member NotificationSubsystem r, Member UserStore r, + Member UserSubsystem r, Member TinyLog r, Member (Embed HttpClientIO) r ) => @@ -1233,28 +1303,28 @@ deleteSelfUser :: Member NotificationSubsystem r, Member UserStore r, Member PasswordStore r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, - Member VerificationCodeSubsystem r + Member UserSubsystem r, + Member VerificationCodeSubsystem r, + Member PropertySubsystem r, + Member Events r ) => - UserId -> + Local UserId -> Public.DeleteUser -> (Handler r) (Maybe Code.Timeout) -deleteSelfUser u body = do - API.deleteSelfUser u (Public.deleteUserPassword body) !>> deleteUserError +deleteSelfUser lu body = do + API.deleteSelfUser lu (Public.deleteUserPassword body) !>> deleteUserError verifyDeleteUser :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member UserStore r, Member TinyLog r, - Member (Input (Local ())) r, Member UserKeyStore r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member PropertySubsystem r, + Member UserSubsystem r, + Member Events r ) => Public.VerifyDeleteUser -> Handler r () @@ -1262,10 +1332,11 @@ verifyDeleteUser body = API.verifyDeleteUser body !>> deleteUserError updateUserEmail :: forall r. - ( Member BlacklistStore r, + ( Member BlockListStore r, Member UserKeyStore r, Member GalleyAPIAccess r, - Member EmailSubsystem r + Member EmailSubsystem r, + Member UserSubsystem r ) => UserId -> UserId -> @@ -1296,11 +1367,9 @@ updateUserEmail zuserId emailOwnerId (Public.EmailUpdate email) = do activate :: ( Member GalleyAPIAccess r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r, + Member PasswordResetCodeStore r ) => Public.ActivationKey -> Public.ActivationCode -> @@ -1313,11 +1382,9 @@ activate k c = do activateKey :: ( Member GalleyAPIAccess r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member Events r, + Member UserSubsystem r, + Member PasswordResetCodeStore r ) => Public.Activate -> (Handler r) ActivationRespWithStatus @@ -1338,7 +1405,9 @@ sendVerificationCode :: forall r. ( Member GalleyAPIAccess r, Member UserKeyStore r, + Member (Input (Local ())) r, Member EmailSubsystem r, + Member UserSubsystem r, Member VerificationCodeSubsystem r ) => Public.SendVerificationCode -> @@ -1351,7 +1420,7 @@ sendVerificationCode req = do case (mbAccount, featureEnabled) of (Just account, True) -> do let gen = mk6DigitVerificationCodeGen email - timeout <- setVerificationTimeout <$> view settings + timeout <- verificationTimeout <$> asks (.settings) code <- lift . liftSem $ createCodeOverwritePrevious @@ -1359,39 +1428,40 @@ sendVerificationCode req = do (scopeFromAction action) (Retries 3) timeout - (Just $ toUUID $ Public.userId $ accountUser account) - sendMail email code.codeValue (Just $ Public.userLocale $ accountUser account) action + (Just $ toUUID $ Public.userId $ account) + sendMail email code.codeValue (Just (Public.userLocale account)) action _ -> pure () where - getAccount :: Public.Email -> (Handler r) (Maybe UserAccount) - getAccount email = lift $ do - mbUserId <- liftSem $ lookupKey $ mkEmailKey email - join <$> wrapClient (Data.lookupAccount `traverse` mbUserId) + getAccount :: Public.EmailAddress -> (Handler r) (Maybe User) + getAccount email = lift . liftSem $ do + mbUserId <- lookupKey $ mkEmailKey email + mbLUserId <- qualifyLocal' `traverse` mbUserId + join <$> User.getAccountNoFilter `traverse` mbLUserId - sendMail :: Public.Email -> Code.Value -> Maybe Public.Locale -> Public.VerificationAction -> (Handler r) () + sendMail :: Public.EmailAddress -> Code.Value -> Maybe Public.Locale -> Public.VerificationAction -> (Handler r) () sendMail email value mbLocale = lift . liftSem . \case Public.CreateScimToken -> sendCreateScimTokenVerificationMail email value mbLocale Public.Login -> sendLoginVerificationMail email value mbLocale Public.DeleteTeam -> sendTeamDeletionVerificationMail email value mbLocale - getFeatureStatus :: Maybe UserAccount -> (Handler r) Bool + getFeatureStatus :: Maybe User -> (Handler r) Bool getFeatureStatus mbAccount = do - mbStatusEnabled <- lift $ liftSem $ GalleyAPIAccess.getVerificationCodeEnabled `traverse` (Public.userTeam <$> accountUser =<< mbAccount) + mbStatusEnabled <- lift $ liftSem $ GalleyAPIAccess.getVerificationCodeEnabled `traverse` (Public.userTeam =<< mbAccount) pure $ fromMaybe False mbStatusEnabled getSystemSettings :: (Handler r) SystemSettingsPublic getSystemSettings = do - optSettings <- view settings + optSettings <- asks (.settings) pure $ SystemSettingsPublic $ - fromMaybe False (setRestrictUserCreation optSettings) + fromMaybe False optSettings.restrictUserCreation getSystemSettingsInternal :: UserId -> (Handler r) SystemSettings getSystemSettingsInternal _ = do - optSettings <- view settings - let pSettings = SystemSettingsPublic $ fromMaybe False (setRestrictUserCreation optSettings) - let iSettings = SystemSettingsInternal $ fromMaybe False (setEnableMLS optSettings) + optSettings <- asks (.settings) + let pSettings = SystemSettingsPublic $ fromMaybe False optSettings.restrictUserCreation + let iSettings = SystemSettingsInternal $ fromMaybe False optSettings.enableMLS pure $ SystemSettings pSettings iSettings -- Deprecated diff --git a/services/brig/src/Brig/API/Public/Swagger.hs b/services/brig/src/Brig/API/Public/Swagger.hs index 6db030d193f..5cca138c03a 100644 --- a/services/brig/src/Brig/API/Public/Swagger.hs +++ b/services/brig/src/Brig/API/Public/Swagger.hs @@ -54,6 +54,7 @@ type InternalEndpointsSwaggerDocsAPI = :<|> VersionedSwaggerDocsAPIBase "cargohold" :<|> VersionedSwaggerDocsAPIBase "galley" :<|> VersionedSwaggerDocsAPIBase "spar" + :<|> VersionedSwaggerDocsAPIBase "gundeck" ) type NotificationSchemasAPI = "api" :> "event-notification-schemas" :> Get '[JSON] [S.Definitions S.Schema] @@ -122,38 +123,10 @@ adjustSwaggerForFederationEndpoints service swagger = tag :: InsOrdSet.InsOrdHashSet S.TagName tag = InsOrdSet.singleton @S.TagName (T.pack service) -emptySwagger :: Servant.Server (ServiceSwaggerDocsAPIBase a) -emptySwagger = - swaggerSchemaUIServer $ - mempty @S.OpenApi - & S.info . S.description - ?~ "There is no Swagger documentation for this version. Please refer to v5 or later." +emptySwagger :: Text -> Servant.Server (ServiceSwaggerDocsAPIBase a) +emptySwagger msg = + swaggerSchemaUIServer $ mempty @S.OpenApi & S.info . S.description ?~ msg -{- FUTUREWORK(fisx): there are a few things that need to be fixed before this schema collection - is of any practical use! - -- `ToSchema` instances of team notifications are wrong. To do this right, search - schema-profunctor tutorial for bind/dispatch, and consult conversation events for - examples. - -- swagger2 doesn't handle types with the same name from different models well; it silently - drops the second definition, which is what you want only if there are no name clashes as - in our case with three types called `Event` and three types called `EventType`. We have - solved this by rendering the three event types seperately and returning each - declarations list in a super-list. For a better work-around, check - https://github.com/GetShopTV/swagger2/issues/14. - -- The different `EventData` constructors all have different json schema that partially - overlap. Our schemas only represent the union of all those constructors, rather than a - list of cases. There may be a better way even in swagger v2: - https://swagger.io/specification/v2/ (look for "polymorphism") - -- (wire cloud) expose end-point via nginz (only on staging). - -- Document how this works in - https://docs.wire.com/understand/api-client-perspective/swagger.html - -tracked in https://wearezeta.atlassian.net/browse/FS-1008 -} eventNotificationSchemas :: [S.Definitions S.Schema] eventNotificationSchemas = fst . (`S.runDeclare` mempty) <$> renderAll where diff --git a/services/brig/src/Brig/API/Types.hs b/services/brig/src/Brig/API/Types.hs index 2152214961c..5da615a530f 100644 --- a/services/brig/src/Brig/API/Types.hs +++ b/services/brig/src/Brig/API/Types.hs @@ -22,7 +22,6 @@ module Brig.API.Types Activation (..), ActivationError (..), ClientDataError (..), - PropertiesDataError (..), AuthError (..), ReAuthError (..), LegalHoldLoginError (..), @@ -33,8 +32,6 @@ where import Brig.Data.Activation (Activation (..), ActivationError (..)) import Brig.Data.Client (ClientDataError (..)) -import Brig.Data.Properties (PropertiesDataError (..)) -import Brig.Data.User (AuthError (..), ReAuthError (..)) import Brig.Types.Intra import Data.Code import Data.Id @@ -45,6 +42,7 @@ import Imports import Network.Wai.Utilities.Error qualified as Wai import Wire.API.Federation.Error import Wire.API.User +import Wire.AuthenticationSubsystem.Error import Wire.UserKeyStore ------------------------------------------------------------------------------- @@ -52,7 +50,7 @@ import Wire.UserKeyStore data CreateUserResult = CreateUserResult { -- | The newly created user account. - createdAccount :: !UserAccount, + createdAccount :: !User, -- | Activation data for the registered email address, if any. createdEmailActivation :: !(Maybe Activation), -- | Info of a team just created/joined @@ -60,24 +58,20 @@ data CreateUserResult = CreateUserResult } deriving (Show) -data CreateUserTeam = CreateUserTeam - { createdTeamId :: !TeamId, - createdTeamName :: !Text - } - deriving (Show) - data ActivationResult = -- | The key/code was valid and successfully activated. ActivationSuccess !(Maybe UserIdentity) !Bool | -- | The key/code was valid but already recently activated. ActivationPass + deriving (Show) -- | Outcome of the invariants check in 'Brig.API.User.changeEmail'. data ChangeEmailResult = -- | The request was successful, user needs to verify the new email address - ChangeEmailNeedsActivation !(User, Activation, Email) + ChangeEmailNeedsActivation !(User, Activation, EmailAddress) | -- | The user asked to change the email address to the one already owned ChangeEmailIdempotent + deriving (Show) ------------------------------------------------------------------------------- -- Failures @@ -87,7 +81,7 @@ data CreateUserError | MissingIdentity | EmailActivationError ActivationError | PhoneActivationError ActivationError - | InvalidEmail Email String + | InvalidEmail EmailAddress String | InvalidPhone Phone | DuplicateUserKey EmailKey | BlacklistedUserKey EmailKey @@ -98,8 +92,8 @@ data CreateUserError data InvitationError = InviteeEmailExists UserId - | InviteInvalidEmail Email - | InviteBlacklistedEmail Email + | InviteInvalidEmail EmailAddress + | InviteBlacklistedEmail EmailAddress data ConnectionError = -- | Max. # of 'Accepted' / 'Sent' connections reached @@ -117,7 +111,7 @@ data ConnectionError | -- | An attempt at creating an invitation to a blacklisted user key. ConnectBlacklistedUserKey EmailKey | -- | An attempt at creating an invitation to an invalid email address. - ConnectInvalidEmail Email String + ConnectInvalidEmail EmailAddress String | -- | An attempt at creating an invitation to an invalid phone nbumber. ConnectInvalidPhone Phone | -- | An attempt at creating a connection with another user from the same binding team. @@ -160,9 +154,9 @@ data VerificationCodeError | VerificationCodeNoEmail data ChangeEmailError - = InvalidNewEmail !Email !String - | EmailExists !Email - | ChangeBlacklistedEmail !Email + = InvalidNewEmail !EmailAddress !String + | EmailExists !EmailAddress + | ChangeBlacklistedEmail !EmailAddress | EmailManagedByScim data SendActivationCodeError @@ -176,6 +170,9 @@ data ClientError | ClientUserNotFound !UserId | ClientLegalHoldCannotBeRemoved | ClientLegalHoldCannotBeAdded + | -- | this error is thrown if legalhold if incompatible with different features + -- for now, this is the case for MLS and federation + ClientLegalHoldIncompatible | ClientFederationError FederationError | ClientCapabilitiesCannotBeRemoved | ClientMissingLegalholdConsentOldClients diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index da5333d7088..46c3e658e30 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -20,6 +20,7 @@ -- TODO: Move to Brig.User.Account module Brig.API.User ( -- * User Accounts / Profiles + upgradePersonalToTeam, createUser, createUserSpar, createUserInviteViaScim, @@ -29,20 +30,13 @@ module Brig.API.User CheckHandleResp (..), checkHandle, lookupHandle, - changeManagedBy, changeAccountStatus, changeSingleAccountStatus, - Data.lookupAccounts, - Data.lookupAccount, - lookupAccountsByIdentity, - lookupProfilesV3, getLegalHoldStatus, Data.lookupName, Data.lookupUser, - Data.lookupRichInfo, Data.lookupRichInfoMultiUsers, removeEmail, - removePhone, revokeIdentity, deleteUserNoVerify, deleteUsersNoVerify, @@ -52,7 +46,6 @@ module Brig.API.User deleteAccount, checkHandles, isBlacklistedHandle, - Data.reauthenticate, -- * Activation sendActivationCode, @@ -75,37 +68,32 @@ module Brig.API.User where import Brig.API.Error qualified as Error -import Brig.API.Handler qualified as API (UserNotAllowedToJoinTeam (..)) import Brig.API.Types import Brig.API.Util -import Brig.App +import Brig.App as App import Brig.Data.Activation (ActivationEvent (..), activationErrorToRegisterError) import Brig.Data.Activation qualified as Data import Brig.Data.Client qualified as Data import Brig.Data.Connection (countConnections) import Brig.Data.Connection qualified as Data -import Brig.Data.Properties qualified as Data import Brig.Data.User import Brig.Data.User qualified as Data -import Brig.Effects.BlacklistStore (BlacklistStore) -import Brig.Effects.BlacklistStore qualified as BlacklistStore -import Brig.Effects.ConnectionStore (ConnectionStore) +import Brig.Effects.ConnectionStore import Brig.Effects.UserPendingActivationStore (UserPendingActivation (..), UserPendingActivationStore) import Brig.Effects.UserPendingActivationStore qualified as UserPendingActivationStore import Brig.IO.Intra qualified as Intra -import Brig.Options hiding (Timeout, internalEvents) -import Brig.Team.DB qualified as Team +import Brig.Options hiding (internalEvents) +import Brig.Team.Email import Brig.Types.Activation (ActivationPair) import Brig.Types.Intra import Brig.User.Auth.Cookie qualified as Auth -import Brig.User.Search.Index (reindex) -import Brig.User.Search.TeamSize qualified as TeamSize import Cassandra hiding (Set) import Control.Error -import Control.Lens (view, (^.)) +import Control.Lens (preview, to, (^.), _Just) import Control.Monad.Catch import Data.ByteString.Conversion import Data.Code +import Data.Coerce (coerce) import Data.Currency qualified as Currency import Data.Handle (Handle (fromHandle)) import Data.Id as Id @@ -116,12 +104,12 @@ import Data.List1 as List1 (List1, singleton) import Data.Misc import Data.Qualified import Data.Range -import Data.Time.Clock (UTCTime, addUTCTime) +import Data.Time.Clock import Data.UUID.V4 (nextRandom) import Imports import Network.Wai.Utilities import Polysemy -import Polysemy.Input (Input) +import Polysemy.Input import Polysemy.TinyLog (TinyLog) import Polysemy.TinyLog qualified as Log import Prometheus qualified as Prom @@ -130,30 +118,36 @@ import UnliftIO.Async (mapConcurrently_) import Wire.API.Connection import Wire.API.Error import Wire.API.Error.Brig qualified as E -import Wire.API.Federation.Error import Wire.API.Password import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Team hiding (newTeam) -import Wire.API.Team.Feature -import Wire.API.Team.Invitation -import Wire.API.Team.Invitation qualified as Team import Wire.API.Team.Member (legalHoldStatus) import Wire.API.Team.Role -import Wire.API.Team.Size import Wire.API.User import Wire.API.User.Activation import Wire.API.User.Client import Wire.API.User.RichInfo import Wire.API.UserEvent +import Wire.ActivationCodeStore (ActivationCodeStore) +import Wire.ActivationCodeStore qualified as ActivationCode import Wire.AuthenticationSubsystem (AuthenticationSubsystem, internalLookupPasswordResetCode) +import Wire.BlockListStore as BlockListStore import Wire.DeleteQueue import Wire.EmailSubsystem import Wire.Error +import Wire.Events (Events) +import Wire.Events qualified as Events import Wire.GalleyAPIAccess as GalleyAPIAccess +import Wire.HashPassword (HashPassword) +import Wire.HashPassword qualified as HashPassword +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.Sem.Concurrency -import Wire.Sem.Paging.Cassandra (InternalPaging) +import Wire.Sem.Paging.Cassandra import Wire.UserKeyStore import Wire.UserStore import Wire.UserSubsystem as User @@ -180,14 +174,14 @@ identityErrorToBrigError = \case IdentityErrorUserKeyExists -> StdError $ errorToWai @'E.UserKeyExists verifyUniquenessAndCheckBlacklist :: - ( Member BlacklistStore r, + ( Member BlockListStore r, Member UserKeyStore r ) => EmailKey -> ExceptT IdentityError (AppT r) () verifyUniquenessAndCheckBlacklist uk = do checkKey Nothing uk - blacklisted <- lift $ liftSem $ BlacklistStore.exists uk + blacklisted <- lift $ liftSem $ BlockListStore.exists uk when blacklisted $ throwE IdentityErrorBlacklistedEmail where checkKey u k = do @@ -198,13 +192,10 @@ verifyUniquenessAndCheckBlacklist uk = do createUserSpar :: forall r. ( Member GalleyAPIAccess r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, Member UserSubsystem r, - Member (ConnectionStore InternalPaging) r + Member HashPassword r, + Member Events r ) => NewUserSpar -> ExceptT CreateUserSparError (AppT r) CreateUserResult @@ -216,9 +207,9 @@ createUserSpar new = do -- Create account account <- lift $ do - (account, pw) <- wrapClient $ newAccount new' Nothing (Just tid) handle' + (account, pw) <- newAccount new' Nothing (Just tid) handle' - let uid = userId (accountUser account) + let uid = userId account -- FUTUREWORK: make this transactional if possible wrapClient $ Data.insertAccount account Nothing pw False @@ -226,7 +217,8 @@ createUserSpar new = do Just richInfo -> wrapClient $ Data.updateRichInfo uid richInfo Nothing -> pure () -- Nothing to do liftSem $ GalleyAPIAccess.createSelfConv uid - liftSem $ Intra.onUserEvent uid Nothing (UserCreated (accountUser account)) + liftSem $ User.internalUpdateSearchIndex uid + liftSem $ Events.generateUserEvent uid Nothing (UserCreated account) pure account @@ -234,7 +226,7 @@ createUserSpar new = do userTeam <- withExceptT CreateUserSparRegistrationError $ addUserToTeamSSO account tid (SSOIdentity ident Nothing) (newUserSparRole new) -- Set up feature flags - luid <- lift $ ensureLocal (userQualifiedId (accountUser account)) + luid <- lift $ ensureLocal (userQualifiedId account) lift $ initAccountFeatureConfig (tUnqualified luid) -- Set handle @@ -247,10 +239,10 @@ createUserSpar new = do updateHandle' luid (Just h) = liftSem $ User.updateHandle luid Nothing UpdateOriginScim (fromHandle h) - addUserToTeamSSO :: UserAccount -> TeamId -> UserIdentity -> Role -> ExceptT RegisterError (AppT r) CreateUserTeam + addUserToTeamSSO :: User -> TeamId -> UserIdentity -> Role -> ExceptT RegisterError (AppT r) CreateUserTeam addUserToTeamSSO account tid ident role = do - let uid = userId (accountUser account) - added <- lift $ liftSem $ GalleyAPIAccess.addTeamMember uid tid (Nothing, role) + let uid = userId account + added <- lift $ liftSem $ GalleyAPIAccess.addTeamMember uid tid Nothing role unless added $ throwE RegisterErrorTooManyTeamMembers lift $ do @@ -264,43 +256,96 @@ createUserSpar new = do Team.TeamName nm <- lift $ liftSem $ GalleyAPIAccess.getTeamName tid pure $ CreateUserTeam tid nm +upgradePersonalToTeam :: + forall r. + ( Member GalleyAPIAccess r, + Member UserStore r, + Member UserSubsystem r, + Member TinyLog r, + Member (Embed HttpClientIO) r, + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r + ) => + Local UserId -> + BindingNewTeamUser -> + ExceptT UpgradePersonalToTeamError (AppT r) CreateUserTeam +upgradePersonalToTeam luid bNewTeam = do + -- check that the user is not part of a team + mSelfProfile <- lift $ liftSem $ getSelfProfile luid + user <- + maybe + (throwE UpgradePersonalToTeamErrorUserNotFound) + (pure . selfUser) + mSelfProfile + when (isJust user.userTeam) $ + throwE UpgradePersonalToTeamErrorAlreadyInATeam + + lift $ do + -- generate team ID + tid <- randomId + + let uid = tUnqualified luid + createUserTeam <- do + liftSem $ GalleyAPIAccess.createTeam uid (bnuTeam bNewTeam) tid + let newTeam = bNewTeam.bnuTeam + pure $ CreateUserTeam tid (fromRange newTeam.newTeamName) + + liftSem $ updateUserTeam uid tid + liftSem $ Intra.sendUserEvent uid Nothing (teamUpdated uid tid) + initAccountFeatureConfig uid + + -- send confirmation email + for_ (userEmail user) $ \email -> do + sendPersonalUserCreatorWelcomeMail + email + tid + bNewTeam.bnuTeam.newTeamName.fromRange + (Just user.userLocale) + + pure $! createUserTeam + -- docs/reference/user/registration.md {#RefRegistration} createUser :: forall r p. - ( Member BlacklistStore r, + ( Member BlockListStore r, Member GalleyAPIAccess r, Member (UserPendingActivationStore p) r, Member UserKeyStore r, + Member UserSubsystem r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, + Member Events r, Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member PasswordResetCodeStore r, + Member HashPassword r, + Member InvitationStore r ) => NewUser -> ExceptT RegisterError (AppT r) CreateUserResult createUser new = do - email <- validateEmailAndPhone new + email <- fetchAndValidateEmail new -- get invitation and existing account (mNewTeamUser, teamInvitation, tid) <- case newUserTeam new of Just (NewTeamMember i) -> do - mbTeamInv <- findTeamInvitation (mkEmailKey <$> email) i - case mbTeamInv of - Just (inv, info, tid) -> - pure (Nothing, Just (inv, info), Just tid) - Nothing -> - pure (Nothing, Nothing, Nothing) + inv <- lift $ liftSem $ internalFindTeamInvitation (mkEmailKey <$> email) i + pure (Nothing, Just inv, Just inv.teamId) Just (NewTeamCreator t) -> do (Just t,Nothing,) <$> (Just . Id <$> liftIO nextRandom) Just (NewTeamMemberSSO tid) -> pure (Nothing, Nothing, Just tid) Nothing -> pure (Nothing, Nothing, Nothing) - let mbInv = Team.inInvitation . fst <$> teamInvitation - mbExistingAccount <- lift $ join <$> for mbInv (\(Id uuid) -> wrapClient $ Data.lookupAccount (Id uuid)) + let mbInv = (.invitationId) <$> teamInvitation + mbExistingAccount <- + lift $ + join + <$> for mbInv do + \invid -> liftSem $ do + luid :: Local UserId <- qualifyLocal' (coerce invid) + User.getLocalAccountBy WithPendingInvitations luid let (new', mbHandle) = case mbExistingAccount of Nothing -> @@ -308,37 +353,39 @@ createUser new = do Nothing ) Just existingAccount -> - let existingUser = accountUser existingAccount - mbSSOid = - case (teamInvitation, email, userManagedBy existingUser) of + let mbSSOid = + case (teamInvitation, email, existingAccount.userManagedBy, userSSOId existingAccount) of -- isJust teamInvitation And ManagedByScim implies that the -- user invitation has been generated by SCIM and there is no IdP - (Just _, Just em, ManagedByScim) -> + (Just _, _, ManagedByScim, ssoId@(Just (UserScimExternalId _))) -> + -- if the existing user has an external ID, we have to use it because it can differ from the email address + ssoId + (Just _, Just em, ManagedByScim, _) -> Just $ UserScimExternalId (fromEmail em) _ -> newUserSSOId new in ( new - { newUserManagedBy = Just (userManagedBy existingUser), + { newUserManagedBy = Just existingAccount.userManagedBy, newUserIdentity = newIdentity email mbSSOid }, - userHandle existingUser + existingAccount.userHandle ) -- Create account account <- lift $ do - (account, pw) <- wrapClient $ newAccount new' mbInv tid mbHandle + (account, pw) <- newAccount new' mbInv tid mbHandle - let uid = userId (accountUser account) + let uid = userId account liftSem $ do Log.debug $ field "user" (toByteString uid) . field "action" (val "User.createUser") Log.info $ field "user" (toByteString uid) . msg (val "Creating user") wrapClient $ Data.insertAccount account Nothing pw False liftSem $ GalleyAPIAccess.createSelfConv uid - liftSem $ Intra.onUserEvent uid Nothing (UserCreated (accountUser account)) + liftSem $ Events.generateUserEvent uid Nothing (UserCreated account) pure account - let uid = userId (accountUser account) + let uid = qUnqualified account.userQualifiedId createUserTeam <- do activatedTeam <- lift $ do @@ -346,19 +393,18 @@ createUser new = do (Just tid', Just newTeamUser) -> do liftSem $ GalleyAPIAccess.createTeam uid (bnuTeam newTeamUser) tid' let activating = isJust (newUserEmailCode new) - BindingNewTeam newTeam = newTeamUser.bnuTeam + newTeam = newTeamUser.bnuTeam pure $ if activating - then Just $ CreateUserTeam tid' (fromRange (newTeam ^. newTeamName)) + then Just $ CreateUserTeam tid' (fromRange newTeam.newTeamName) else Nothing _ -> pure Nothing joinedTeamInvite <- case teamInvitation of - Just (inv, invInfo) -> do - let em = Team.inInviteeEmail inv - acceptTeamInvitation account inv invInfo (mkEmailKey em) (EmailIdentity em) - Team.TeamName nm <- lift $ liftSem $ GalleyAPIAccess.getTeamName (Team.inTeam inv) - pure (Just $ CreateUserTeam (Team.inTeam inv) nm) + Just inv -> do + acceptInvitationToTeam account inv (mkEmailKey inv.email) (EmailIdentity inv.email) + Team.TeamName nm <- lift $ liftSem $ GalleyAPIAccess.getTeamName inv.teamId + pure (Just $ CreateUserTeam inv.teamId nm) Nothing -> pure Nothing joinedTeamSSO <- case (newUserIdentity new', tid) of @@ -378,84 +424,47 @@ createUser new = do where -- NOTE: all functions in the where block don't use any arguments of createUser - validateEmailAndPhone :: NewUser -> ExceptT RegisterError (AppT r) (Maybe Email) - validateEmailAndPhone newUser = do - -- Validate e-mail - email <- for (newUserEmail newUser) $ \e -> - either - (const $ throwE RegisterErrorInvalidEmail) - pure - (validateEmail e) - - -- Disallow registering a user with a phone number - when (isJust (newUserPhone newUser)) $ - throwE RegisterErrorInvalidPhone - + fetchAndValidateEmail :: NewUser -> ExceptT RegisterError (AppT r) (Maybe EmailAddress) + fetchAndValidateEmail newUser = do + let email = newUserEmail newUser for_ (mkEmailKey <$> email) $ \k -> verifyUniquenessAndCheckBlacklist k !>> identityErrorToRegisterError - pure email - findTeamInvitation :: Maybe EmailKey -> InvitationCode -> ExceptT RegisterError (AppT r) (Maybe (Team.Invitation, Team.InvitationInfo, TeamId)) - findTeamInvitation Nothing _ = throwE RegisterErrorMissingIdentity - findTeamInvitation (Just e) c = - lift (wrapClient $ Team.lookupInvitationInfo c) >>= \case - Just ii -> do - inv <- lift . wrapClient $ Team.lookupInvitation HideInvitationUrl (Team.iiTeam ii) (Team.iiInvId ii) - case (inv, Team.inInviteeEmail <$> inv) of - (Just invite, Just em) - | e == mkEmailKey em -> do - _ <- ensureMemberCanJoin (Team.iiTeam ii) - pure $ Just (invite, ii, Team.iiTeam ii) - _ -> throwE RegisterErrorInvalidInvitationCode - Nothing -> throwE RegisterErrorInvalidInvitationCode - - ensureMemberCanJoin :: TeamId -> ExceptT RegisterError (AppT r) () - ensureMemberCanJoin tid = do - maxSize <- fromIntegral . setMaxTeamSize <$> view settings - (TeamSize teamSize) <- TeamSize.teamSize tid - when (teamSize >= maxSize) $ - throwE RegisterErrorTooManyTeamMembers - -- FUTUREWORK: The above can easily be done/tested in the intra call. - -- Remove after the next release. - canAdd <- lift $ liftSem $ GalleyAPIAccess.checkUserCanJoinTeam tid - case canAdd of - Just e -> throwM $ API.UserNotAllowedToJoinTeam e - Nothing -> pure () - - acceptTeamInvitation :: - UserAccount -> - Team.Invitation -> - Team.InvitationInfo -> + acceptInvitationToTeam :: + User -> + StoredInvitation -> EmailKey -> UserIdentity -> ExceptT RegisterError (AppT r) () - acceptTeamInvitation account inv ii uk ident = do - let uid = userId (accountUser account) + acceptInvitationToTeam account inv uk ident = do + let uid = userId account ok <- lift $ liftSem $ claimKey uk uid unless ok $ throwE RegisterErrorUserKeyExists - let minvmeta :: (Maybe (UserId, UTCTimeMillis), Role) - minvmeta = ((,inCreatedAt inv) <$> inCreatedBy inv, Team.inRole inv) - added <- lift $ liftSem $ GalleyAPIAccess.addTeamMember uid (Team.iiTeam ii) minvmeta + let minvmeta :: Maybe (UserId, UTCTimeMillis) + minvmeta = (,inv.createdAt) <$> inv.createdBy + role :: Role + role = fromMaybe defaultRole inv.role + added <- lift $ liftSem $ GalleyAPIAccess.addTeamMember uid inv.teamId minvmeta role unless added $ throwE RegisterErrorTooManyTeamMembers lift $ do - wrapClient $ activateUser uid ident -- ('insertAccount' sets column activated to False; here it is set to True.) + -- ('insertAccount' sets column activated to False; here it is set to True.) + wrapClient $ activateUser uid ident void $ onActivated (AccountActivated account) - liftSem $ + liftSem do Log.info $ field "user" (toByteString uid) - . field "team" (toByteString $ Team.iiTeam ii) + . field "team" (toByteString $ inv.teamId) . msg (val "Accepting invitation") - liftSem $ UserPendingActivationStore.remove uid - wrapClient $ do - Team.deleteInvitation (Team.inTeam inv) (Team.inInvitation inv) + UserPendingActivationStore.remove uid + InvitationStore.deleteInvitation inv.teamId inv.invitationId - addUserToTeamSSO :: UserAccount -> TeamId -> UserIdentity -> ExceptT RegisterError (AppT r) CreateUserTeam + addUserToTeamSSO :: User -> TeamId -> UserIdentity -> ExceptT RegisterError (AppT r) CreateUserTeam addUserToTeamSSO account tid ident = do - let uid = userId (accountUser account) - added <- lift $ liftSem $ GalleyAPIAccess.addTeamMember uid tid (Nothing, defaultRole) + let uid = userId account + added <- lift $ liftSem $ GalleyAPIAccess.addTeamMember uid tid Nothing defaultRole unless added $ throwE RegisterErrorTooManyTeamMembers lift $ do @@ -470,11 +479,11 @@ createUser new = do pure $ CreateUserTeam tid nm -- Handle e-mail activation (deprecated, see #RefRegistrationNoPreverification in /docs/reference/user/registration.md) - handleEmailActivation :: Maybe Email -> UserId -> Maybe BindingNewTeamUser -> ExceptT RegisterError (AppT r) (Maybe Activation) + handleEmailActivation :: Maybe EmailAddress -> UserId -> Maybe BindingNewTeamUser -> ExceptT RegisterError (AppT r) (Maybe Activation) handleEmailActivation email uid newTeam = do fmap join . for (mkEmailKey <$> email) $ \ek -> case newUserEmailCode new of Nothing -> do - timeout <- setActivationTimeout <$> view settings + timeout <- asks (.settings.activationTimeout) edata <- lift . wrapClient $ Data.newActivation ek timeout (Just uid) lift . liftSem . Log.info $ field "user" (toByteString uid) @@ -490,32 +499,31 @@ createUser new = do initAccountFeatureConfig :: UserId -> (AppT r) () initAccountFeatureConfig uid = do - mbCciDefNew <- view (settings . getAfcConferenceCallingDefNewMaybe) - forM_ (forgetLock <$> mbCciDefNew) $ wrapClient . Data.updateFeatureConferenceCalling uid . Just + mStatus <- preview (App.settingsLens . featureFlagsLens . _Just . to conferenceCalling . to forNew . _Just) + wrapClient $ traverse_ (Data.updateFeatureConferenceCalling uid . Just) mStatus -- | 'createUser' is becoming hard to maintain, and instead of adding more case distinctions -- all over the place there, we add a new function that handles just the one new flow where -- users are invited to the team via scim. createUserInviteViaScim :: - ( Member BlacklistStore r, + ( Member BlockListStore r, Member UserKeyStore r, Member (UserPendingActivationStore p) r, Member TinyLog r ) => NewUserScimInvitation -> - ExceptT HttpError (AppT r) UserAccount -createUserInviteViaScim (NewUserScimInvitation tid uid loc name rawEmail _) = do - email <- either (const . throwE . StdError $ errorToWai @'E.InvalidEmail) pure (validateEmail rawEmail) + ExceptT HttpError (AppT r) User +createUserInviteViaScim (NewUserScimInvitation tid uid extId loc name email _) = do let emKey = mkEmailKey email verifyUniquenessAndCheckBlacklist emKey !>> identityErrorToBrigError - account <- lift . wrapClient $ newAccountInviteViaScim uid tid loc name email - lift . liftSem . Log.debug $ field "user" (toByteString . userId . accountUser $ account) . field "action" (val "User.createUserInviteViaScim") + account <- lift . wrapClient $ newAccountInviteViaScim uid extId tid loc name email + lift . liftSem . Log.debug $ field "user" (toByteString . userId $ account) . field "action" (val "User.createUserInviteViaScim") -- add the expiry table entry first! (if brig creates an account, and then crashes before -- creating the expiry table entry, gc will miss user data.) expiresAt <- do - ttl <- setTeamInvitationTimeout <$> view settings - now <- liftIO =<< view currentTime + ttl <- asks (.settings.teamInvitationTimeout) + now <- liftIO =<< asks (.currentTime) pure $ addUTCTime (realToFrac ttl) now lift . liftSem $ UserPendingActivationStore.add (UserPendingActivation uid expiresAt) @@ -530,7 +538,7 @@ createUserInviteViaScim (NewUserScimInvitation tid uid loc name rawEmail _) = do -- | docs/reference/user/registration.md {#RefRestrictRegistration}. checkRestrictedUserCreation :: NewUser -> ExceptT RegisterError (AppT r) () checkRestrictedUserCreation new = do - restrictPlease <- lift . asks $ fromMaybe False . setRestrictUserCreation . view settings + restrictPlease <- fromMaybe False <$> asks (.settings.restrictUserCreation) when ( restrictPlease && not (isNewUserTeamMember new) @@ -538,31 +546,21 @@ checkRestrictedUserCreation new = do ) $ throwE RegisterErrorUserCreationRestricted -------------------------------------------------------------------------------- --- Update ManagedBy - -changeManagedBy :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r - ) => - UserId -> - ConnId -> - ManagedByUpdate -> - (AppT r) () -changeManagedBy uid conn (ManagedByUpdate mb) = do - wrapClient $ Data.updateManagedBy uid mb - liftSem $ Intra.onUserEvent uid (Just conn) (managedByUpdate uid mb) - ------------------------------------------------------------------------------- -- Change Email -- | Call 'changeEmail' and process result: if email changes to itself, succeed, if not, send -- validation email. -changeSelfEmail :: (Member BlacklistStore r, Member UserKeyStore r, Member EmailSubsystem r) => UserId -> Email -> UpdateOriginType -> ExceptT HttpError (AppT r) ChangeEmailResponse +changeSelfEmail :: + ( Member BlockListStore r, + Member UserKeyStore r, + Member EmailSubsystem r, + Member UserSubsystem r + ) => + UserId -> + EmailAddress -> + UpdateOriginType -> + ExceptT HttpError (AppT r) ChangeEmailResponse changeSelfEmail u email allowScim = do changeEmail u email allowScim !>> Error.changeEmailError >>= \case ChangeEmailIdempotent -> @@ -570,7 +568,7 @@ changeSelfEmail u email allowScim = do ChangeEmailNeedsActivation (usr, adata, en) -> lift $ do liftSem $ sendOutEmail usr adata en wrapClient $ Data.updateEmailUnvalidated u email - wrapClient $ reindex u + liftSem $ User.internalUpdateSearchIndex u pure ChangeEmailResponseNeedsActivation where sendOutEmail usr adata en = do @@ -582,15 +580,10 @@ changeSelfEmail u email allowScim = do (Just (userLocale usr)) -- | Prepare changing the email (checking a number of invariants). -changeEmail :: (Member BlacklistStore r, Member UserKeyStore r) => UserId -> Email -> UpdateOriginType -> ExceptT ChangeEmailError (AppT r) ChangeEmailResult +changeEmail :: (Member BlockListStore r, Member UserKeyStore r) => UserId -> EmailAddress -> UpdateOriginType -> ExceptT ChangeEmailError (AppT r) ChangeEmailResult changeEmail u email updateOrigin = do - em <- - either - (throwE . InvalidNewEmail email) - pure - (validateEmail email) - let ek = mkEmailKey em - blacklisted <- lift . liftSem $ BlacklistStore.exists ek + let ek = mkEmailKey email + blacklisted <- lift . liftSem $ BlockListStore.exists ek when blacklisted $ throwE (ChangeBlacklistedEmail email) available <- lift $ liftSem $ keyAvailable ek (Just u) @@ -600,26 +593,21 @@ changeEmail u email updateOrigin = do usr <- maybe (throwM $ UserProfileNotFound u) pure =<< lift (wrapClient $ Data.lookupUser WithPendingInvitations u) case emailIdentity =<< userIdentity usr of -- The user already has an email address and the new one is exactly the same - Just current | current == em -> pure ChangeEmailIdempotent + Just current | current == email -> pure ChangeEmailIdempotent _ -> do unless (userManagedBy usr /= ManagedByScim || updateOrigin == UpdateOriginScim) $ throwE EmailManagedByScim - timeout <- setActivationTimeout <$> view settings + timeout <- asks (.settings.activationTimeout) act <- lift . wrapClient $ Data.newActivation ek timeout (Just u) - pure $ ChangeEmailNeedsActivation (usr, act, em) + pure $ ChangeEmailNeedsActivation (usr, act, email) ------------------------------------------------------------------------------- -- Remove Email removeEmail :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member UserKeyStore r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, - Member UserSubsystem r + ( Member UserKeyStore r, + Member UserSubsystem r, + Member Events r ) => UserId -> ExceptT RemoveIdentityError (AppT r) () @@ -629,17 +617,11 @@ removeEmail uid = do Just (SSOIdentity (UserSSOId _) (Just e)) -> lift $ do liftSem $ deleteKey $ mkEmailKey e wrapClient $ Data.deleteEmail uid - liftSem $ Intra.onUserEvent uid Nothing (emailRemoved uid e) + liftSem $ Events.generateUserEvent uid Nothing (emailRemoved uid e) + liftSem $ User.internalUpdateSearchIndex uid Just _ -> throwE LastIdentity Nothing -> throwE NoIdentity -------------------------------------------------------------------------------- --- Remove Phone - --- | Phones are not supported any longer. -removePhone :: UserId -> ExceptT RemoveIdentityError (AppT r) () -removePhone _uid = pure () - ------------------------------------------------------------------------------- -- Forcefully revoke a verified identity @@ -649,7 +631,7 @@ revokeIdentity :: ( Member UserSubsystem r, Member UserKeyStore r ) => - Email -> + EmailAddress -> AppT r () revokeIdentity key = do mu <- liftSem . lookupKey . mkEmailKey $ key @@ -663,12 +645,9 @@ revokeIdentity key = do changeAccountStatus :: forall r. ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member (Concurrency 'Unsafe) r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r ) => List1 UserId -> AccountStatus -> @@ -683,15 +662,12 @@ changeAccountStatus usrs status = do Sem r () update ev u = do embed $ Data.updateStatus u status - Intra.onUserEvent u Nothing (ev u) + User.internalUpdateSearchIndex u + Events.generateUserEvent u Nothing (ev u) changeSingleAccountStatus :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + ( Member UserSubsystem r, + Member Events r ) => UserId -> AccountStatus -> @@ -701,7 +677,8 @@ changeSingleAccountStatus uid status = do ev <- mkUserEvent (List1.singleton uid) status lift $ do wrapClient $ Data.updateStatus uid status - liftSem $ Intra.onUserEvent uid Nothing (ev uid) + liftSem $ User.internalUpdateSearchIndex uid + liftSem $ Events.generateUserEvent uid Nothing (ev uid) mkUserEvent :: (Traversable t) => t UserId -> AccountStatus -> ExceptT AccountStatusError (AppT r) (UserId -> UserEvent) mkUserEvent usrs status = @@ -720,11 +697,9 @@ mkUserEvent usrs status = activate :: ( Member GalleyAPIAccess r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member Events r, + Member PasswordResetCodeStore r, + Member UserSubsystem r ) => ActivationTarget -> ActivationCode -> @@ -736,11 +711,9 @@ activate tgt code usr = activateWithCurrency tgt code usr Nothing activateWithCurrency :: ( Member GalleyAPIAccess r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member Events r, + Member PasswordResetCodeStore r, + Member UserSubsystem r ) => ActivationTarget -> ActivationCode -> @@ -756,7 +729,7 @@ activateWithCurrency tgt code usr cur = do field "activation.key" (toByteString key) . field "activation.code" (toByteString code) . msg (val "Activating") - event <- wrapClientE $ Data.activateKey key code usr + event <- Data.activateKey key code usr case event of Nothing -> pure ActivationPass Just e -> do @@ -783,50 +756,45 @@ preverify tgt code = do onActivated :: ( Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r ) => ActivationEvent -> AppT r (UserId, Maybe UserIdentity, Bool) onActivated (AccountActivated account) = liftSem $ do - let uid = userId (accountUser account) + let uid = userId account Log.debug $ field "user" (toByteString uid) . field "action" (val "User.onActivated") Log.info $ field "user" (toByteString uid) . msg (val "User activated") - Intra.onUserEvent uid Nothing $ UserActivated (accountUser account) - pure (uid, userIdentity (accountUser account), True) + User.internalUpdateSearchIndex uid + Events.generateUserEvent uid Nothing $ UserActivated account + pure (uid, userIdentity account, True) onActivated (EmailActivated uid email) = do - liftSem $ Intra.onUserEvent uid Nothing (emailUpdated uid email) + liftSem $ User.internalUpdateSearchIndex uid + liftSem $ Events.generateUserEvent uid Nothing (emailUpdated uid email) wrapHttpClient $ Data.deleteEmailUnvalidated uid pure (uid, Just (EmailIdentity email), False) -- docs/reference/user/activation.md {#RefActivationRequest} sendActivationCode :: - ( Member BlacklistStore r, + ( Member BlockListStore r, Member EmailSubsystem r, Member GalleyAPIAccess r, + Member ActivationCodeStore r, Member UserKeyStore r ) => - Email -> + EmailAddress -> Maybe Locale -> - Bool -> ExceptT SendActivationCodeError (AppT r) () -sendActivationCode email loc _call = do - ek <- - either - (const . throwE . InvalidRecipient $ mkEmailKey email) - (pure . mkEmailKey) - (validateEmail email) - exists <- lift $ liftSem $ isJust <$> lookupKey ek - when exists $ +sendActivationCode email loc = do + let ek = mkEmailKey email + doesExist <- lift $ liftSem $ isJust <$> lookupKey ek + when doesExist $ throwE $ UserKeyInUse ek - blacklisted <- lift . liftSem $ BlacklistStore.exists ek + blacklisted <- lift . liftSem $ BlockListStore.exists ek when blacklisted $ throwE (ActivationBlacklistedUserKey ek) - uc <- lift . wrapClient $ Data.lookupActivationCode ek + uc <- lift . liftSem $ ActivationCode.lookupActivationCode ek case uc of Nothing -> sendVerificationEmail ek Nothing -- Fresh code request, no user Just (Nothing, c) -> sendVerificationEmail ek (Just c) -- Re-requesting existing code @@ -834,7 +802,7 @@ sendActivationCode email loc _call = do where notFound = throwM . UserDisplayNameNotFound mkPair k c u = do - timeout <- setActivationTimeout <$> view settings + timeout <- asks (.settings.activationTimeout) case c of Just c' -> liftIO $ (,c') <$> Data.mkActivationKey k Nothing -> lift $ do @@ -869,27 +837,28 @@ sendActivationCode email loc _call = do mkActivationKey :: (MonadClient m, MonadReader Env m) => ActivationTarget -> ExceptT ActivationError m ActivationKey mkActivationKey (ActivateKey k) = pure k -mkActivationKey (ActivateEmail e) = do - ek <- - either - (throwE . InvalidActivationEmail e) - (pure . mkEmailKey) - (validateEmail e) - liftIO $ Data.mkActivationKey ek -mkActivationKey (ActivatePhone p) = throwE $ InvalidActivationPhone p +mkActivationKey (ActivateEmail e) = + liftIO $ Data.mkActivationKey (mkEmailKey e) ------------------------------------------------------------------------------- -- Password Management -changePassword :: (Member PasswordStore r, Member UserStore r) => UserId -> PasswordChange -> ExceptT ChangePasswordError (AppT r) () +changePassword :: + ( Member PasswordStore r, + Member UserStore r, + Member HashPassword r + ) => + UserId -> + PasswordChange -> + ExceptT ChangePasswordError (AppT r) () changePassword uid cp = do activated <- lift $ liftSem $ isActivated uid unless activated $ throwE ChangePasswordNoIdentity currpw <- lift $ liftSem $ lookupHashedPassword uid - let newpw = cpNewPassword cp - hashedNewPw <- mkSafePasswordScrypt newpw - case (currpw, cpOldPassword cp) of + let newpw = cp.newPassword + hashedNewPw <- lift . liftSem $ HashPassword.hashPassword8 newpw + case (currpw, cp.oldPassword) of (Nothing, _) -> lift . liftSem $ upsertHashedPassword uid hashedNewPw (Just _, Nothing) -> throwE InvalidCurrentPassword (Just pw, Just pw') -> do @@ -904,7 +873,7 @@ changePassword uid cp = do -- User Deletion -- | Initiate validation of a user's delete request. Called via @delete /self@. Users with an --- 'UserSSOId' can still do this if they also have an 'Email', 'Phone', and/or password. Otherwise, +-- 'UserSSOId' can still do this if they also have an 'Email' and/or password. Otherwise, -- the team admin has to delete them via the team console on galley. -- -- Owners are not allowed to delete themselves. Instead, they must ask a fellow owner to @@ -921,38 +890,38 @@ deleteSelfUser :: Member (Embed HttpClientIO) r, Member UserKeyStore r, Member NotificationSubsystem r, - Member (Input (Local ())) r, Member PasswordStore r, Member UserStore r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member Events r, + Member UserSubsystem r, + Member PropertySubsystem r ) => - UserId -> + Local UserId -> Maybe PlainTextPassword6 -> ExceptT DeleteUserError (AppT r) (Maybe Timeout) -deleteSelfUser uid pwd = do - account <- lift . wrapClient $ Data.lookupAccount uid +deleteSelfUser luid@(tUnqualified -> uid) pwd = do + account <- lift . liftSem $ User.getAccountNoFilter luid case account of Nothing -> throwE DeleteUserInvalid - Just a -> case accountStatus a of + Just a -> case userStatus a of Deleted -> pure Nothing Suspended -> ensureNotOwner a >> go a Active -> ensureNotOwner a >> go a Ephemeral -> go a PendingInvitation -> go a where - ensureNotOwner :: UserAccount -> ExceptT DeleteUserError (AppT r) () + ensureNotOwner :: User -> ExceptT DeleteUserError (AppT r) () ensureNotOwner acc = do - case userTeam $ accountUser acc of + case userTeam acc of Nothing -> pure () Just tid -> do isOwner <- lift $ liftSem $ GalleyAPIAccess.memberIsTeamOwner tid uid when isOwner $ throwE DeleteUserOwnerDeletingSelf go a = maybe (byIdentity a) (byPassword a) pwd - byIdentity a = case emailIdentity =<< userIdentity (accountUser a) of - Just emailOrPhone -> sendCode a emailOrPhone + byIdentity a = case emailIdentity =<< userIdentity a of + Just email -> sendCode a email Nothing -> case pwd of Just _ -> throwE DeleteUserMissingPassword Nothing -> lift . liftSem $ deleteAccount a >> pure Nothing @@ -978,8 +947,8 @@ deleteSelfUser uid pwd = do . msg (val "Sending verification code for account deletion") let k = VerificationCode.codeKey c let v = VerificationCode.codeValue c - let l = userLocale (accountUser a) - let n = userDisplayName (accountUser a) + let l = userLocale a + let n = userDisplayName a lift (liftSem $ sendAccountDeletionEmail target n k v l) `onException` lift (liftSem $ deleteCode k VerificationCode.AccountDeletion) pure $! Just $! VerificationCode.codeTTL c @@ -994,11 +963,11 @@ verifyDeleteUser :: Member NotificationSubsystem r, Member UserKeyStore r, Member TinyLog r, - Member (Input (Local ())) r, Member UserStore r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member Events r, + Member UserSubsystem r, + Member PropertySubsystem r ) => VerifyDeleteUser -> ExceptT DeleteUserError (AppT r) () @@ -1007,7 +976,8 @@ verifyDeleteUser d = do let code = verifyDeleteUserCode d c <- lift . liftSem $ verifyCode key VerificationCode.AccountDeletion code a <- maybe (throwE DeleteUserInvalidCode) pure (VerificationCode.codeAccount =<< c) - account <- lift . wrapClient $ Data.lookupAccount (Id a) + luid <- qualifyLocal $ Id a + account <- lift . liftSem $ User.getAccountNoFilter luid for_ account $ lift . liftSem . deleteAccount lift . liftSem $ deleteCode key VerificationCode.AccountDeletion @@ -1021,21 +991,20 @@ ensureAccountDeleted :: Member NotificationSubsystem r, Member TinyLog r, Member UserKeyStore r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, - Member UserStore r + Member UserStore r, + Member Events r, + Member UserSubsystem r, + Member PropertySubsystem r ) => - UserId -> + Local UserId -> AppT r DeleteUserResult -ensureAccountDeleted uid = do - mbAcc <- wrapClient $ lookupAccount uid +ensureAccountDeleted luid@(tUnqualified -> uid) = do + mbAcc <- liftSem $ User.getAccountNoFilter luid case mbAcc of Nothing -> pure NoUser Just acc -> do - probs <- wrapClient $ Data.lookupPropertyKeysAndValues uid + probs <- liftSem $ getPropertyKeys uid - let accIsDeleted = accountStatus acc == Deleted clients <- wrapClient $ Data.lookupClients uid localUid <- qualifyLocal uid @@ -1043,7 +1012,7 @@ ensureAccountDeleted uid = do cookies <- wrapClient $ Auth.listCookies uid [] if notNull probs - || not accIsDeleted + || not (userDeleted acc) || notNull clients || conCount > 0 || notNull cookies @@ -1064,34 +1033,35 @@ ensureAccountDeleted uid = do -- -- FUTUREWORK(mangoiv): this uses 'UserStore', hence it must be moved to 'UserSubsystem' -- as an effet operation --- FUTUREWORK: this does not need the whole UserAccount structure, only the User. +-- FUTUREWORK: this does not need the whole User structure, only the User. deleteAccount :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member UserKeyStore r, Member TinyLog r, - Member (Input (Local ())) r, Member UserStore r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member PropertySubsystem r, + Member UserSubsystem r, + Member Events r ) => - UserAccount -> + User -> Sem r () -deleteAccount (accountUser -> user) = do +deleteAccount user = do let uid = userId user Log.info $ field "user" (toByteString uid) . msg (val "Deleting account") do -- Free unique keys for_ (userEmail user) $ deleteKeyForUser uid . mkEmailKey - embed $ Data.clearProperties uid + PropertySubsystem.onUserDeleted uid deleteUser user Intra.rmUser uid (userAssets user) embed $ Data.lookupClients uid >>= mapM_ (Data.rmClient uid . clientId) luid <- embed $ qualifyLocal uid - Intra.onUserEvent uid Nothing (UserDeleted (tUntagged luid)) + User.internalUpdateSearchIndex uid + Events.generateUserEvent uid Nothing (UserDeleted (tUntagged luid)) embed do -- Note: Connections can only be deleted afterwards, since -- they need to be notified. @@ -1102,19 +1072,21 @@ deleteAccount (accountUser -> user) = do -- Lookups lookupActivationCode :: - (MonadClient m) => - Email -> - m (Maybe ActivationPair) + ( Member ActivationCodeStore r, + Member (Embed IO) r + ) => + EmailAddress -> + Sem r (Maybe ActivationPair) lookupActivationCode email = do let uk = mkEmailKey email k <- liftIO $ Data.mkActivationKey uk - c <- fmap snd <$> Data.lookupActivationCode uk + c <- fmap snd <$> ActivationCode.lookupActivationCode uk pure $ (k,) <$> c lookupPasswordResetCode :: ( Member AuthenticationSubsystem r ) => - Email -> + EmailAddress -> (AppT r) (Maybe PasswordResetPair) lookupPasswordResetCode = liftSem . internalLookupPasswordResetCode . mkEmailKey @@ -1155,21 +1127,16 @@ enqueueMultiDeleteCallsCounter = Prom.metricHelp = "Number of users enqueued to be deleted" } --- | Similar to lookupProfiles except it returns all results and all errors --- allowing for partial success. -lookupProfilesV3 :: - (Member UserSubsystem r) => - Local UserId -> - -- | The users ('others') for which to obtain the profiles. - [Qualified UserId] -> - Sem r ([(Qualified UserId, FederationError)], [UserProfile]) -lookupProfilesV3 self others = getUserProfilesWithErrors self others - getLegalHoldStatus :: - (Member GalleyAPIAccess r) => - UserId -> + ( Member GalleyAPIAccess r, + Member UserSubsystem r + ) => + Local UserId -> AppT r (Maybe UserLegalHoldStatus) -getLegalHoldStatus uid = traverse (liftSem . getLegalHoldStatus' . accountUser) =<< wrapHttpClient (lookupAccount uid) +getLegalHoldStatus uid = + liftSem $ + traverse getLegalHoldStatus' + =<< User.getLocalAccountBy NoPendingInvitations uid getLegalHoldStatus' :: (Member GalleyAPIAccess r) => @@ -1182,33 +1149,17 @@ getLegalHoldStatus' user = teamMember <- GalleyAPIAccess.getTeamMember (userId user) tid pure $ maybe defUserLegalHoldStatus (^. legalHoldStatus) teamMember --- | Find user accounts for a given identity, both activated and those --- currently pending activation. -lookupAccountsByIdentity :: - (Member UserKeyStore r) => - Email -> - Bool -> - AppT r [UserAccount] -lookupAccountsByIdentity email includePendingInvitations = do - let uk = mkEmailKey email - activeUid <- liftSem $ lookupKey uk - uidFromKey <- (>>= fst) <$> wrapClient (Data.lookupActivationCode uk) - result <- wrapClient $ Data.lookupAccounts (nub $ catMaybes [activeUid, uidFromKey]) - if includePendingInvitations - then pure result - else pure $ filter ((/= PendingInvitation) . accountStatus) result - -isBlacklisted :: (Member BlacklistStore r) => Email -> AppT r Bool +isBlacklisted :: (Member BlockListStore r) => EmailAddress -> AppT r Bool isBlacklisted email = do let uk = mkEmailKey email - liftSem $ BlacklistStore.exists uk + liftSem $ BlockListStore.exists uk -blacklistInsert :: (Member BlacklistStore r) => Email -> AppT r () +blacklistInsert :: (Member BlockListStore r) => EmailAddress -> AppT r () blacklistInsert email = do let uk = mkEmailKey email - liftSem $ BlacklistStore.insert uk + liftSem $ BlockListStore.insert uk -blacklistDelete :: (Member BlacklistStore r) => Email -> AppT r () +blacklistDelete :: (Member BlockListStore r) => EmailAddress -> AppT r () blacklistDelete email = do let uk = mkEmailKey email - liftSem $ BlacklistStore.delete uk + liftSem $ BlockListStore.delete uk diff --git a/services/brig/src/Brig/API/Util.hs b/services/brig/src/Brig/API/Util.hs index 6a1d1d532d7..81b1e79d754 100644 --- a/services/brig/src/Brig/API/Util.hs +++ b/services/brig/src/Brig/API/Util.hs @@ -17,54 +17,34 @@ module Brig.API.Util ( fetchUserIdentity, - lookupProfilesMaybeFilterSameTeamOnly, logInvitationCode, - validateHandle, logEmail, - traverseConcurrentlyAppT, traverseConcurrentlySem, traverseConcurrentlyWithErrors, - traverseConcurrentlyWithErrorsSem, - traverseConcurrentlyWithErrorsAppT, exceptTToMaybe, ensureLocal, ) where -import Brig.API.Error -import Brig.API.Handler import Brig.API.Types import Brig.App -import Brig.Data.User qualified as Data import Control.Monad.Catch (throwM) import Control.Monad.Trans.Except import Data.Bifunctor -import Data.Handle (Handle, parseHandle) import Data.Id import Data.Maybe import Data.Text qualified as T import Data.Text.Ascii (AsciiText (toText)) import Imports import Polysemy -import Polysemy.Error qualified as E import System.Logger (Msg) import System.Logger qualified as Log import UnliftIO.Async import UnliftIO.Exception (throwIO, try) import Util.Logging (sha256String) -import Wire.API.Error -import Wire.API.Error.Brig import Wire.API.User -import Wire.Sem.Concurrency qualified as C import Wire.UserSubsystem -lookupProfilesMaybeFilterSameTeamOnly :: UserId -> [UserProfile] -> (Handler r) [UserProfile] -lookupProfilesMaybeFilterSameTeamOnly self us = do - selfTeam <- lift $ wrapClient $ Data.lookupUserTeam self - pure $ case selfTeam of - Just team -> filter (\x -> profileTeam x == Just team) us - Nothing -> us - fetchUserIdentity :: (Member UserSubsystem r) => UserId -> AppT r (Maybe UserIdentity) fetchUserIdentity uid = do luid <- qualifyLocal uid @@ -73,31 +53,13 @@ fetchUserIdentity uid = do (throwM $ UserProfileNotFound uid) (pure . userIdentity . selfUser) -validateHandle :: Text -> (Handler r) Handle -validateHandle = maybe (throwStd (errorToWai @'InvalidHandle)) pure . parseHandle - -logEmail :: Email -> (Msg -> Msg) +logEmail :: EmailAddress -> (Msg -> Msg) logEmail email = Log.field "email_sha256" (sha256String . T.pack . show $ email) logInvitationCode :: InvitationCode -> (Msg -> Msg) logInvitationCode code = Log.field "invitation_code" (toText $ fromInvitationCode code) --- | Traverse concurrently and collect errors. -traverseConcurrentlyAppT :: - (Traversable t, Member (C.Concurrency 'C.Unsafe) r) => - (a -> ExceptT e (AppT r) b) -> - t a -> - AppT r [Either (a, e) b] -traverseConcurrentlyAppT f t = do - env <- temporaryGetEnv - AppT $ - lift $ - C.unsafePooledMapConcurrentlyN - 8 - (\a -> first (a,) <$> lowerAppT env (runExceptT $ f a)) - t - -- | Traverse concurrently and fail on first error. traverseConcurrentlyWithErrors :: (Traversable t, Exception e, MonadUnliftIO m) => @@ -119,35 +81,5 @@ traverseConcurrentlySem :: traverseConcurrentlySem f = pooledMapConcurrentlyN 8 $ \a -> first (a,) <$> runExceptT (f a) --- | Traverse concurrently and fail on first error. -traverseConcurrentlyWithErrorsSem :: - forall t e a r b. - (Traversable t, Member (C.Concurrency 'C.Unsafe) r) => - (a -> ExceptT e (Sem r) b) -> - t a -> - ExceptT e (Sem r) [b] -traverseConcurrentlyWithErrorsSem f = - ExceptT - . E.runError - . ( traverse (either E.throw pure) - <=< C.unsafePooledMapConcurrentlyN 8 (raise . runExceptT . f) - ) - -traverseConcurrentlyWithErrorsAppT :: - forall t e a r b. - (Traversable t, Member (C.Concurrency 'C.Unsafe) r) => - (a -> ExceptT e (AppT r) b) -> - t a -> - ExceptT e (AppT r) [b] -traverseConcurrentlyWithErrorsAppT f t = do - env <- lift temporaryGetEnv - ExceptT $ - AppT $ - lift $ - runExceptT $ - traverseConcurrentlyWithErrorsSem - (mapExceptT (lowerAppT env) . f) - t - exceptTToMaybe :: (Monad m) => ExceptT e m () -> m (Maybe e) exceptTToMaybe = (pure . either Just (const Nothing)) <=< runExceptT diff --git a/services/brig/src/Brig/AWS.hs b/services/brig/src/Brig/AWS.hs index 48e927124be..1d86abdc0fe 100644 --- a/services/brig/src/Brig/AWS.hs +++ b/services/brig/src/Brig/AWS.hs @@ -20,7 +20,7 @@ module Brig.AWS ( -- * Monad - Env, + Env (..), mkEnv, Amazon, amazonkaEnv, @@ -174,10 +174,10 @@ listen throttleMillis url callback = forever . handleAny unexpectedError $ do & set SQS.receiveMessage_waitTimeSeconds (Just 20) . set SQS.receiveMessage_maxNumberOfMessages (Just 10) onMessage m = - case decodeStrict . Text.encodeUtf8 =<< (m ^. SQS.message_body) of - Nothing -> err $ msg ("Failed to parse SQS event: " ++ show m) - Just n -> do - debug $ msg ("Received SQS event: " ++ show n) + case eitherDecodeStrict . Text.encodeUtf8 =<< maybe (Left "No message body received") Right (m ^. SQS.message_body) of + Left e -> err $ msg (val "Failed to parse SQS event") . field "error" e . field "message" (show m) + Right n -> do + debug $ msg (val "Received SQS event") . field "event" (show n) liftIO $ callback n for_ (m ^. SQS.message_receiptHandle) (void . send . SQS.newDeleteMessage url) unexpectedError x = do diff --git a/services/brig/src/Brig/AWS/SesNotification.hs b/services/brig/src/Brig/AWS/SesNotification.hs index 9902d260830..37aca042cc5 100644 --- a/services/brig/src/Brig/AWS/SesNotification.hs +++ b/services/brig/src/Brig/AWS/SesNotification.hs @@ -22,36 +22,35 @@ where import Brig.AWS.Types import Brig.App -import Brig.Effects.BlacklistStore (BlacklistStore) -import Brig.Effects.BlacklistStore qualified as BlacklistStore +import Data.Mailbox import Imports import Polysemy (Member) import System.Logger.Class (field, msg, (~~)) import System.Logger.Class qualified as Log import Wire.API.User.Identity -import Wire.UserKeyStore +import Wire.UserSubsystem -onEvent :: (Member BlacklistStore r) => SESNotification -> AppT r () -onEvent (MailBounce BouncePermanent es) = onPermanentBounce es -onEvent (MailBounce BounceTransient es) = onTransientBounce es -onEvent (MailBounce BounceUndetermined es) = onUndeterminedBounce es -onEvent (MailComplaint es) = onComplaint es +onEvent :: (Member UserSubsystem r) => SESNotification -> AppT r () +onEvent (MailBounce BouncePermanent recipients) = onPermanentBounce recipients +onEvent (MailBounce BounceTransient recipients) = onTransientBounce recipients +onEvent (MailBounce BounceUndetermined recipients) = onUndeterminedBounce recipients +onEvent (MailComplaint recipients) = onComplaint recipients -onPermanentBounce :: (Member BlacklistStore r) => [Email] -> AppT r () -onPermanentBounce = mapM_ $ \e -> do - logEmailEvent "Permanent bounce" e - liftSem $ BlacklistStore.insert (mkEmailKey e) +onPermanentBounce :: (Member UserSubsystem r) => [Mailbox] -> AppT r () +onPermanentBounce = mapM_ $ \mailbox -> do + logEmailEvent "Permanent bounce" mailbox.address + liftSem $ blockListInsert mailbox.address -onTransientBounce :: [Email] -> AppT r () -onTransientBounce = mapM_ (logEmailEvent "Transient bounce") +onTransientBounce :: [Mailbox] -> AppT r () +onTransientBounce = mapM_ (logEmailEvent "Transient bounce" . (.address)) -onUndeterminedBounce :: [Email] -> AppT r () -onUndeterminedBounce = mapM_ (logEmailEvent "Undetermined bounce") +onUndeterminedBounce :: [Mailbox] -> AppT r () +onUndeterminedBounce = mapM_ (logEmailEvent "Undetermined bounce" . (.address)) -onComplaint :: (Member BlacklistStore r) => [Email] -> AppT r () -onComplaint = mapM_ $ \e -> do - logEmailEvent "Complaint" e - liftSem $ BlacklistStore.insert (mkEmailKey e) +onComplaint :: (Member UserSubsystem r) => [Mailbox] -> AppT r () +onComplaint = mapM_ $ \mailbox -> do + logEmailEvent "Complaint" mailbox.address + liftSem $ blockListInsert mailbox.address -logEmailEvent :: Text -> Email -> AppT r () +logEmailEvent :: Text -> EmailAddress -> AppT r () logEmailEvent t e = Log.info $ field "email" (fromEmail e) ~~ msg t diff --git a/services/brig/src/Brig/AWS/Types.hs b/services/brig/src/Brig/AWS/Types.hs index 53272094dbb..c2201d59d2f 100644 --- a/services/brig/src/Brig/AWS/Types.hs +++ b/services/brig/src/Brig/AWS/Types.hs @@ -23,15 +23,15 @@ module Brig.AWS.Types where import Data.Aeson +import Data.Mailbox import Imports -import Wire.API.User.Identity ------------------------------------------------------------------------------- -- Notifications data SESNotification - = MailBounce !SESBounceType [Email] - | MailComplaint [Email] + = MailBounce !SESBounceType [Mailbox] + | MailComplaint [Mailbox] deriving (Eq, Show) data SESBounceType diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 9d475b37262..eb7b06457e2 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -1,6 +1,5 @@ {-# LANGUAGE DeepSubsumption #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} -- FUTUREWORK: Get rid of this option once Polysemy is fully introduced to Brig @@ -27,44 +26,47 @@ module Brig.App ( schemaVersion, -- * App Environment - Env, + Env (..), + mkIndexEnv, newEnv, closeEnv, - awsEnv, - smtpEnv, - cargohold, - galley, - galleyEndpoint, - gundeckEndpoint, - cargoholdEndpoint, - federator, - casClient, - userTemplates, - usrTemplates, - providerTemplates, - teamTemplates, - templateBranding, - requestId, - httpManager, - http2Manager, - extGetManager, - settings, - currentTime, - zauthEnv, - digestSHA256, - digestMD5, - applog, - turnEnv, - sftEnv, - internalEvents, - emailSender, - randomPrekeyLocalLock, - keyPackageLocalLock, - rabbitmqChannel, - fsWatcher, - disabledVersions, - enableSFTFederation, - mkIndexEnv, + providerTemplatesWithLocale, + teamTemplatesWithLocale, + teamTemplatesNoLocale, + cargoholdLens, + galleyLens, + galleyEndpointLens, + gundeckEndpointLens, + cargoholdEndpointLens, + federatorLens, + casClientLens, + smtpEnvLens, + emailSenderLens, + awsEnvLens, + appLoggerLens, + internalEventsLens, + requestIdLens, + userTemplatesLens, + providerTemplatesLens, + teamTemplatesLens, + templateBrandingLens, + httpManagerLens, + http2ManagerLens, + extGetManagerLens, + settingsLens, + fsWatcherLens, + turnEnvLens, + sftEnvLens, + currentTimeLens, + zauthEnvLens, + digestSHA256Lens, + digestMD5Lens, + indexEnvLens, + randomPrekeyLocalLockLens, + keyPackageLocalLockLens, + rabbitmqChannelLens, + disabledVersionsLens, + enableSFTFederationLens, -- * App Monad AppT (..), @@ -84,14 +86,12 @@ module Brig.App runHttpClientIO, liftSem, lowerAppT, - temporaryGetEnv, initHttpManagerWithTLSConfig, adhocUserKeyStoreInterpreter, adhocSessionStoreInterpreter, ) where -import Bilge (RequestId (..)) import Bilge qualified as RPC import Bilge.IO import Bilge.RPC (HasRequestId (..)) @@ -110,7 +110,7 @@ import Brig.User.Search.Index (IndexEnv (..), MonadIndexIO (..), runIndexIO) import Brig.User.Template import Brig.ZAuth (MonadZAuth (..), runZAuth) import Brig.ZAuth qualified as ZAuth -import Cassandra (MonadClient, runClient) +import Cassandra (runClient) import Cassandra qualified as Cas import Cassandra.Util (initCassandraForService) import Control.AutoUpdate @@ -121,6 +121,7 @@ import Control.Monad.Trans.Resource import Data.ByteString.Conversion import Data.Credentials (Credentials (..)) import Data.Domain +import Data.Id import Data.Misc import Data.Qualified import Data.Text qualified as Text @@ -139,6 +140,7 @@ import OpenSSL.EVP.Digest (Digest, getDigestByName) import OpenSSL.Session (SSLOption (..)) import OpenSSL.Session qualified as SSL import Polysemy +import Polysemy.Fail import Polysemy.Final import Polysemy.Input (Input, input) import Prometheus @@ -148,10 +150,11 @@ import System.Logger.Class hiding (Settings, settings) import System.Logger.Class qualified as LC import System.Logger.Extended qualified as Log import Util.Options +import Util.SuffixNamer import Wire.API.Federation.Error (federationNotImplemented) import Wire.API.Locale (Locale) import Wire.API.Routes.Version -import Wire.API.User.Identity (Email) +import Wire.API.User.Identity import Wire.EmailSending.SMTP qualified as SMTP import Wire.EmailSubsystem.Template (TemplateBranding, forLocale) import Wire.SessionStore @@ -168,43 +171,43 @@ schemaVersion = Migrations.lastSchemaVersion -- Environment data Env = Env - { _cargohold :: RPC.Request, - _galley :: RPC.Request, - _galleyEndpoint :: Endpoint, - _gundeckEndpoint :: Endpoint, - _cargoholdEndpoint :: Endpoint, - _federator :: Maybe Endpoint, -- FUTUREWORK: should we use a better type here? E.g. to avoid fresh connections all the time? - _casClient :: Cas.ClientState, - _smtpEnv :: Maybe SMTP.SMTP, - _emailSender :: Email, - _awsEnv :: AWS.Env, - _applog :: Logger, - _internalEvents :: QueueEnv, - _requestId :: RequestId, - _usrTemplates :: Localised UserTemplates, - _provTemplates :: Localised ProviderTemplates, - _tmTemplates :: Localised TeamTemplates, - _templateBranding :: TemplateBranding, - _httpManager :: Manager, - _http2Manager :: Http2Manager, - _extGetManager :: (Manager, [Fingerprint Rsa] -> SSL.SSL -> IO ()), - _settings :: Settings, - _fsWatcher :: FS.WatchManager, - _turnEnv :: Calling.TurnEnv, - _sftEnv :: Maybe Calling.SFTEnv, - _currentTime :: IO UTCTime, - _zauthEnv :: ZAuth.Env, - _digestSHA256 :: Digest, - _digestMD5 :: Digest, - _indexEnv :: IndexEnv, - _randomPrekeyLocalLock :: Maybe (MVar ()), - _keyPackageLocalLock :: MVar (), - _rabbitmqChannel :: Maybe (MVar Q.Channel), - _disabledVersions :: Set Version, - _enableSFTFederation :: Maybe Bool + { cargohold :: RPC.Request, + galley :: RPC.Request, + galleyEndpoint :: Endpoint, + gundeckEndpoint :: Endpoint, + cargoholdEndpoint :: Endpoint, + federator :: Maybe Endpoint, -- FUTUREWORK: should we use a better type here? E.g. to avoid fresh connections all the time? + casClient :: Cas.ClientState, + smtpEnv :: Maybe SMTP.SMTP, + emailSender :: EmailAddress, + awsEnv :: AWS.Env, + appLogger :: Logger, + internalEvents :: QueueEnv, + requestId :: RequestId, + userTemplates :: Localised UserTemplates, + providerTemplates :: Localised ProviderTemplates, + teamTemplates :: Localised TeamTemplates, + templateBranding :: TemplateBranding, + httpManager :: Manager, + http2Manager :: Http2Manager, + extGetManager :: (Manager, [Fingerprint Rsa] -> SSL.SSL -> IO ()), + settings :: Settings, + fsWatcher :: FS.WatchManager, + turnEnv :: Calling.TurnEnv, + sftEnv :: Maybe Calling.SFTEnv, + currentTime :: IO UTCTime, + zauthEnv :: ZAuth.Env, + digestSHA256 :: Digest, + digestMD5 :: Digest, + indexEnv :: IndexEnv, + randomPrekeyLocalLock :: Maybe (MVar ()), + keyPackageLocalLock :: MVar (), + rabbitmqChannel :: Maybe (MVar Q.Channel), + disabledVersions :: Set Version, + enableSFTFederation :: Maybe Bool } -makeLenses ''Env +makeLensesWith (lensRules & lensField .~ suffixNamer) ''Env validateOptions :: Opts -> IO () validateOptions o = @@ -214,44 +217,43 @@ validateOptions o = _ -> pure () newEnv :: Opts -> IO Env -newEnv o = do - validateOptions o +newEnv opts = do + validateOptions opts Just md5 <- getDigestByName "MD5" Just sha256 <- getDigestByName "SHA256" Just sha512 <- getDigestByName "SHA512" - lgr <- Log.mkLogger (Opt.logLevel o) (Opt.logNetStrings o) (Opt.logFormat o) - cas <- initCassandra o lgr + lgr <- Log.mkLogger (Opt.logLevel opts) (Opt.logNetStrings opts) (Opt.logFormat opts) + cas <- initCassandra opts lgr mgr <- initHttpManager h2Mgr <- initHttp2Manager ext <- initExtGetManager - utp <- loadUserTemplates o - ptp <- loadProviderTemplates o - ttp <- loadTeamTemplates o - let branding = genTemplateBranding . Opt.templateBranding . Opt.general . Opt.emailSMS $ o - (emailAWSOpts, emailSMTP) <- emailConn lgr $ Opt.email (Opt.emailSMS o) - aws <- AWS.mkEnv lgr (Opt.aws o) emailAWSOpts mgr - zau <- initZAuth o + utp <- loadUserTemplates opts + ptp <- loadProviderTemplates opts + ttp <- loadTeamTemplates opts + let branding = genTemplateBranding . Opt.templateBranding . Opt.general . Opt.emailSMS $ opts + (emailAWSOpts, emailSMTP) <- emailConn lgr $ Opt.email (Opt.emailSMS opts) + aws <- AWS.mkEnv lgr (Opt.aws opts) emailAWSOpts mgr + zau <- initZAuth opts clock <- mkAutoUpdate defaultUpdateSettings {updateAction = getCurrentTime} w <- FS.startManagerConf $ FS.defaultConfig {FS.confWatchMode = FS.WatchModeOS} - let turnOpts = Opt.turn o + let turnOpts = Opt.turn opts turnSecret <- Text.encodeUtf8 . Text.strip <$> Text.readFile (Opt.secret turnOpts) turn <- Calling.mkTurnEnv (Opt.serversSource turnOpts) (Opt.tokenTTL turnOpts) (Opt.configTTL turnOpts) turnSecret sha512 - let sett = Opt.optSettings o - eventsQueue :: QueueEnv <- case Opt.internalEventsQueue (Opt.internalEvents o) of + eventsQueue :: QueueEnv <- case opts.internalEvents.internalEventsQueue of StompQueueOpts q -> do - stomp :: Stomp.Env <- case (Opt.stomp o, Opt.setStomp sett) of + stomp :: Stomp.Env <- case (opts.stompOptions, opts.settings.stomp) of (Just s, Just c) -> Stomp.mkEnv s <$> initCredentials c (Just _, Nothing) -> error "STOMP is configured but 'setStomp' is not set" (Nothing, Just _) -> error "'setStomp' is present but STOMP is not configured" (Nothing, Nothing) -> error "stomp is selected for internal events, but not configured in 'setStomp', STOMP" pure (StompQueueEnv (Stomp.broker stomp) q) SqsQueueOpts q -> do - let throttleMillis = fromMaybe Opt.defSqsThrottleMillis (view Opt.sqsThrottleMillis $ Opt.optSettings o) + let throttleMillis = fromMaybe Opt.defSqsThrottleMillis opts.settings.sqsThrottleMillis SqsQueueEnv aws throttleMillis <$> AWS.getQueueUrl (aws ^. AWS.amazonkaEnv) q - mSFTEnv <- mapM (Calling.mkSFTEnv sha512) $ Opt.sft o - prekeyLocalLock <- case Opt.randomPrekeys o of + mSFTEnv <- mapM (Calling.mkSFTEnv sha512) $ Opt.sft opts + prekeyLocalLock <- case Opt.randomPrekeys opts of Just True -> do Log.info lgr $ Log.msg (Log.val "randomPrekeys: active") Just <$> newMVar () @@ -259,58 +261,58 @@ newEnv o = do Log.info lgr $ Log.msg (Log.val "randomPrekeys: not active; using dynamoDB instead.") pure Nothing kpLock <- newMVar () - rabbitChan <- traverse (Q.mkRabbitMqChannelMVar lgr) o.rabbitmq - let allDisabledVersions = foldMap expandVersionExp (Opt.setDisabledAPIVersions sett) - idxEnv <- mkIndexEnv o.elasticsearch lgr (Opt.galley o) mgr + rabbitChan <- traverse (Q.mkRabbitMqChannelMVar lgr) opts.rabbitmq + let allDisabledVersions = foldMap expandVersionExp opts.settings.disabledAPIVersions + idxEnv <- mkIndexEnv opts.elasticsearch lgr (Opt.galley opts) mgr pure $! Env - { _cargohold = mkEndpoint $ Opt.cargohold o, - _galley = mkEndpoint $ Opt.galley o, - _galleyEndpoint = Opt.galley o, - _gundeckEndpoint = Opt.gundeck o, - _cargoholdEndpoint = Opt.cargohold o, - _federator = Opt.federatorInternal o, - _casClient = cas, - _smtpEnv = emailSMTP, - _emailSender = Opt.emailSender . Opt.general . Opt.emailSMS $ o, - _awsEnv = aws, -- used by `journalEvent` directly - _applog = lgr, - _internalEvents = (eventsQueue :: QueueEnv), - _requestId = RequestId "N/A", - _usrTemplates = utp, - _provTemplates = ptp, - _tmTemplates = ttp, - _templateBranding = branding, - _httpManager = mgr, - _http2Manager = h2Mgr, - _extGetManager = ext, - _settings = sett, - _turnEnv = turn, - _sftEnv = mSFTEnv, - _fsWatcher = w, - _currentTime = clock, - _zauthEnv = zau, - _digestMD5 = md5, - _digestSHA256 = sha256, - _indexEnv = idxEnv, - _randomPrekeyLocalLock = prekeyLocalLock, - _keyPackageLocalLock = kpLock, - _rabbitmqChannel = rabbitChan, - _disabledVersions = allDisabledVersions, - _enableSFTFederation = Opt.multiSFT o + { cargohold = mkEndpoint $ opts.cargohold, + galley = mkEndpoint $ opts.galley, + galleyEndpoint = opts.galley, + gundeckEndpoint = opts.gundeck, + cargoholdEndpoint = opts.cargohold, + federator = opts.federatorInternal, + casClient = cas, + smtpEnv = emailSMTP, + emailSender = opts.emailSMS.general.emailSender, + awsEnv = aws, -- used by `journalEvent` directly + appLogger = lgr, + internalEvents = (eventsQueue :: QueueEnv), + requestId = RequestId defRequestId, + userTemplates = utp, + providerTemplates = ptp, + teamTemplates = ttp, + templateBranding = branding, + httpManager = mgr, + http2Manager = h2Mgr, + extGetManager = ext, + settings = opts.settings, + turnEnv = turn, + sftEnv = mSFTEnv, + fsWatcher = w, + currentTime = clock, + zauthEnv = zau, + digestMD5 = md5, + digestSHA256 = sha256, + indexEnv = idxEnv, + randomPrekeyLocalLock = prekeyLocalLock, + keyPackageLocalLock = kpLock, + rabbitmqChannel = rabbitChan, + disabledVersions = allDisabledVersions, + enableSFTFederation = opts.multiSFT } where emailConn _ (Opt.EmailAWS aws) = pure (Just aws, Nothing) emailConn lgr (Opt.EmailSMTP s) = do - let h = Opt.smtpEndpoint s ^. host - p = Just $ fromInteger $ toInteger $ Opt.smtpEndpoint s ^. port + let h = s.smtpEndpoint.host + p = Just . fromInteger . toInteger $ s.smtpEndpoint.port smtpCredentials <- case Opt.smtpCredentials s of Just (Opt.EmailSMTPCredentials u p') -> do Just . (SMTP.Username u,) . SMTP.Password <$> initCredentials p' _ -> pure Nothing smtp <- SMTP.initSMTP lgr h p smtpCredentials (Opt.smtpConnType s) pure (Nothing, Just smtp) - mkEndpoint service = RPC.host (encodeUtf8 (service ^. host)) . RPC.port (service ^. port) $ RPC.empty + mkEndpoint service = RPC.host (encodeUtf8 service.host) . RPC.port service.port $ RPC.empty mkIndexEnv :: ElasticSearchOpts -> Logger -> Endpoint -> Manager -> IO IndexEnv mkIndexEnv esOpts logger galleyEp rpcHttpManager = do @@ -437,21 +439,23 @@ initCassandra o g = (Just schemaVersion) g -userTemplates :: (MonadReader Env m) => Maybe Locale -> m (Locale, UserTemplates) -userTemplates l = forLocale l <$> view usrTemplates +teamTemplatesWithLocale :: (MonadReader Env m) => Maybe Locale -> m (Locale, TeamTemplates) +teamTemplatesWithLocale l = forLocale l <$> asks (.teamTemplates) -providerTemplates :: (MonadReader Env m) => Maybe Locale -> m (Locale, ProviderTemplates) -providerTemplates l = forLocale l <$> view provTemplates +providerTemplatesWithLocale :: (MonadReader Env m) => Maybe Locale -> m (Locale, ProviderTemplates) +providerTemplatesWithLocale l = forLocale l <$> asks (.providerTemplates) -teamTemplates :: (MonadReader Env m) => Maybe Locale -> m (Locale, TeamTemplates) -teamTemplates l = forLocale l <$> view tmTemplates +-- this works because team templates is not affected by `forLocale`; it is useful where we +-- use the `TeamTemplates` only for finding invitation url templates (those are not localized). +teamTemplatesNoLocale :: (MonadReader Env m) => m TeamTemplates +teamTemplatesNoLocale = snd <$> teamTemplatesWithLocale Nothing closeEnv :: Env -> IO () closeEnv e = do - Cas.shutdown $ e ^. casClient - FS.stopManager $ e ^. fsWatcher - Log.flush $ e ^. applog - Log.close $ e ^. applog + Cas.shutdown $ e.casClient + FS.stopManager $ e.fsWatcher + Log.flush $ e.appLogger + Log.close $ e.appLogger ------------------------------------------------------------------------------- -- App Monad @@ -468,9 +472,6 @@ newtype AppT r a = AppT lowerAppT :: (Member (Final IO) r) => Env -> AppT r a -> Sem r a lowerAppT env (AppT r) = runReaderT r env -temporaryGetEnv :: AppT r Env -temporaryGetEnv = AppT ask - instance Functor (AppT r) where fmap fab (AppT x0) = AppT $ fmap fab x0 @@ -490,6 +491,9 @@ instance MonadMonitor (AppT r) where instance MonadThrow (AppT r) where throwM = liftIO . throwM +instance (Member Fail r) => MonadFail (AppT r) where + fail = AppT . fail + instance (Member (Final IO) r) => MonadThrow (Sem r) where throwM = embedFinal . throwM @IO @@ -514,14 +518,14 @@ liftSem sem = AppT $ lift sem instance (MonadIO m) => MonadLogger (ReaderT Env m) where log l m = do - g <- view applog - r <- view requestId + g <- asks (.appLogger) + r <- asks (.requestId) Log.log g l $ field "request" (unRequestId r) ~~ m instance MonadLogger (AppT r) where log l m = do - g <- view applog - r <- view requestId + g <- asks (.appLogger) + r <- asks (.requestId) AppT $ lift $ embedFinal @IO $ @@ -533,23 +537,22 @@ instance MonadLogger (ExceptT err (AppT r)) where instance MonadHttp (AppT r) where handleRequestWithCont req handler = do - manager <- view httpManager + manager <- asks (.httpManager) liftIO $ withResponse req manager handler instance MonadZAuth (AppT r) where - liftZAuth za = view zauthEnv >>= \e -> runZAuth e za + liftZAuth za = asks (.zauthEnv) >>= \e -> runZAuth e za instance MonadZAuth (ExceptT err (AppT r)) where - liftZAuth za = lift (view zauthEnv) >>= flip runZAuth za + liftZAuth za = lift (asks (.zauthEnv)) >>= flip runZAuth za -- | The function serves as a crutch while Brig is being polysemised. Use it -- whenever the compiler complains that there is no instance of `MonadClient` -- for `AppT r`. It can be removed once there is no `AppT` anymore. wrapClient :: ReaderT Env Cas.Client a -> AppT r a wrapClient m = do - c <- view casClient env <- ask - runClient c $ runReaderT m env + runClient env.casClient $ runReaderT m env wrapClientE :: ExceptT e (ReaderT Env Cas.Client) a -> ExceptT e (AppT r) a wrapClientE = mapExceptT wrapClient @@ -584,22 +587,22 @@ newtype HttpClientIO a = HttpClientIO runHttpClientIO :: (MonadIO m) => Env -> HttpClientIO a -> m a runHttpClientIO env = - runClient (env ^. casClient) - . runHttpT (env ^. httpManager) + runClient (env.casClient) + . runHttpT (env.httpManager) . flip runReaderT env . unHttpClientIO instance MonadZAuth HttpClientIO where - liftZAuth za = view zauthEnv >>= flip runZAuth za + liftZAuth za = asks (.zauthEnv) >>= flip runZAuth za instance HasRequestId HttpClientIO where - getRequestId = view requestId + getRequestId = asks (.requestId) instance Cas.MonadClient HttpClientIO where liftClient cl = do env <- ask - liftIO $ runClient (view casClient env) cl - localState f = local (casClient %~ f) + liftIO $ runClient (asks (.casClient) env) cl + localState f = local (casClientLens %~ f) instance MonadMonitor HttpClientIO where doIO = liftIO @@ -613,38 +616,38 @@ wrapHttpClientE :: ExceptT e HttpClientIO a -> ExceptT e (AppT r) a wrapHttpClientE = mapExceptT wrapHttpClient instance (MonadIO m) => MonadIndexIO (ReaderT Env m) where - liftIndexIO m = view indexEnv >>= \e -> runIndexIO e m + liftIndexIO m = asks (.indexEnv) >>= \e -> runIndexIO e m instance MonadIndexIO (AppT r) where liftIndexIO m = do AppT $ mapReaderT (embedToFinal @IO) $ liftIndexIO m instance (MonadIndexIO (AppT r)) => MonadIndexIO (ExceptT err (AppT r)) where - liftIndexIO m = view indexEnv >>= \e -> runIndexIO e m + liftIndexIO m = asks (.indexEnv) >>= \e -> runIndexIO e m instance HasRequestId (AppT r) where - getRequestId = view requestId + getRequestId = asks (.requestId) ------------------------------------------------------------------------------- -- Ad hoc interpreters -- | similarly to `wrapClient`, this function serves as a crutch while Brig is being polysemised. -adhocUserKeyStoreInterpreter :: (MonadClient m, MonadReader Env m) => Sem '[UserKeyStore, UserStore, Embed IO] a -> m a +adhocUserKeyStoreInterpreter :: (MonadIO m, MonadReader Env m) => Sem '[UserKeyStore, UserStore, Embed IO] a -> m a adhocUserKeyStoreInterpreter action = do - clientState <- asks (view casClient) + 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 :: (MonadClient m, MonadReader Env m) => Sem '[SessionStore, Embed IO] a -> m a +adhocSessionStoreInterpreter :: (MonadIO m, MonadReader Env m) => Sem '[SessionStore, Embed IO] a -> m a adhocSessionStoreInterpreter action = do - clientState <- asks (view casClient) + clientState <- asks (.casClient) liftIO $ runM . interpretSessionStoreCassandra clientState $ action -------------------------------------------------------------------------------- -- Federation viewFederationDomain :: (MonadReader Env m) => m Domain -viewFederationDomain = view (settings . Opt.federationDomain) +viewFederationDomain = asks (.settings.federationDomain) -- FUTUREWORK: rename to 'qualifyLocalMtl' qualifyLocal :: (MonadReader Env m) => a -> m (Local a) diff --git a/services/brig/src/Brig/Calling.hs b/services/brig/src/Brig/Calling.hs index c9501b3fcad..8c1c2749d32 100644 --- a/services/brig/src/Brig/Calling.hs +++ b/services/brig/src/Brig/Calling.hs @@ -45,7 +45,6 @@ module Brig.Calling turnConfigTTL, turnSecret, turnSHA512, - turnPrng, ) where @@ -74,7 +73,6 @@ import Polysemy.TinyLog import System.FSNotify qualified as FS import System.FilePath qualified as Path import System.Logger qualified as Log -import System.Random.MWC (GenIO, createSystemRandom) import System.Random.Shuffle import UnliftIO (Async) import UnliftIO.Async qualified as Async @@ -189,7 +187,6 @@ srvDiscoveryLoop domain discoveryInterval saveAction = forever $ do data SFTTokenEnv = SFTTokenEnv { sftTokenTTL :: Word32, sftTokenSecret :: ByteString, - sftTokenPRNG :: GenIO, sftTokenSHA :: Digest } @@ -214,7 +211,6 @@ mkSFTTokenEnv :: Digest -> Opts.SFTTokenOptions -> IO SFTTokenEnv mkSFTTokenEnv digest opts = SFTTokenEnv (Opts.sttTTL opts) <$> BS.readFile (Opts.sttSecret opts) - <*> createSystemRandom <*> pure digest -- | Start SFT service discovery synchronously @@ -240,8 +236,7 @@ data TurnEnv = TurnEnv _turnTokenTTL :: Word32, _turnConfigTTL :: Word32, _turnSecret :: ByteString, - _turnSHA512 :: Digest, - _turnPrng :: GenIO + _turnSHA512 :: Digest } makeLenses ''TurnEnv @@ -260,7 +255,6 @@ mkTurnEnv serversSource _turnTokenTTL _turnConfigTTL _turnSecret _turnSHA512 = d TurnServersFromFiles files <$> newIORef NotDiscoveredYet <*> newIORef NotDiscoveredYet - _turnPrng <- createSystemRandom pure $ TurnEnv {..} turnServersV1 :: (MonadIO m) => TurnServers -> m (Discovery (NonEmpty TurnURI)) diff --git a/services/brig/src/Brig/Calling/API.hs b/services/brig/src/Brig/Calling/API.hs index e97c51e19c2..bc5ae8d6013 100644 --- a/services/brig/src/Brig/Calling/API.hs +++ b/services/brig/src/Brig/Calling/API.hs @@ -21,6 +21,7 @@ module Brig.Calling.API ( getCallsConfig, getCallsConfigV2, + base26, -- * Exposed for testing purposes newConfig, @@ -40,26 +41,28 @@ import Brig.Options (ListAllSFTServers (..)) import Brig.Options qualified as Opt import Control.Error (hush, throwE) import Control.Lens +import Crypto.Hash qualified as Crypto +import Data.ByteArray (convert) +import Data.ByteString qualified as B import Data.ByteString.Conversion -import Data.ByteString.Lens +import Data.ByteString.Lazy qualified as BL import Data.Id import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty import Data.Misc (HttpsUrl) import Data.Range import Data.Text.Ascii (AsciiBase64, encodeBase64) -import Data.Text.Strict.Lens import Data.Time.Clock.POSIX +import Data.UUID qualified as UUID import Imports hiding (head) import OpenSSL.EVP.Digest (Digest, hmacBS) import Polysemy import Polysemy.Error qualified as Polysemy import System.Logger.Class qualified as Log -import System.Random.MWC qualified as MWC import Wire.API.Call.Config qualified as Public -import Wire.API.Team.Feature (AllFeatureConfigs (afcConferenceCalling), FeatureStatus (FeatureStatusDisabled, FeatureStatusEnabled), wsStatus) +import Wire.API.Team.Feature import Wire.Error -import Wire.GalleyAPIAccess (GalleyAPIAccess, getAllFeatureConfigsForUser) +import Wire.GalleyAPIAccess (GalleyAPIAccess, getAllTeamFeaturesForUser) import Wire.Network.DNS.SRV (srvTarget) -- | ('UserId', 'ConnId' are required as args here to make sure this is an authenticated end-point.) @@ -73,14 +76,14 @@ getCallsConfigV2 :: Maybe (Range 1 10 Int) -> (Handler r) Public.RTCConfiguration getCallsConfigV2 uid _ limit = do - env <- view turnEnv - staticUrl <- view $ settings . Opt.sftStaticUrl - sftListAllServers <- fromMaybe Opt.HideAllSFTServers <$> view (settings . Opt.sftListAllServers) - sftEnv' <- view sftEnv - sftFederation <- view enableSFTFederation + env <- asks (.turnEnv) + staticUrl <- asks (.settings.sftStaticUrl) + sftListAllServers <- fromMaybe Opt.HideAllSFTServers <$> asks (.settings.sftListAllServers) + sftEnv' <- asks (.sftEnv) + sftFederation <- asks (.enableSFTFederation) discoveredServers <- turnServersV2 (env ^. turnServers) shared <- do - ccStatus <- lift $ liftSem $ (wsStatus . afcConferenceCalling <$> getAllFeatureConfigsForUser (Just uid)) + ccStatus <- lift $ liftSem $ ((.status) . npProject @ConferenceCallingConfig <$> getAllTeamFeaturesForUser (Just uid)) pure $ case ccStatus of FeatureStatusEnabled -> True FeatureStatusDisabled -> False @@ -88,7 +91,7 @@ getCallsConfigV2 uid _ limit = do lift . liftSem . Polysemy.runError - $ newConfig env discoveredServers staticUrl sftEnv' limit sftListAllServers (CallsConfigV2 sftFederation) shared + $ newConfig uid env discoveredServers staticUrl sftEnv' limit sftListAllServers (CallsConfigV2 sftFederation) shared handleNoTurnServers eitherConfig -- | Throws '500 Internal Server Error' when no turn servers are found. This is @@ -112,10 +115,10 @@ getCallsConfig :: ConnId -> (Handler r) Public.RTCConfiguration getCallsConfig uid _ = do - env <- view turnEnv + env <- asks (.turnEnv) discoveredServers <- turnServersV1 (env ^. turnServers) shared <- do - ccStatus <- lift $ liftSem $ (wsStatus . afcConferenceCalling <$> getAllFeatureConfigsForUser (Just uid)) + ccStatus <- lift $ liftSem $ ((.status) . npProject @ConferenceCallingConfig <$> getAllTeamFeaturesForUser (Just uid)) pure $ case ccStatus of FeatureStatusEnabled -> True FeatureStatusDisabled -> False @@ -124,7 +127,7 @@ getCallsConfig uid _ = do . lift . liftSem . Polysemy.runError - $ newConfig env discoveredServers Nothing Nothing Nothing HideAllSFTServers CallsConfigDeprecated shared + $ newConfig uid env discoveredServers Nothing Nothing Nothing HideAllSFTServers CallsConfigDeprecated shared handleNoTurnServers eitherConfig where -- In order to avoid being backwards incompatible, remove the `transport` query param from the URIs @@ -153,6 +156,7 @@ newConfig :: Member SFT r, Member (Polysemy.Error NoTurnServers) r ) => + UserId -> Calling.TurnEnv -> Discovery (NonEmpty Public.TurnURI) -> Maybe HttpsUrl -> @@ -162,7 +166,7 @@ newConfig :: CallsConfigVersion -> Bool -> Sem r Public.RTCConfiguration -newConfig env discoveredServers sftStaticUrl mSftEnv limit listAllServers version shared = do +newConfig uid env discoveredServers sftStaticUrl mSftEnv limit listAllServers version shared = do -- randomize list of servers (before limiting the list, to ensure not always the same servers are chosen if limit is set) randomizedUris <- liftIO . randomize @@ -173,7 +177,7 @@ newConfig env discoveredServers sftStaticUrl mSftEnv limit listAllServers versio -- randomize again (as limitedList partially re-orders uris) finalUris <- liftIO $ randomize limitedUris srvs <- for finalUris $ \uri -> do - u <- liftIO $ genTurnUsername (env ^. turnTokenTTL) (env ^. turnPrng) + u <- liftIO $ genTurnUsername (env ^. turnTokenTTL) pure . Public.rtcIceServer (pure uri) u $ computeCred (env ^. turnSHA512) (env ^. turnSecret) u let staticSft = pure . Public.sftServer <$> sftStaticUrl @@ -211,20 +215,38 @@ newConfig env discoveredServers sftStaticUrl mSftEnv limit listAllServers versio NonEmpty.nonEmpty (Public.limitServers (NonEmpty.toList uris) (fromRange lim)) & fromMaybe (error "newConfig:limitedList: empty list of servers") - genUsername :: Word32 -> MWC.GenIO -> IO (POSIXTime, Text) - genUsername ttl prng = do - rnd <- view (packedBytes . utf8) <$> replicateM 16 (MWC.uniformR (97, 122) prng) - t <- fromIntegral . (+ ttl) . round <$> getPOSIXTime - pure $ (t, rnd) + hash :: ByteString -> ByteString + hash = convert . Crypto.hash @ByteString @Crypto.SHA256 + + genUsername :: UserId -> Text + genUsername = + base26 + . foldr (\x r -> fromIntegral x + r * 256) 0 + . take 16 + . B.unpack + . hash + . BL.toStrict + . UUID.toByteString + . toUUID - genTurnUsername :: Word32 -> MWC.GenIO -> IO Public.TurnUsername - genTurnUsername = (fmap (uncurry Public.turnUsername) .) . genUsername + getTime :: Word32 -> IO POSIXTime + getTime ttl = fromIntegral . (+ ttl) . round <$> getPOSIXTime - genSFTUsername :: Word32 -> MWC.GenIO -> IO Public.SFTUsername - genSFTUsername = (fmap (uncurry (Public.mkSFTUsername shared)) .) . genUsername + genTurnUsername :: Word32 -> IO Public.TurnUsername + genTurnUsername ttl = + Public.turnUsername + <$> getTime ttl + <*> pure (genUsername uid) + + genSFTUsername :: Word32 -> IO Public.SFTUsername + genSFTUsername ttl = + Public.mkSFTUsername shared + <$> getTime ttl + <*> pure (genUsername uid) computeCred :: (ToByteString a) => Digest -> ByteString -> a -> AsciiBase64 computeCred dig secret = encodeBase64 . hmacBS dig secret . toByteString' + authenticate :: (Member (Embed IO) r) => Public.SFTServer -> @@ -233,7 +255,7 @@ newConfig env discoveredServers sftStaticUrl mSftEnv limit listAllServers versio maybe (pure . Public.nauthSFTServer) ( \SFTTokenEnv {..} sftsvr -> do - username <- liftIO $ genSFTUsername sftTokenTTL sftTokenPRNG + username <- liftIO $ genSFTUsername sftTokenTTL let credential = computeCred sftTokenSHA sftTokenSecret username pure $ Public.authSFTServer sftsvr username credential ) diff --git a/services/brig/src/Brig/Calling/Internal.hs b/services/brig/src/Brig/Calling/Internal.hs index cc891c77590..d06a25431e4 100644 --- a/services/brig/src/Brig/Calling/Internal.hs +++ b/services/brig/src/Brig/Calling/Internal.hs @@ -20,6 +20,7 @@ module Brig.Calling.Internal where import Control.Lens ((?~)) import Data.ByteString.Char8 qualified as BS import Data.Misc (ensureHttpsUrl) +import Data.Text qualified as T import Imports import URI.ByteString qualified as URI import URI.ByteString.QQ qualified as URI @@ -40,3 +41,13 @@ sftServerFromSrvTarget (SrvTarget host port) = if BS.last bs == '.' then BS.init bs else bs + +base26 :: Integer -> Text +base26 0 = "a" +base26 num = T.pack $ go [] num + where + go :: String -> Integer -> String + go acc 0 = acc + go acc n = + let (q, r) = divMod n 26 + in go (chr (fromIntegral r + ord 'a') : acc) q diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 13158e6f03d..5248effd92b 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -3,20 +3,18 @@ module Brig.CanonicalInterpreter where import Brig.AWS (amazonkaEnv) import Brig.App as App import Brig.DeleteQueue.Interpreter as DQ -import Brig.Effects.BlacklistStore (BlacklistStore) -import Brig.Effects.BlacklistStore.Cassandra (interpretBlacklistStoreToCassandra) import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.ConnectionStore.Cassandra (connectionStoreToCassandra) -import Brig.Effects.FederationConfigStore (FederationConfigStore) -import Brig.Effects.FederationConfigStore.Cassandra (interpretFederationDomainConfig, remotesMapFromCfgFile) import Brig.Effects.JwtTools import Brig.Effects.PublicKeyBundle import Brig.Effects.SFT (SFT, interpretSFT) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import Brig.Effects.UserPendingActivationStore.Cassandra (userPendingActivationStoreToCassandra) -import Brig.IO.Intra (runUserEvents) -import Brig.Options (ImplicitNoFederationRestriction (federationDomainConfig), federationDomainConfigs, federationStrategy) +import Brig.IO.Intra (runEvents) +import Brig.Options (federationDomainConfigs, federationStrategy) import Brig.Options qualified as Opt +import Brig.Team.Template (TeamTemplates) +import Brig.User.Search.Index (IndexEnv (..)) import Cassandra qualified as Cas import Control.Exception (ErrorCall) import Control.Lens (to, (^.)) @@ -30,12 +28,17 @@ import Polysemy.Conc import Polysemy.Embed (runEmbedded) import Polysemy.Error (Error, errorToIOFinal, mapError, runError) import Polysemy.Input (Input, runInputConst, runInputSem) +import Polysemy.Internal.Kind import Polysemy.TinyLog (TinyLog) import Wire.API.Allowlists (AllowlistEmailDomains) import Wire.API.Federation.Client qualified import Wire.API.Federation.Error +import Wire.ActivationCodeStore (ActivationCodeStore) +import Wire.ActivationCodeStore.Cassandra (interpretActivationCodeStoreToCassandra) import Wire.AuthenticationSubsystem import Wire.AuthenticationSubsystem.Interpreter +import Wire.BlockListStore +import Wire.BlockListStore.Cassandra import Wire.DeleteQueue import Wire.EmailSending import Wire.EmailSending.SES @@ -43,12 +46,19 @@ import Wire.EmailSending.SMTP import Wire.EmailSubsystem import Wire.EmailSubsystem.Interpreter import Wire.Error +import Wire.Events import Wire.FederationAPIAccess qualified import Wire.FederationAPIAccess.Interpreter (FederationAPIAccessConfig (..), interpretFederationAPIAccess) +import Wire.FederationConfigStore (FederationConfigStore) +import Wire.FederationConfigStore.Cassandra (interpretFederationDomainConfig, remotesMapFromCfgFile) import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess.Rpc import Wire.GundeckAPIAccess import Wire.HashPassword +import Wire.IndexedUserStore +import Wire.IndexedUserStore.ElasticSearch +import Wire.InvitationStore (InvitationStore) +import Wire.InvitationStore.Cassandra (interpretInvitationStoreToCassandra) import Wire.NotificationSubsystem import Wire.NotificationSubsystem.Interpreter (defaultNotificationSubsystemConfig, runNotificationSubsystemGundeck) import Wire.ParseException @@ -56,12 +66,18 @@ import Wire.PasswordResetCodeStore (PasswordResetCodeStore) import Wire.PasswordResetCodeStore.Cassandra (interpretClientToIO, passwordResetCodeStoreToCassandra) import Wire.PasswordStore (PasswordStore) import Wire.PasswordStore.Cassandra (interpretPasswordStore) +import Wire.PropertyStore +import Wire.PropertyStore.Cassandra +import Wire.PropertySubsystem +import Wire.PropertySubsystem.Interpreter import Wire.Rpc import Wire.Sem.Concurrency import Wire.Sem.Concurrency.IO import Wire.Sem.Delay import Wire.Sem.Jwk import Wire.Sem.Logger.TinyLog (loggerToTinyLogReqId) +import Wire.Sem.Metrics +import Wire.Sem.Metrics.IO (runMetricsToIO) import Wire.Sem.Now (Now) import Wire.Sem.Now.IO (nowToIOAction) import Wire.Sem.Paging.Cassandra (InternalPaging) @@ -69,7 +85,9 @@ import Wire.Sem.Random import Wire.Sem.Random.IO import Wire.SessionStore import Wire.SessionStore.Cassandra (interpretSessionStoreCassandra) -import Wire.UserEvents +import Wire.TeamInvitationSubsystem +import Wire.TeamInvitationSubsystem.Error +import Wire.TeamInvitationSubsystem.Interpreter import Wire.UserKeyStore import Wire.UserKeyStore.Cassandra import Wire.UserStore @@ -84,36 +102,51 @@ import Wire.VerificationCodeSubsystem.Interpreter type BrigCanonicalEffects = '[ AuthenticationSubsystem, - UserSubsystem, - EmailSubsystem, + TeamInvitationSubsystem, + UserSubsystem + ] + `Append` BrigLowerLevelEffects + +-- | These effects have interpreters which don't depend on each other +type BrigLowerLevelEffects = + '[ EmailSubsystem, VerificationCodeSubsystem, + PropertySubsystem, DeleteQueue, - UserEvents, + Wire.Events.Events, Error UserSubsystemError, + Error TeamInvitationSubsystemError, Error AuthenticationSubsystemError, Error Wire.API.Federation.Error.FederationError, Error VerificationCodeSubsystemError, + Error PropertySubsystemError, Error HttpError, Wire.FederationAPIAccess.FederationAPIAccess Wire.API.Federation.Client.FederatorClient, HashPassword, UserKeyStore, UserStore, + IndexedUserStore, SessionStore, PasswordStore, VerificationCodeStore, + ActivationCodeStore, + InvitationStore, + PropertyStore, SFT, ConnectionStore InternalPaging, + Input UserSubsystemConfig, Input VerificationCodeThrottleTTL, Input UTCTime, Input (Local ()), Input (Maybe AllowlistEmailDomains), + Input TeamTemplates, NotificationSubsystem, GundeckAPIAccess, FederationConfigStore, Jwk, PublicKeyBundle, JwtTools, - BlacklistStore, + BlockListStore, UserPendingActivationStore InternalPaging, Now, Delay, @@ -122,6 +155,7 @@ type BrigCanonicalEffects = GalleyAPIAccess, EmailSending, Rpc, + Metrics, Embed Cas.Client, Error ParseException, Error ErrorCall, @@ -139,16 +173,52 @@ runBrigToIO :: App.Env -> AppT BrigCanonicalEffects a -> IO a runBrigToIO e (AppT ma) = do let userSubsystemConfig = UserSubsystemConfig - { emailVisibilityConfig = e ^. settings . Opt.emailVisibility, - defaultLocale = e ^. settings . to Opt.setDefaultUserLocale + { emailVisibilityConfig = e.settings.emailVisibility, + defaultLocale = Opt.defaultUserLocale e.settings, + searchSameTeamOnly = fromMaybe False e.settings.searchSameTeamOnly, + maxTeamSize = e.settings.maxTeamSize + } + teamInvitationSubsystemConfig = + TeamInvitationSubsystemConfig + { maxTeamSize = e.settings.maxTeamSize, + teamInvitationTimeout = e.settings.teamInvitationTimeout } federationApiAccessConfig = FederationAPIAccessConfig - { ownDomain = e ^. settings . Opt.federationDomain, - federatorEndpoint = e ^. federator, - http2Manager = e ^. App.http2Manager, - requestId = e ^. App.requestId + { ownDomain = e.settings.federationDomain, + federatorEndpoint = e.federator, + http2Manager = e.http2Manager, + requestId = e.requestId + } + propertySubsystemConfig = + PropertySubsystemConfig + { maxKeyLength = fromMaybe Opt.defMaxKeyLen e.settings.propertyMaxKeyLen, + maxValueLength = fromMaybe Opt.defMaxValueLen e.settings.propertyMaxValueLen, + maxProperties = 16 } + mainESEnv = e.indexEnv ^. to idxElastic + indexedUserStoreConfig = + IndexedUserStoreConfig + { conn = + ESConn + { env = mainESEnv, + indexName = e.indexEnv ^. to idxName + }, + additionalConn = + (e.indexEnv ^. to idxAdditionalName) <&> \additionalIndexName -> + ESConn + { env = e.indexEnv ^. to idxAdditionalElastic . to (fromMaybe mainESEnv), + indexName = additionalIndexName + } + } + + -- These interpreters depend on each other, we use let recursion to solve that. + 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 @@ -156,50 +226,61 @@ runBrigToIO e (AppT ma) = do . interpretRace . embedToFinal . runEmbedded (runHttpClientIO e) - . loggerToTinyLogReqId (e ^. App.requestId) (e ^. applog) + . loggerToTinyLogReqId e.requestId e.appLogger . runError @SomeException . mapError @ErrorCall SomeException . mapError @ParseException SomeException - . interpretClientToIO (e ^. casClient) - . runRpcWithHttp (e ^. httpManager) (e ^. App.requestId) + . interpretClientToIO e.casClient + . runMetricsToIO + . runRpcWithHttp e.httpManager e.requestId . emailSendingInterpreter e - . interpretGalleyAPIAccessToRpc (e ^. disabledVersions) (e ^. galleyEndpoint) + . interpretGalleyAPIAccessToRpc e.disabledVersions e.galleyEndpoint . passwordResetCodeStoreToCassandra @Cas.Client . randomToIO . runDelay - . nowToIOAction (e ^. currentTime) + . nowToIOAction e.currentTime . userPendingActivationStoreToCassandra - . interpretBlacklistStoreToCassandra @Cas.Client + . interpretBlockListStoreToCassandra e.casClient . interpretJwtTools . interpretPublicKeyBundle . interpretJwk - . interpretFederationDomainConfig (e ^. settings . federationStrategy) (foldMap (remotesMapFromCfgFile . fmap (.federationDomainConfig)) (e ^. settings . federationDomainConfigs)) - . runGundeckAPIAccess (e ^. gundeckEndpoint) - . runNotificationSubsystemGundeck (defaultNotificationSubsystemConfig (e ^. App.requestId)) - . runInputConst (e ^. settings . Opt.allowlistEmailDomains) - . runInputConst (toLocalUnsafe (e ^. settings . Opt.federationDomain) ()) + . interpretFederationDomainConfig e.casClient e.settings.federationStrategy (foldMap (remotesMapFromCfgFile . fmap (.federationDomainConfig)) e.settings.federationDomainConfigs) + . runGundeckAPIAccess e.gundeckEndpoint + . runNotificationSubsystemGundeck (defaultNotificationSubsystemConfig e.requestId) + . runInputConst (teamTemplatesNoLocale e) + . runInputConst e.settings.allowlistEmailDomains + . runInputConst (toLocalUnsafe e.settings.federationDomain ()) . runInputSem (embed getCurrentTime) - . runInputConst (e ^. settings . to Opt.set2FACodeGenerationDelaySecs . to fromIntegral) + . runInputConst (fromIntegral $ Opt.twoFACodeGenerationDelaySecs e.settings) + . runInputConst userSubsystemConfig . connectionStoreToCassandra - . interpretSFT (e ^. httpManager) - . interpretVerificationCodeStoreCassandra (e ^. casClient) - . interpretPasswordStore (e ^. casClient) - . interpretSessionStoreCassandra (e ^. casClient) - . interpretUserStoreCassandra (e ^. casClient) - . interpretUserKeyStoreCassandra (e ^. casClient) - . runHashPassword + . interpretSFT e.httpManager + . interpretPropertyStoreCassandra e.casClient + . interpretInvitationStoreToCassandra e.casClient + . interpretActivationCodeStoreToCassandra e.casClient + . interpretVerificationCodeStoreCassandra e.casClient + . interpretPasswordStore e.casClient + . interpretSessionStoreCassandra e.casClient + . interpretIndexedUserStoreES indexedUserStoreConfig + . interpretUserStoreCassandra e.casClient + . interpretUserKeyStoreCassandra e.casClient + . runHashPassword e.settings.passwordHashingOptions . interpretFederationAPIAccess federationApiAccessConfig . rethrowHttpErrorIO + . mapError propertySubsystemErrorToHttpError . mapError verificationCodeSubsystemErrorToHttpError . mapError (StdError . federationErrorToWai) . mapError authenticationSubsystemErrorToHttpError + . mapError teamInvitationErrorToHttpError . mapError userSubsystemErrorToHttpError - . runUserEvents - . runDeleteQueue (e ^. internalEvents) + . runEvents + . runDeleteQueue e.internalEvents + . interpretPropertySubsystem propertySubsystemConfig . interpretVerificationCodeSubsystem - . emailSubsystemInterpreter (e ^. usrTemplates) (e ^. templateBranding) - . runUserSubsystem userSubsystemConfig - . interpretAuthenticationSubsystem + . emailSubsystemInterpreter e.userTemplates e.teamTemplates e.templateBranding + . userSubsystemInterpreter + . runTeamInvitationSubsystem teamInvitationSubsystemConfig + . authSubsystemInterpreter ) ) $ runReaderT ma e @@ -213,6 +294,6 @@ rethrowHttpErrorIO act = do emailSendingInterpreter :: (Member (Embed IO) r) => Env -> InterpreterFor EmailSending r emailSendingInterpreter e = do - case (e ^. smtpEnv) of - Just smtp -> emailViaSMTPInterpreter (e ^. applog) smtp - Nothing -> emailViaSESInterpreter (e ^. awsEnv . amazonkaEnv) + case e.smtpEnv of + Just smtp -> emailViaSMTPInterpreter e.appLogger smtp + Nothing -> emailViaSESInterpreter (e.awsEnv ^. amazonkaEnv) diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index d665051b8ce..ae9ce48899f 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -23,15 +23,13 @@ module Brig.Data.Activation activationErrorToRegisterError, newActivation, mkActivationKey, - lookupActivationCode, activateKey, verifyCode, ) where -import Brig.App (Env, adhocUserKeyStoreInterpreter) +import Brig.App (AppT, adhocUserKeyStoreInterpreter, liftSem, qualifyLocal, wrapClient, wrapClientE) import Brig.Data.User -import Brig.Options import Brig.Types.Intra import Cassandra import Control.Error @@ -45,12 +43,15 @@ import OpenSSL.BN (randIntegerZeroToNMinusOne) import OpenSSL.EVP.Digest (digestBS, getDigestByName) import Polysemy import Text.Printf (printf) +import Util.Timeout import Wire.API.User import Wire.API.User.Activation import Wire.API.User.Password -import Wire.PasswordResetCodeStore qualified as E -import Wire.PasswordResetCodeStore.Cassandra +import Wire.PasswordResetCodeStore (PasswordResetCodeStore) +import Wire.PasswordResetCodeStore qualified as Password import Wire.UserKeyStore +import Wire.UserSubsystem (UserSubsystem) +import Wire.UserSubsystem qualified as User -- | The information associated with the pending activation of a 'UserKey'. data Activation = Activation @@ -65,7 +66,7 @@ data ActivationError = UserKeyExists !LT.Text | InvalidActivationCodeWrongUser | InvalidActivationCodeWrongCode - | InvalidActivationEmail !Email !String + | InvalidActivationEmail !EmailAddress !String | InvalidActivationPhone !Phone activationErrorToRegisterError :: ActivationError -> RegisterError @@ -77,8 +78,9 @@ activationErrorToRegisterError = \case InvalidActivationPhone _ -> RegisterErrorInvalidPhone data ActivationEvent - = AccountActivated !UserAccount - | EmailActivated !UserId !Email + = AccountActivated !User + | EmailActivated !UserId !EmailAddress + deriving (Show) -- | Max. number of activation attempts per 'ActivationKey'. maxAttempts :: Int32 @@ -86,31 +88,43 @@ maxAttempts = 3 -- docs/reference/user/activation.md {#RefActivationSubmit} activateKey :: - forall m. - (MonadClient m, MonadReader Env m) => + forall r. + ( Member UserSubsystem r, + Member PasswordResetCodeStore r + ) => ActivationKey -> ActivationCode -> Maybe UserId -> - ExceptT ActivationError m (Maybe ActivationEvent) -activateKey k c u = verifyCode k c >>= pickUser >>= activate + ExceptT ActivationError (AppT r) (Maybe ActivationEvent) +activateKey k c u = wrapClientE (verifyCode k c) >>= pickUser >>= activate where + pickUser :: (t, Maybe UserId) -> ExceptT ActivationError (AppT r) (t, UserId) pickUser (uk, u') = maybe (throwE invalidUser) (pure . (uk,)) (u <|> u') - activate (key :: EmailKey, uid) = do - a <- lift (lookupAccount uid) >>= maybe (throwE invalidUser) pure - unless (accountStatus a == Active) $ -- this is never 'PendingActivation' in the flow this function is used in. + + activate :: (EmailKey, UserId) -> ExceptT ActivationError (AppT r) (Maybe ActivationEvent) + activate (key, uid) = do + luid <- qualifyLocal uid + a <- lift (liftSem $ User.getAccountNoFilter luid) >>= maybe (throwE invalidUser) pure + unless (userStatus a == Active) $ -- this is never 'PendingActivation' in the flow this function is used in. throwE invalidCode - case userIdentity (accountUser a) of + case userIdentity a of Nothing -> do claim key uid let ident = EmailIdentity (emailKeyOrig key) - lift $ activateUser uid ident - let a' = a {accountUser = (accountUser a) {userIdentity = Just ident}} + wrapClientE (activateUser uid ident) + let a' = a {userIdentity = Just ident} pure . Just $ AccountActivated a' Just _ -> do - let usr = accountUser a - profileNeedsUpdate = Just (emailKeyOrig key) /= userEmail usr - oldKey :: Maybe EmailKey = mkEmailKey <$> userEmail usr + let profileNeedsUpdate = Just (emailKeyOrig key) /= userEmail a + oldKey :: Maybe EmailKey = mkEmailKey <$> userEmail a in handleExistingIdentity uid profileNeedsUpdate oldKey key + + handleExistingIdentity :: + UserId -> + Bool -> + Maybe EmailKey -> + EmailKey -> + ExceptT ActivationError (AppT r) (Maybe ActivationEvent) handleExistingIdentity uid profileNeedsUpdate oldKey key | oldKey == Just key && not profileNeedsUpdate = pure Nothing -- activating existing key and exactly same profile @@ -120,15 +134,17 @@ activateKey k c u = verifyCode k c >>= pickUser >>= activate pure . Just $ EmailActivated uid (emailKeyOrig key) -- if the key is the same, we only want to update our profile | otherwise = do - lift (runM (passwordResetCodeStoreToCassandra @m @'[Embed m] (E.codeDelete (mkPasswordResetKey uid)))) + lift . liftSem $ Password.codeDelete (mkPasswordResetKey uid) claim key uid lift $ updateEmailAndDeleteEmailUnvalidated uid (emailKeyOrig key) for_ oldKey $ lift . adhocUserKeyStoreInterpreter . deleteKey pure . Just $ EmailActivated uid (emailKeyOrig key) where - updateEmailAndDeleteEmailUnvalidated :: UserId -> Email -> m () + updateEmailAndDeleteEmailUnvalidated :: UserId -> EmailAddress -> AppT r () updateEmailAndDeleteEmailUnvalidated u' email = - updateEmail u' email <* deleteEmailUnvalidated u' + wrapClient (updateEmail u' email <* deleteEmailUnvalidated u') + + claim :: EmailKey -> UserId -> ExceptT ActivationError (AppT r) () claim key uid = do ok <- lift $ adhocUserKeyStoreInterpreter (claimKey key uid) unless ok $ @@ -158,12 +174,6 @@ newActivation uk timeout u = do ActivationCode . Ascii.unsafeFromText . pack . printf "%06d" <$> randIntegerZeroToNMinusOne 1000000 --- | Lookup an activation code and it's associated owner (if any) for a 'UserKey'. -lookupActivationCode :: (MonadClient m) => EmailKey -> m (Maybe (Maybe UserId, ActivationCode)) -lookupActivationCode k = - liftIO (mkActivationKey k) - >>= retry x1 . query1 codeSelect . params LocalQuorum . Identity - -- | Verify an activation code. verifyCode :: (MonadClient m) => @@ -180,7 +190,7 @@ verifyCode key code = do | otherwise -> revoke >> throwE invalidCode Nothing -> throwE invalidCode where - mkScope "email" k u = case parseEmail k of + mkScope "email" k u = case emailAddressText k of Just e -> pure (mkEmailKey e, u) Nothing -> throwE invalidCode mkScope _ _ _ = throwE invalidCode @@ -212,8 +222,5 @@ keyInsert = keySelect :: PrepQuery R (Identity ActivationKey) (Int32, Ascii, Text, ActivationCode, Maybe UserId, Int32) keySelect = "SELECT ttl(code) as ttl, key_type, key_text, code, user, retries FROM activation_keys WHERE key = ?" -codeSelect :: PrepQuery R (Identity ActivationKey) (Maybe UserId, ActivationCode) -codeSelect = "SELECT user, code FROM activation_keys WHERE key = ?" - keyDelete :: PrepQuery W (Identity ActivationKey) () keyDelete = "DELETE FROM activation_keys WHERE key = ?" diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index ef2909b35d9..d82762802e5 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -27,7 +27,6 @@ module Brig.Data.Client addClientWithReAuthPolicy, addClient, rmClient, - hasClient, lookupClient, lookupClients, lookupPubClientsBulk, @@ -56,8 +55,6 @@ import Amazonka.DynamoDB.Lens qualified as AWS import Bilge.Retry (httpHandlers) import Brig.AWS import Brig.App -import Brig.Data.User (AuthError (..), ReAuthError (..)) -import Brig.Data.User qualified as User import Brig.Types.Instances () import Cassandra as C hiding (Client) import Cassandra.Settings as C hiding (Client) @@ -74,11 +71,13 @@ import Data.HashMap.Strict qualified as HashMap import Data.Id import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) import Data.Map qualified as Map +import Data.Qualified import Data.Set qualified as Set import Data.Text qualified as Text import Data.Time.Clock import Data.UUID qualified as UUID import Imports +import Polysemy (Member) import Prometheus qualified as Prom import System.CryptoBox (Result (Success)) import System.CryptoBox qualified as CryptoBox @@ -90,6 +89,9 @@ import Wire.API.User.Auth import Wire.API.User.Client hiding (UpdateClient (..)) import Wire.API.User.Client.Prekey import Wire.API.UserMap (UserMap (..)) +import Wire.AuthenticationSubsystem (AuthenticationSubsystem) +import Wire.AuthenticationSubsystem qualified as Authentication +import Wire.AuthenticationSubsystem.Error data ClientDataError = TooManyClients @@ -116,36 +118,39 @@ reAuthForNewClients :: ReAuthPolicy reAuthForNewClients count upsert = count > 0 && not upsert addClient :: - (MonadClient m, MonadReader Brig.App.Env m) => - UserId -> + ( Member AuthenticationSubsystem r + ) => + Local UserId -> ClientId -> NewClient -> Int -> - Maybe (Imports.Set ClientCapability) -> - ExceptT ClientDataError m (Client, [Client], Word) + Maybe ClientCapabilityList -> + ExceptT ClientDataError (AppT r) (Client, [Client], Word) addClient = addClientWithReAuthPolicy reAuthForNewClients addClientWithReAuthPolicy :: - (MonadClient m, MonadReader Brig.App.Env m) => + ( MonadReader Brig.App.Env (AppT r), + Member AuthenticationSubsystem r + ) => ReAuthPolicy -> - UserId -> + Local UserId -> ClientId -> NewClient -> Int -> - Maybe (Imports.Set ClientCapability) -> - ExceptT ClientDataError m (Client, [Client], Word) -addClientWithReAuthPolicy reAuthPolicy u newId c maxPermClients cps = do - clients <- lookupClients u + Maybe ClientCapabilityList -> + ExceptT ClientDataError (AppT r) (Client, [Client], Word) +addClientWithReAuthPolicy reAuthPolicy u newId c maxPermClients caps = do + clients <- wrapClientE $ lookupClients (tUnqualified u) let typed = filter ((== newClientType c) . clientType) clients let count = length typed let upsert = any exists typed - when (reAuthPolicy count upsert) $ - fmapLT ClientReAuthError $ - User.reauthenticate u (newClientPassword c) + when (reAuthPolicy count upsert) do + (lift . liftSem $ Authentication.reauthenticateEither (tUnqualified u) (newClientPassword c)) + >>= either (throwE . ClientReAuthError) pure let capacity = fmap (+ (-count)) limit unless (maybe True (> 0) capacity || upsert) $ throwE TooManyClients - new <- insert + new <- wrapClientE $ insert (tUnqualified u) let !total = fromIntegral (length clients + if upsert then 0 else 1) let old = maybe (filter (not . exists) typed) (const []) limit pure (new, old, total) @@ -159,16 +164,16 @@ addClientWithReAuthPolicy reAuthPolicy u newId c maxPermClients cps = do exists :: Client -> Bool exists = (==) newId . clientId - insert :: (MonadClient m, MonadReader Brig.App.Env m) => ExceptT ClientDataError m Client - insert = do + insert :: (MonadClient m, MonadReader Brig.App.Env m) => UserId -> ExceptT ClientDataError m Client + insert uid = do -- Is it possible to do this somewhere else? Otherwise we could use `MonadClient` instead - now <- toUTCTimeMillis <$> (liftIO =<< view currentTime) + now <- toUTCTimeMillis <$> (liftIO =<< asks (.currentTime)) let keys = unpackLastPrekey (newClientLastKey c) : newClientPrekeys c - updatePrekeys u newId keys + updatePrekeys uid newId keys let mdl = newClientModel c - prm = (u, newId, now, newClientType c, newClientLabel c, newClientClass c, newClientCookie c, mdl, C.Set . Set.toList <$> cps) + prm = (uid, newId, now, newClientType c, newClientLabel c, newClientClass c, newClientCookie c, mdl, C.Set . Set.toList . fromClientCapabilityList <$> caps) retry x5 $ write insertClient (params LocalQuorum prm) - addMLSPublicKeys u newId (Map.assocs (newClientMLSPublicKeys c)) + addMLSPublicKeys uid newId (Map.assocs (newClientMLSPublicKeys c)) pure $! Client { clientId = newId, @@ -178,7 +183,7 @@ addClientWithReAuthPolicy reAuthPolicy u newId c maxPermClients cps = do clientLabel = newClientLabel c, clientCookie = newClientCookie c, clientModel = mdl, - clientCapabilities = ClientCapabilityList (fromMaybe mempty cps), + clientCapabilities = fromMaybe mempty caps, clientMLSPublicKeys = mempty, clientLastActive = Nothing } @@ -238,9 +243,6 @@ lookupPrekeyIds u c = map runIdentity <$> retry x1 (query selectPrekeyIds (params LocalQuorum (u, c))) -hasClient :: (MonadClient m) => UserId -> ClientId -> m Bool -hasClient u d = isJust <$> retry x1 (query1 checkClient (params LocalQuorum (u, d))) - rmClient :: ( MonadClient m, MonadReader Brig.App.Env m, @@ -252,13 +254,13 @@ rmClient :: rmClient u c = do retry x5 $ write removeClient (params LocalQuorum (u, c)) retry x5 $ write removeClientKeys (params LocalQuorum (u, c)) - unlessM (isJust <$> view randomPrekeyLocalLock) $ deleteOptLock u c + unlessM (isJust <$> asks (.randomPrekeyLocalLock)) $ deleteOptLock u c updateClientLabel :: (MonadClient m) => UserId -> ClientId -> Maybe Text -> m () updateClientLabel u c l = retry x5 $ write updateClientLabelQuery (params LocalQuorum (l, u, c)) -updateClientCapabilities :: (MonadClient m) => UserId -> ClientId -> Maybe (Imports.Set ClientCapability) -> m () -updateClientCapabilities u c fs = retry x5 $ write updateClientCapabilitiesQuery (params LocalQuorum (C.Set . Set.toList <$> fs, u, c)) +updateClientCapabilities :: (MonadClient m) => UserId -> ClientId -> Maybe ClientCapabilityList -> m () +updateClientCapabilities u c fs = retry x5 $ write updateClientCapabilitiesQuery (params LocalQuorum (C.Set . Set.toList . fromClientCapabilityList <$> fs, u, c)) -- | If the update fails, which can happen if device does not exist, then ignore the error silently. updateClientLastActive :: (MonadClient m) => UserId -> ClientId -> UTCTime -> m () @@ -295,7 +297,7 @@ claimPrekey :: ClientId -> m (Maybe ClientPrekey) claimPrekey u c = - view randomPrekeyLocalLock >>= \case + asks (.randomPrekeyLocalLock) >>= \case -- Use random prekey selection strategy Just localLock -> withLocalLock localLock $ do prekeys <- retry x1 $ query userPrekeys (params LocalQuorum (u, c)) @@ -416,9 +418,6 @@ selectPrekeyIds = "SELECT key FROM prekeys where user = ? and client = ?" removePrekey :: PrepQuery W (UserId, ClientId, PrekeyId) () removePrekey = "DELETE FROM prekeys where user = ? and client = ? and key = ?" -checkClient :: PrepQuery R (UserId, ClientId) (Identity ClientId) -checkClient = "SELECT client from clients where user = ? and client = ?" - selectMLSPublicKey :: PrepQuery R (UserId, ClientId, SignatureSchemeTag) (Identity Blob) selectMLSPublicKey = "SELECT key from mls_public_keys where user = ? and client = ? and sig_scheme = ?" @@ -490,8 +489,8 @@ deleteOptLock :: ClientId -> m () deleteOptLock u c = do - t <- view (awsEnv . prekeyTable) - e <- view (awsEnv . amazonkaEnv) + t <- asks ((.awsEnv) <&> view prekeyTable) + e <- asks ((.awsEnv) <&> view amazonkaEnv) void $ exec e (AWS.newDeleteItem t & AWS.deleteItem_key .~ key u c) withOptLock :: @@ -561,8 +560,8 @@ withOptLock u c ma = go (10 :: Int) (Text -> r) -> m (Maybe x) execDyn cnv mkCmd = do - cmd <- mkCmd <$> view (awsEnv . prekeyTable) - e <- view (awsEnv . amazonkaEnv) + cmd <- mkCmd <$> asks ((.awsEnv) <&> view prekeyTable) + e <- asks ((.awsEnv) <&> view amazonkaEnv) liftIO $ execDyn' e cnv cmd where execDyn' :: diff --git a/services/brig/src/Brig/Data/Connection.hs b/services/brig/src/Brig/Data/Connection.hs index ff843f215f4..fbe8221018e 100644 --- a/services/brig/src/Brig/Data/Connection.hs +++ b/services/brig/src/Brig/Data/Connection.hs @@ -21,7 +21,6 @@ module Brig.Data.Connection updateConnection, updateConnectionStatus, lookupConnection, - lookupRelation, lookupLocalConnectionsPage, lookupRemoteConnectionsPage, lookupRelationWithHistory, @@ -33,17 +32,14 @@ module Brig.Data.Connection lookupLocalConnectionStatuses, lookupRemoteConnectionStatuses, lookupAllStatuses, - lookupRemoteConnectedUsersC, lookupRemoteConnectedUsersPaginated, countConnections, deleteConnections, deleteRemoteConnections, - deleteRemoteConnectionsDomain, remoteConnectionInsert, remoteConnectionSelect, remoteConnectionSelectFrom, remoteConnectionDelete, - remoteConnectionSelectFromDomain, remoteConnectionClear, -- * Re-exports @@ -56,7 +52,7 @@ import Brig.Data.Types as T import Cassandra import Control.Monad.Morph import Control.Monad.Trans.Maybe -import Data.Conduit (ConduitT, runConduit, (.|)) +import Data.Conduit (runConduit, (.|)) import Data.Conduit.List qualified as C import Data.Domain (Domain) import Data.Id @@ -154,12 +150,6 @@ lookupRelationWithHistory self target = do query1 remoteRelationSelect (params LocalQuorum (tUnqualified self, domain, rtarget)) runIdentity <$$> retry x1 (foldQualified self local remote target) -lookupRelation :: (MonadClient m) => Local UserId -> Qualified UserId -> m Relation -lookupRelation self target = - lookupRelationWithHistory self target <&> \case - Nothing -> Cancelled - Just relh -> (relationDropHistory relh) - -- | For a given user 'A', lookup their outgoing connections (A -> X) to other users. lookupLocalConnections :: (MonadClient m) => @@ -267,11 +257,6 @@ lookupAllStatuses lfroms = do map (\(d, u, r) -> toConnectionStatusV2 from d u r) <$> retry x1 (query remoteRelationsSelectAll (params LocalQuorum (Identity from))) -lookupRemoteConnectedUsersC :: forall m. (MonadClient m) => Local UserId -> Int32 -> ConduitT () [Remote UserConnection] m () -lookupRemoteConnectedUsersC u maxResults = - paginateC remoteConnectionSelect (paramsP LocalQuorum (Identity (tUnqualified u)) maxResults) x1 - .| C.map (\xs -> map (\x@(d, _, _, _, _, _) -> toRemoteUnsafe d (toRemoteUserConnection u x)) xs) - lookupRemoteConnectedUsersPaginated :: (MonadClient m) => Local UserId -> Int32 -> m (Page (Remote UserConnection)) lookupRemoteConnectedUsersPaginated u maxResults = do (\x@(d, _, _, _, _, _) -> toRemoteUnsafe d (toRemoteUserConnection u x)) <$$> retry x1 (paginate remoteConnectionSelect (paramsP LocalQuorum (Identity (tUnqualified u)) maxResults)) @@ -329,14 +314,6 @@ deleteRemoteConnections (tUntagged -> Qualified remoteUser remoteDomain) (fromRa pooledForConcurrentlyN_ 16 locals $ \u -> write remoteConnectionDelete $ params LocalQuorum (u, remoteDomain, remoteUser) -deleteRemoteConnectionsDomain :: (MonadClient m, MonadUnliftIO m) => Domain -> m () -deleteRemoteConnectionsDomain dom = do - -- Select all triples for the given domain, and then delete them - runConduit $ - paginateC remoteConnectionSelectFromDomain (paramsP LocalQuorum (pure dom) 100) x1 - .| C.mapM_ - (pooledMapConcurrentlyN_ 16 $ write remoteConnectionDelete . params LocalQuorum) - -- Queries connectionInsert :: PrepQuery W (UserId, UserId, RelationWithHistory, UTCTimeMillis, ConvId) () @@ -399,9 +376,6 @@ remoteConnectionUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDA remoteConnectionDelete :: PrepQuery W (UserId, Domain, UserId) () remoteConnectionDelete = "DELETE FROM connection_remote where left = ? AND right_domain = ? AND right_user = ?" -remoteConnectionSelectFromDomain :: PrepQuery R (Identity Domain) (UserId, Domain, UserId) -remoteConnectionSelectFromDomain = "SELECT left, right_domain, right_user FROM connection_remote where right_domain = ?" - remoteConnectionClear :: PrepQuery W (Identity UserId) () remoteConnectionClear = "DELETE FROM connection_remote where left = ?" diff --git a/services/brig/src/Brig/Data/LoginCode.hs b/services/brig/src/Brig/Data/LoginCode.hs deleted file mode 100644 index 3103e939747..00000000000 --- a/services/brig/src/Brig/Data/LoginCode.hs +++ /dev/null @@ -1,93 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - --- | Persistent storage for login codes. --- TODO: Use Brig.Data.Codes --- TODO: Move to Brig.User.Auth.DB.LoginCode -module Brig.Data.LoginCode - ( LoginCode, - createLoginCode, - verifyLoginCode, - lookupLoginCode, - ) -where - -import Brig.App (Env, currentTime) -import Cassandra -import Control.Lens (view) -import Data.Code -import Data.Id -import Data.Text qualified as T -import Data.Time.Clock -import Imports -import OpenSSL.BN (randIntegerZeroToNMinusOne) -import Text.Printf (printf) -import Wire.API.User.Auth - --- | Max. number of verification attempts per code. -maxAttempts :: Int32 -maxAttempts = 3 - --- | Timeout of individual codes. -ttl :: NominalDiffTime -ttl = 600 - -createLoginCode :: (MonadClient m, MonadReader Env m) => UserId -> m PendingLoginCode -createLoginCode u = do - now <- liftIO =<< view currentTime - code <- liftIO genCode - insertLoginCode u code maxAttempts (ttl `addUTCTime` now) - pure $! PendingLoginCode code (Timeout ttl) - where - genCode = LoginCode . T.pack . printf "%06d" <$> randIntegerZeroToNMinusOne 1000000 - -verifyLoginCode :: (MonadClient m, MonadReader Env m) => UserId -> LoginCode -> m Bool -verifyLoginCode u c = do - code <- retry x1 (query1 codeSelect (params LocalQuorum (Identity u))) - now <- liftIO =<< view currentTime - case code of - Just (c', _, t) | c == c' && t >= now -> deleteLoginCode u >> pure True - Just (c', n, t) | n > 1 && t > now -> insertLoginCode u c' (n - 1) t >> pure False - Just (_, _, _) -> deleteLoginCode u >> pure False - Nothing -> pure False - -lookupLoginCode :: (MonadReader Env m, MonadClient m) => UserId -> m (Maybe PendingLoginCode) -lookupLoginCode u = do - now <- liftIO =<< view currentTime - validate now =<< retry x1 (query1 codeSelect (params LocalQuorum (Identity u))) - where - validate now (Just (c, _, t)) | now < t = pure (Just (pending c now t)) - validate _ _ = pure Nothing - pending c now t = PendingLoginCode c (timeout now t) - timeout now t = Timeout (t `diffUTCTime` now) - -deleteLoginCode :: (MonadClient m) => UserId -> m () -deleteLoginCode u = retry x5 . write codeDelete $ params LocalQuorum (Identity u) - -insertLoginCode :: (MonadClient m) => UserId -> LoginCode -> Int32 -> UTCTime -> m () -insertLoginCode u c n t = retry x5 . write codeInsert $ params LocalQuorum (u, c, n, t, round ttl) - --- Queries - -codeInsert :: PrepQuery W (UserId, LoginCode, Int32, UTCTime, Int32) () -codeInsert = "INSERT INTO login_codes (user, code, retries, timeout) VALUES (?, ?, ?, ?) USING TTL ?" - -codeSelect :: PrepQuery R (Identity UserId) (LoginCode, Int32, UTCTime) -codeSelect = "SELECT code, retries, timeout FROM login_codes WHERE user = ?" - -codeDelete :: PrepQuery W (Identity UserId) () -codeDelete = "DELETE FROM login_codes WHERE user = ?" diff --git a/services/brig/src/Brig/Data/MLS/KeyPackage.hs b/services/brig/src/Brig/Data/MLS/KeyPackage.hs index de816cbbbf0..20f95a40afa 100644 --- a/services/brig/src/Brig/Data/MLS/KeyPackage.hs +++ b/services/brig/src/Brig/Data/MLS/KeyPackage.hs @@ -26,11 +26,10 @@ where import Brig.API.MLS.KeyPackages.Validation import Brig.App -import Brig.Options hiding (Timeout) +import Brig.Options import Cassandra import Control.Arrow import Control.Error -import Control.Lens import Control.Monad.Random (randomRIO) import Data.Functor import Data.Id @@ -70,7 +69,7 @@ claimKeyPackage :: MaybeT m (KeyPackageRef, KeyPackageData) claimKeyPackage u c suite = do -- FUTUREWORK: investigate better locking strategies - lock <- lift $ view keyPackageLocalLock + lock <- lift $ asks (.keyPackageLocalLock) -- get a random key package and delete it (ref, kpd) <- MaybeT . withMVar lock . const $ do kps <- getNonClaimedKeyPackages u c suite @@ -98,7 +97,7 @@ getNonClaimedKeyPackages u c suite = do let decodedKps = foldMap (keepDecoded . (decodeKp &&& id)) kps now <- liftIO getPOSIXTime - mMaxLifetime <- setKeyPackageMaximumLifetime <$> view settings + mMaxLifetime <- asks (.settings.keyPackageMaximumLifetime) let (kpsExpired, kpsNonExpired) = partition (hasExpired now mMaxLifetime) decodedKps @@ -145,10 +144,10 @@ deleteKeyPackages u c suite refs = deleteQuery = "DELETE FROM mls_key_packages WHERE user = ? AND client = ? AND cipher_suite = ? AND ref in ?" deleteAllKeyPackages :: - (MonadClient m, MonadUnliftIO m) => + (MonadClient m, MonadUnliftIO m, Foldable f) => UserId -> ClientId -> - [CipherSuiteTag] -> + f CipherSuiteTag -> m () deleteAllKeyPackages u c suites = pooledForConcurrentlyN_ 16 suites $ \suite -> diff --git a/services/brig/src/Brig/Data/Properties.hs b/services/brig/src/Brig/Data/Properties.hs deleted file mode 100644 index 6fd099d8620..00000000000 --- a/services/brig/src/Brig/Data/Properties.hs +++ /dev/null @@ -1,95 +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 Brig.Data.Properties - ( PropertiesDataError (..), - insertProperty, - deleteProperty, - clearProperties, - lookupProperty, - lookupPropertyKeys, - lookupPropertyKeysAndValues, - ) -where - -import Cassandra -import Control.Error -import Data.Id -import Imports -import Wire.API.Properties - -maxProperties :: Int64 -maxProperties = 16 - -data PropertiesDataError - = TooManyProperties - -insertProperty :: - (MonadClient m) => - UserId -> - PropertyKey -> - RawPropertyValue -> - ExceptT PropertiesDataError m () -insertProperty u k v = do - n <- lift . fmap (maybe 0 runIdentity) . retry x1 $ query1 propertyCount (params LocalQuorum (Identity u)) - unless (n < maxProperties) $ - throwE TooManyProperties - lift . retry x5 $ write propertyInsert (params LocalQuorum (u, k, v)) - -deleteProperty :: (MonadClient m) => UserId -> PropertyKey -> m () -deleteProperty u k = retry x5 $ write propertyDelete (params LocalQuorum (u, k)) - -clearProperties :: (MonadClient m) => UserId -> m () -clearProperties u = retry x5 $ write propertyReset (params LocalQuorum (Identity u)) - -lookupProperty :: (MonadClient m) => UserId -> PropertyKey -> m (Maybe RawPropertyValue) -lookupProperty u k = - fmap runIdentity - <$> retry x1 (query1 propertySelect (params LocalQuorum (u, k))) - -lookupPropertyKeys :: (MonadClient m) => UserId -> m [PropertyKey] -lookupPropertyKeys u = - map runIdentity - <$> retry x1 (query propertyKeysSelect (params LocalQuorum (Identity u))) - -lookupPropertyKeysAndValues :: (MonadClient m) => UserId -> m [(PropertyKey, RawPropertyValue)] -lookupPropertyKeysAndValues u = - retry x1 (query propertyKeysValuesSelect (params LocalQuorum (Identity u))) - -------------------------------------------------------------------------------- --- Queries - -propertyInsert :: PrepQuery W (UserId, PropertyKey, RawPropertyValue) () -propertyInsert = "INSERT INTO properties (user, key, value) VALUES (?, ?, ?)" - -propertyDelete :: PrepQuery W (UserId, PropertyKey) () -propertyDelete = "DELETE FROM properties where user = ? and key = ?" - -propertyReset :: PrepQuery W (Identity UserId) () -propertyReset = "DELETE FROM properties where user = ?" - -propertySelect :: PrepQuery R (UserId, PropertyKey) (Identity RawPropertyValue) -propertySelect = "SELECT value FROM properties where user = ? and key = ?" - -propertyKeysSelect :: PrepQuery R (Identity UserId) (Identity PropertyKey) -propertyKeysSelect = "SELECT key FROM properties where user = ?" - -propertyKeysValuesSelect :: PrepQuery R (Identity UserId) (PropertyKey, RawPropertyValue) -propertyKeysValuesSelect = "SELECT key, value FROM properties where user = ?" - -propertyCount :: PrepQuery R (Identity UserId) (Identity Int64) -propertyCount = "SELECT COUNT(*) FROM properties where user = ?" diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 7dcd1ed89d5..7e1f8e57656 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -1,4 +1,5 @@ -{-# OPTIONS_GHC -fno-warn-orphans #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} +{-# OPTIONS_GHC -Wno-orphans #-} -- This file is part of the Wire Server implementation. -- @@ -17,27 +18,15 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . --- for Show UserRowInsert - --- TODO: Move to Brig.User.Account.DB module Brig.Data.User - ( AuthError (..), - ReAuthError (..), - newAccount, + ( newAccount, newAccountInviteViaScim, insertAccount, - authenticate, - reauthenticate, - filterActive, - isSamlUser, -- * Lookups - lookupAccount, - lookupAccounts, lookupUser, lookupUsers, lookupName, - lookupRichInfo, lookupRichInfoMultiUsers, lookupUserTeam, lookupServiceUsers, @@ -73,9 +62,9 @@ import Control.Lens hiding (from) import Data.Conduit (ConduitM) import Data.Domain import Data.Handle (Handle) +import Data.HavePendingInvitations import Data.Id import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) -import Data.Misc import Data.Qualified import Data.Range (fromRange) import Data.Time (addUTCTime) @@ -84,26 +73,10 @@ import Imports import Polysemy import Wire.API.Password import Wire.API.Provider.Service -import Wire.API.Team.Feature qualified as ApiFt +import Wire.API.Team.Feature import Wire.API.User import Wire.API.User.RichInfo -import Wire.PasswordStore - --- | Authentication errors. -data AuthError - = AuthInvalidUser - | AuthInvalidCredentials - | AuthSuspended - | AuthEphemeral - | AuthPendingInvitation - --- | Re-authentication errors. -data ReAuthError - = ReAuthError !AuthError - | ReAuthMissingPassword - | ReAuthCodeVerificationRequired - | ReAuthCodeVerificationNoPendingCode - | ReAuthCodeVerificationNoEmail +import Wire.HashPassword -- | Preconditions: -- @@ -113,9 +86,15 @@ data ReAuthError -- Condition (2.) is essential for maintaining handle uniqueness. It is guaranteed by the -- fact that we're setting getting @mbHandle@ from table @"user"@, and when/if it was added -- there, it was claimed properly. -newAccount :: (MonadClient m, MonadReader Env m) => NewUser -> Maybe InvitationId -> Maybe TeamId -> Maybe Handle -> m (UserAccount, Maybe Password) +newAccount :: + (Member HashPassword r) => + NewUser -> + Maybe InvitationId -> + Maybe TeamId -> + Maybe Handle -> + AppT r (User, Maybe Password) newAccount u inv tid mbHandle = do - defLoc <- setDefaultUserLocale <$> view settings + defLoc <- defaultUserLocale <$> asks (.settings) domain <- viewFederationDomain uid <- Id <$> do @@ -123,17 +102,17 @@ newAccount u inv tid mbHandle = do (Just (toUUID -> uuid), _) -> pure uuid (_, Just uuid) -> pure uuid (Nothing, Nothing) -> liftIO nextRandom - passwd <- maybe (pure Nothing) (fmap Just . liftIO . mkSafePasswordScrypt) pass + passwd <- maybe (pure Nothing) (fmap Just . liftSem . hashPassword8) pass expiry <- case status of Ephemeral -> do -- Ephemeral users' expiry time is in expires_in (default sessionTokenTimeout) seconds - e <- view zauthEnv + e <- asks (.zauthEnv) let ZAuth.SessionTokenTimeout defTTL = e ^. ZAuth.settings . ZAuth.sessionTokenTimeout ttl = maybe defTTL fromRange (newUserExpiresIn u) - now <- liftIO =<< view currentTime + now <- liftIO =<< asks (.currentTime) pure . Just . toUTCTimeMillis $ addUTCTime (fromIntegral ttl) now _ -> pure Nothing - pure (UserAccount (user uid domain (locale defLoc) expiry) status, passwd) + pure (user uid domain (locale defLoc) expiry, passwd) where ident = newUserIdentity u pass = newUserPassword u @@ -148,86 +127,35 @@ newAccount u inv tid mbHandle = do locale defLoc = fromMaybe defLoc (newUserLocale u) managedBy = fromMaybe defaultManagedBy (newUserManagedBy u) prots = fromMaybe defSupportedProtocols (newUserSupportedProtocols u) - user uid domain l e = User (Qualified uid domain) ident name pict assets colour False l Nothing mbHandle e tid managedBy prots + user uid domain l e = User (Qualified uid domain) ident Nothing name Nothing pict assets colour status l Nothing mbHandle e tid managedBy prots -newAccountInviteViaScim :: (MonadReader Env m) => UserId -> TeamId -> Maybe Locale -> Name -> Email -> m UserAccount -newAccountInviteViaScim uid tid locale name email = do - defLoc <- setDefaultUserLocale <$> view settings +newAccountInviteViaScim :: (MonadReader Env m) => UserId -> Text -> TeamId -> Maybe Locale -> Name -> EmailAddress -> m User +newAccountInviteViaScim uid externalId tid locale name email = do + defLoc <- defaultUserLocale <$> asks (.settings) let loc = fromMaybe defLoc locale domain <- viewFederationDomain - pure (UserAccount (user domain loc) PendingInvitation) - where - user domain loc = - User - (Qualified uid domain) - (Just $ EmailIdentity email) - name - (Pict []) - [] - defaultAccentId - False - loc - Nothing - Nothing - Nothing - (Just tid) - ManagedByScim - defSupportedProtocols - --- | Mandatory password authentication. -authenticate :: forall r. (Member PasswordStore r) => UserId -> PlainTextPassword6 -> ExceptT AuthError (AppT r) () -authenticate u pw = - lift (wrapHttp $ lookupAuth u) >>= \case - Nothing -> throwE AuthInvalidUser - Just (_, Deleted) -> throwE AuthInvalidUser - Just (_, Suspended) -> throwE AuthSuspended - Just (_, Ephemeral) -> throwE AuthEphemeral - Just (_, PendingInvitation) -> throwE AuthPendingInvitation - Just (Nothing, _) -> throwE AuthInvalidCredentials - Just (Just pw', Active) -> - case verifyPasswordWithStatus pw pw' of - (False, _) -> throwE AuthInvalidCredentials - (True, PasswordStatusNeedsUpdate) -> do - -- FUTUREWORK(elland): 6char pwd allowed for now - -- throwE AuthStalePassword in the future - for_ (plainTextPassword8 . fromPlainTextPassword $ pw) (lift . hashAndUpdatePwd u) - (True, _) -> pure () - where - hashAndUpdatePwd :: UserId -> PlainTextPassword8 -> AppT r () - hashAndUpdatePwd uid pwd = do - hashed <- mkSafePasswordScrypt pwd - liftSem $ upsertHashedPassword uid hashed - --- | Password reauthentication. If the account has a password, reauthentication --- is mandatory. If the account has no password, or is an SSO user, and no password is given, --- reauthentication is a no-op. -reauthenticate :: (MonadClient m, MonadReader Env m) => UserId -> Maybe PlainTextPassword6 -> ExceptT ReAuthError m () -reauthenticate u pw = - lift (lookupAuth u) >>= \case - Nothing -> throwE (ReAuthError AuthInvalidUser) - Just (_, Deleted) -> throwE (ReAuthError AuthInvalidUser) - Just (_, Suspended) -> throwE (ReAuthError AuthSuspended) - Just (_, PendingInvitation) -> throwE (ReAuthError AuthPendingInvitation) - Just (Nothing, _) -> for_ pw $ const (throwE $ ReAuthError AuthInvalidCredentials) - Just (Just pw', Active) -> maybeReAuth pw' - Just (Just pw', Ephemeral) -> maybeReAuth pw' - where - maybeReAuth pw' = case pw of - Nothing -> unlessM (isSamlUser u) $ throwE ReAuthMissingPassword - Just p -> - unless (verifyPassword p pw') $ - throwE (ReAuthError AuthInvalidCredentials) - -isSamlUser :: (MonadClient m, MonadReader Env m) => UserId -> m Bool -isSamlUser uid = do - account <- lookupAccount uid - case userIdentity . accountUser =<< account of - Just (SSOIdentity (UserSSOId _) _) -> pure True - _ -> pure False + pure $ + User + (Qualified uid domain) + (Just $ SSOIdentity (UserScimExternalId externalId) (Just email)) + Nothing + name + Nothing + (Pict []) + [] + defaultAccentId + PendingInvitation + loc + Nothing + Nothing + Nothing + (Just tid) + ManagedByScim + defSupportedProtocols insertAccount :: (MonadClient m) => - UserAccount -> + User -> -- | If a bot: conversation and team -- (if a team conversation) Maybe (ConvId, Maybe TeamId) -> @@ -235,7 +163,7 @@ insertAccount :: -- | Whether the user is activated Bool -> m () -insertAccount (UserAccount u status) mbConv password activated = retry x5 . batch $ do +insertAccount u mbConv password activated = retry x5 . batch $ do setType BatchLogged setConsistency LocalQuorum let Locale l c = userLocale u @@ -243,6 +171,7 @@ insertAccount (UserAccount u status) mbConv password activated = retry x5 . batc userInsert ( userId u, userDisplayName u, + userTextStatus u, userPict u, userAssets u, userEmail u, @@ -250,7 +179,7 @@ insertAccount (UserAccount u status) mbConv password activated = retry x5 . batc userAccentId u, password, activated, - status, + userStatus u, userExpire u, l, c, @@ -277,10 +206,10 @@ insertAccount (UserAccount u status) mbConv password activated = retry x5 . batc "INSERT INTO service_team (provider, service, user, conv, team) \ \VALUES (?, ?, ?, ?, ?)" -updateEmail :: (MonadClient m) => UserId -> Email -> m () +updateEmail :: (MonadClient m) => UserId -> EmailAddress -> m () updateEmail u e = retry x5 $ write userEmailUpdate (params LocalQuorum (e, u)) -updateEmailUnvalidated :: (MonadClient m) => UserId -> Email -> m () +updateEmailUnvalidated :: (MonadClient m) => UserId -> EmailAddress -> m () updateEmailUnvalidated u e = retry x5 $ write userEmailUnvalidatedUpdate (params LocalQuorum (e, u)) updateSSOId :: (MonadClient m) => UserId -> Maybe UserSSOId -> m Bool @@ -298,13 +227,11 @@ updateManagedBy u h = retry x5 $ write userManagedByUpdate (params LocalQuorum ( updateRichInfo :: (MonadClient m) => UserId -> RichInfoAssocList -> m () updateRichInfo u ri = retry x5 $ write userRichInfoUpdate (params LocalQuorum (ri, u)) -updateFeatureConferenceCalling :: (MonadClient m) => UserId -> Maybe (ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig) -> m (Maybe (ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig)) -updateFeatureConferenceCalling uid mbStatus = do - let flag = ApiFt.wssStatus <$> mbStatus - retry x5 $ write update (params LocalQuorum (flag, uid)) - pure mbStatus +updateFeatureConferenceCalling :: (MonadClient m) => UserId -> Maybe FeatureStatus -> m () +updateFeatureConferenceCalling uid mStatus = + retry x5 $ write update (params LocalQuorum (mStatus, uid)) where - update :: PrepQuery W (Maybe ApiFt.FeatureStatus, UserId) () + update :: PrepQuery W (Maybe FeatureStatus, UserId) () update = fromString "update user set feature_conference_calling = ? where id = ?" deleteEmail :: (MonadClient m) => UserId -> m () @@ -340,15 +267,6 @@ updateStatus u s = userExists :: (MonadClient m) => UserId -> m Bool userExists uid = isJust <$> retry x1 (query1 idSelect (params LocalQuorum (Identity uid))) -filterActive :: (MonadClient m) => [UserId] -> m [UserId] -filterActive us = - map (view _1) . filter isActiveUser - <$> retry x1 (query accountStateSelectAll (params LocalQuorum (Identity us))) - where - isActiveUser :: (UserId, Bool, Maybe AccountStatus) -> Bool - isActiveUser (_, True, Just Active) = True - isActiveUser _ = False - lookupUser :: (MonadClient m, MonadReader Env m) => HavePendingInvitations -> UserId -> m (Maybe User) lookupUser hpi u = listToMaybe <$> lookupUsers hpi [u] @@ -366,11 +284,6 @@ lookupName u = fmap runIdentity <$> retry x1 (query1 nameSelect (params LocalQuorum (Identity u))) -lookupRichInfo :: (MonadClient m) => UserId -> m (Maybe RichInfoAssocList) -lookupRichInfo u = - fmap runIdentity - <$> retry x1 (query1 richInfoSelect (params LocalQuorum (Identity u))) - -- | Returned rich infos are in the same order as users lookupRichInfoMultiUsers :: (MonadClient m) => [UserId] -> m [(UserId, RichInfo)] lookupRichInfoMultiUsers users = do @@ -385,29 +298,15 @@ lookupUserTeam u = (runIdentity =<<) <$> retry x1 (query1 teamSelect (params LocalQuorum (Identity u))) -lookupAuth :: (MonadClient m) => UserId -> m (Maybe (Maybe Password, AccountStatus)) -lookupAuth u = fmap f <$> retry x1 (query1 authSelect (params LocalQuorum (Identity u))) - where - f (pw, st) = (pw, fromMaybe Active st) - -- | Return users with given IDs. -- -- Skips nonexistent users. /Does not/ skip users who have been deleted. lookupUsers :: (MonadClient m, MonadReader Env m) => HavePendingInvitations -> [UserId] -> m [User] lookupUsers hpi usrs = do - loc <- setDefaultUserLocale <$> view settings + loc <- defaultUserLocale <$> asks (.settings) domain <- viewFederationDomain toUsers domain loc hpi <$> retry x1 (query usersSelect (params LocalQuorum (Identity usrs))) -lookupAccount :: (MonadClient m, MonadReader Env m) => UserId -> m (Maybe UserAccount) -lookupAccount u = listToMaybe <$> lookupAccounts [u] - -lookupAccounts :: (MonadClient m, MonadReader Env m) => [UserId] -> m [UserAccount] -lookupAccounts usrs = do - loc <- setDefaultUserLocale <$> view settings - domain <- viewFederationDomain - fmap (toUserAccount domain loc) <$> retry x1 (query accountsSelect (params LocalQuorum (Identity usrs))) - lookupServiceUser :: (MonadClient m) => ProviderId -> ServiceId -> BotId -> m (Maybe (ConvId, Maybe TeamId)) lookupServiceUser pid sid bid = retry x1 (query1 cql (params LocalQuorum (pid, sid, bid))) where @@ -444,15 +343,12 @@ lookupServiceUsersForTeam pid sid tid = "SELECT user, conv FROM service_team \ \WHERE provider = ? AND service = ? AND team = ?" -lookupFeatureConferenceCalling :: (MonadClient m) => UserId -> m (Maybe (ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig)) +lookupFeatureConferenceCalling :: (MonadClient m) => UserId -> m (Maybe FeatureStatus) lookupFeatureConferenceCalling uid = do let q = query1 select (params LocalQuorum (Identity uid)) - mStatusValue <- (>>= runIdentity) <$> retry x1 q - case mStatusValue of - Nothing -> pure Nothing - Just status -> pure $ Just $ ApiFt.defFeatureStatusNoLock {ApiFt.wssStatus = status} + (>>= runIdentity) <$> retry x1 q where - select :: PrepQuery R (Identity UserId) (Identity (Maybe ApiFt.FeatureStatus)) + select :: PrepQuery R (Identity UserId) (Identity (Maybe FeatureStatus)) select = fromString "select feature_conference_calling from user where id = ?" ------------------------------------------------------------------------------- @@ -460,11 +356,15 @@ lookupFeatureConferenceCalling uid = do type Activated = Bool +-- UserRow is the same as AccountRow from the user subsystem. when migrating this code there, +-- consider eliminating it instead. type UserRow = ( UserId, Name, + Maybe TextStatus, Maybe Pict, - Maybe Email, + Maybe EmailAddress, + Maybe EmailAddress, Maybe UserSSOId, ColourId, Maybe [Asset], @@ -484,9 +384,10 @@ type UserRow = type UserRowInsert = ( UserId, Name, + Maybe TextStatus, Pict, [Asset], - Maybe Email, + Maybe EmailAddress, Maybe UserSSOId, ColourId, Maybe Password, @@ -505,12 +406,9 @@ type UserRowInsert = deriving instance Show UserRowInsert --- Represents a 'UserAccount' -type AccountRow = UserRow - usersSelect :: PrepQuery R (Identity [UserId]) UserRow usersSelect = - "SELECT id, name, picture, email, sso_id, accent_id, assets, \ + "SELECT id, name, text_status, picture, email, email_unvalidated, sso_id, accent_id, assets, \ \activated, status, expires, language, country, provider, service, \ \handle, team, managed_by, supported_protocols \ \FROM user where id IN ?" @@ -521,39 +419,23 @@ idSelect = "SELECT id FROM user WHERE id = ?" nameSelect :: PrepQuery R (Identity UserId) (Identity Name) nameSelect = "SELECT name FROM user WHERE id = ?" -authSelect :: PrepQuery R (Identity UserId) (Maybe Password, Maybe AccountStatus) -authSelect = "SELECT password, status FROM user WHERE id = ?" - -accountStateSelectAll :: PrepQuery R (Identity [UserId]) (UserId, Bool, Maybe AccountStatus) -accountStateSelectAll = "SELECT id, activated, status FROM user WHERE id IN ?" - -richInfoSelect :: PrepQuery R (Identity UserId) (Identity RichInfoAssocList) -richInfoSelect = "SELECT json FROM rich_info WHERE user = ?" - richInfoSelectMulti :: PrepQuery R (Identity [UserId]) (UserId, Maybe RichInfoAssocList) richInfoSelectMulti = "SELECT user, json FROM rich_info WHERE user in ?" teamSelect :: PrepQuery R (Identity UserId) (Identity (Maybe TeamId)) teamSelect = "SELECT team FROM user WHERE id = ?" -accountsSelect :: PrepQuery R (Identity [UserId]) AccountRow -accountsSelect = - "SELECT id, name, picture, email, sso_id, accent_id, assets, \ - \activated, status, expires, language, country, provider, \ - \service, handle, team, managed_by, supported_protocols \ - \FROM user WHERE id IN ?" - userInsert :: PrepQuery W UserRowInsert () userInsert = - "INSERT INTO user (id, name, picture, assets, email, sso_id, \ + "INSERT INTO user (id, name, text_status, picture, assets, email, sso_id, \ \accent_id, password, activated, status, expires, language, \ \country, provider, service, handle, team, managed_by, supported_protocols) \ - \VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + \VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" -userEmailUpdate :: PrepQuery W (Email, UserId) () +userEmailUpdate :: PrepQuery W (EmailAddress, UserId) () userEmailUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET email = ? WHERE id = ?" -userEmailUnvalidatedUpdate :: PrepQuery W (Email, UserId) () +userEmailUnvalidatedUpdate :: PrepQuery W (EmailAddress, UserId) () userEmailUnvalidatedUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET email_unvalidated = ? WHERE id = ?" userEmailUnvalidatedDelete :: PrepQuery W (Identity UserId) () @@ -571,11 +453,11 @@ userStatusUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE use userDeactivatedUpdate :: PrepQuery W (Identity UserId) () userDeactivatedUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET activated = false WHERE id = ?" -userActivatedUpdate :: PrepQuery W (Maybe Email, UserId) () +userActivatedUpdate :: PrepQuery W (Maybe EmailAddress, UserId) () userActivatedUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET activated = true, email = ? WHERE id = ?" userEmailDelete :: PrepQuery W (Identity UserId) () -userEmailDelete = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET email = null WHERE id = ?" +userEmailDelete = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET email = null, write_time_bumper = 0 WHERE id = ?" userRichInfoUpdate :: PrepQuery W (RichInfoAssocList, UserId) () userRichInfoUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE rich_info SET json = ? WHERE user = ?" @@ -583,56 +465,8 @@ userRichInfoUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE r ------------------------------------------------------------------------------- -- Conversions --- | Construct a 'UserAccount' from a raw user record in the database. -toUserAccount :: Domain -> Locale -> AccountRow -> UserAccount -toUserAccount - domain - defaultLocale - ( uid, - name, - pict, - email, - ssoid, - accent, - assets, - activated, - status, - expires, - lan, - con, - pid, - sid, - handle, - tid, - managed_by, - prots - ) = - let ident = toIdentity activated email ssoid - deleted = Just Deleted == status - expiration = if status == Just Ephemeral then expires else Nothing - loc = toLocale defaultLocale (lan, con) - svc = newServiceRef <$> sid <*> pid - in UserAccount - ( User - (Qualified uid domain) - ident - name - (fromMaybe noPict pict) - (fromMaybe [] assets) - accent - deleted - loc - svc - handle - expiration - tid - (fromMaybe ManagedByWire managed_by) - (fromMaybe defSupportedProtocols prots) - ) - (fromMaybe Active status) - toUsers :: Domain -> Locale -> HavePendingInvitations -> [UserRow] -> [User] -toUsers domain defaultLocale havePendingInvitations = fmap mk . filter fp +toUsers domain defLocale havePendingInvitations = fmap mk . filter fp where fp :: UserRow -> Bool fp = @@ -641,8 +475,10 @@ toUsers domain defaultLocale havePendingInvitations = fmap mk . filter fp NoPendingInvitations -> ( \( _uid, _name, + _textStatus, _pict, _email, + _emailUnvalidated, _ssoid, _accent, _assets, @@ -664,8 +500,10 @@ toUsers domain defaultLocale havePendingInvitations = fmap mk . filter fp mk ( uid, name, + textStatus, pict, email, + emailUnvalidated, ssoid, accent, assets, @@ -682,18 +520,19 @@ toUsers domain defaultLocale havePendingInvitations = fmap mk . filter fp prots ) = let ident = toIdentity activated email ssoid - deleted = Just Deleted == status expiration = if status == Just Ephemeral then expires else Nothing - loc = toLocale defaultLocale (lan, con) + loc = toLocaleWithDefault defLocale (lan, con) svc = newServiceRef <$> sid <*> pid in User (Qualified uid domain) ident + emailUnvalidated name + textStatus (fromMaybe noPict pict) (fromMaybe [] assets) accent - deleted + (fromMaybe Active status) loc svc handle @@ -702,9 +541,9 @@ toUsers domain defaultLocale havePendingInvitations = fmap mk . filter fp (fromMaybe ManagedByWire managed_by) (fromMaybe defSupportedProtocols prots) -toLocale :: Locale -> (Maybe Language, Maybe Country) -> Locale -toLocale _ (Just l, c) = Locale l c -toLocale l _ = l + toLocaleWithDefault :: Locale -> (Maybe Language, Maybe Country) -> Locale + toLocaleWithDefault _ (Just l, c) = Locale l c + toLocaleWithDefault l _ = l -- | Construct a 'UserIdentity'. -- @@ -713,11 +552,11 @@ toLocale l _ = l -- activated. -- -- The reason it's just a "precaution" is that we /also/ have an invariant that having an --- email or phone in the database means the user has to be activated. +-- email in the database means the user has to be activated. toIdentity :: -- | Whether the user is activated Bool -> - Maybe Email -> + Maybe EmailAddress -> Maybe UserSSOId -> Maybe UserIdentity toIdentity True (Just e) Nothing = Just $! EmailIdentity e diff --git a/services/brig/src/Brig/Effects/BlacklistStore.hs b/services/brig/src/Brig/Effects/BlacklistStore.hs deleted file mode 100644 index e888194d7a3..00000000000 --- a/services/brig/src/Brig/Effects/BlacklistStore.hs +++ /dev/null @@ -1,14 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} - -module Brig.Effects.BlacklistStore where - -import Imports -import Polysemy -import Wire.UserKeyStore - -data BlacklistStore m a where - Insert :: EmailKey -> BlacklistStore m () - Exists :: EmailKey -> BlacklistStore m Bool - Delete :: EmailKey -> BlacklistStore m () - -makeSem ''BlacklistStore diff --git a/services/brig/src/Brig/Effects/JwtTools.hs b/services/brig/src/Brig/Effects/JwtTools.hs index a344f5b7ae4..03b6d6e4f62 100644 --- a/services/brig/src/Brig/Effects/JwtTools.hs +++ b/services/brig/src/Brig/Effects/JwtTools.hs @@ -5,7 +5,6 @@ module Brig.Effects.JwtTools where import Brig.API.Types (CertEnrollmentError (..)) import Control.Monad.Trans.Except import Data.ByteString.Conversion -import Data.Either.Extra import Data.Handle (Handle, fromHandle) import Data.Id import Data.Jwt.Tools qualified as Jwt diff --git a/services/brig/src/Brig/Federation/Client.hs b/services/brig/src/Brig/Federation/Client.hs index d86f706169f..9c5302689d2 100644 --- a/services/brig/src/Brig/Federation/Client.hs +++ b/services/brig/src/Brig/Federation/Client.hs @@ -18,8 +18,7 @@ -- FUTUREWORK: Remove this module all together. module Brig.Federation.Client where -import Brig.App as Brig -import Control.Lens +import Brig.App import Control.Monad import Control.Monad.Catch (MonadMask, throwM) import Control.Monad.Trans.Except (ExceptT (..), throwE) @@ -100,18 +99,6 @@ claimMultiPrekeyBundle domain uc = do lift . Log.info $ Log.msg @Text "Brig-federation: claiming remote multi-user prekey bundle" runBrigFederatorClient domain $ fedClient @'Brig @"claim-multi-prekey-bundle" uc -searchUsers :: - ( MonadReader Env m, - MonadIO m, - Log.MonadLogger m - ) => - Domain -> - SearchRequest -> - ExceptT FederationError m SearchResponse -searchUsers domain searchTerm = do - lift $ Log.info $ Log.msg $ T.pack "Brig-federation: search call on remote backend" - runBrigFederatorClient domain $ fedClient @'Brig @"search-users" searchTerm - getUserClients :: ( MonadReader Env m, MonadIO m, @@ -154,7 +141,7 @@ notifyUserDeleted self remotes = do let remoteConnections = tUnqualified remotes let notif = UserDeletedConnectionsNotification (tUnqualified self) remoteConnections remoteDomain = tDomain remotes - view rabbitmqChannel >>= \case + asks (.rabbitmqChannel) >>= \case Just chanVar -> do enqueueNotification (tDomain self) remoteDomain Q.Persistent chanVar $ fedQueueClient @'OnUserDeletedConnectionsTag notif @@ -172,7 +159,7 @@ enqueueNotification ownDomain remoteDomain deliveryMode chanVar action = do recovering policy [logRetries (const $ pure True) logError] (const go) where logError willRetry (SomeException e) status = do - rid <- view Brig.requestId + rid <- asks (.requestId) Log.err $ Log.msg @Text "failed to enqueue notification in RabbitMQ" . Log.field "error" (displayException e) @@ -180,7 +167,7 @@ enqueueNotification ownDomain remoteDomain deliveryMode chanVar action = do . Log.field "retryCount" status.rsIterNumber . Log.field "request" rid go = do - rid <- view Brig.requestId + rid <- asks (.requestId) mChan <- timeout (1 :: Second) (readMVar chanVar) case mChan of Nothing -> throwM NoRabbitMqChannel @@ -198,9 +185,9 @@ runBrigFederatorClient :: ExceptT FederationError m a runBrigFederatorClient targetDomain action = do ownDomain <- viewFederationDomain - endpoint <- view federator >>= maybe (throwE FederationNotConfigured) pure - mgr <- view http2Manager - rid <- view Brig.requestId + endpoint <- asks (.federator) >>= maybe (throwE FederationNotConfigured) pure + mgr <- asks (.http2Manager) + rid <- asks (.requestId) let env = FederatorClientEnv { ceOriginDomain = ownDomain, diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index 650940bac87..95d49a402b8 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -20,13 +20,13 @@ -- FUTUREWORK: Move to Brig.User.RPC or similar. module Brig.IO.Intra ( -- * Pushing & Journaling Events - onUserEvent, + sendUserEvent, onConnectionEvent, onPropertyEvent, onClientEvent, -- * user subsystem interpretation for user events - runUserEvents, + runEvents, -- * Conversations createConnectConv, @@ -61,7 +61,6 @@ import Brig.Federation.Client (notifyUserDeleted, sendConnectionAction) import Brig.IO.Journal qualified as Journal import Brig.IO.Logging import Brig.RPC -import Brig.User.Search.Index qualified as Search import Control.Error (ExceptT, runExceptT) import Control.Lens (view, (.~), (?~), (^.), (^?)) import Control.Monad.Catch @@ -73,13 +72,10 @@ import Data.ByteString.Lazy qualified as BL import Data.Id import Data.Json.Util import Data.List.NonEmpty (NonEmpty (..)) -import Data.List1 (List1, singleton) import Data.Proxy import Data.Qualified import Data.Range import Data.Time.Clock (UTCTime) -import Gundeck.Types.Push.V2 (RecipientClients (RecipientClientsAll)) -import Gundeck.Types.Push.V2 qualified as V2 import Imports import Network.HTTP.Types.Method import Network.HTTP.Types.Status @@ -92,6 +88,8 @@ import Wire.API.Conversation hiding (Member) import Wire.API.Event.Conversation (Connect (Connect)) import Wire.API.Federation.API.Brig import Wire.API.Federation.Error +import Wire.API.Push.V2 (RecipientClients (RecipientClientsAll)) +import Wire.API.Push.V2 qualified as V2 import Wire.API.Routes.Internal.Galley.ConversationsIntra import Wire.API.Routes.Internal.Galley.TeamsIntra (GuardLegalholdPolicyConflicts (GuardLegalholdPolicyConflicts)) import Wire.API.Team.LegalHold (LegalholdProtectee) @@ -99,17 +97,17 @@ import Wire.API.Team.Member qualified as Team import Wire.API.User import Wire.API.User.Client import Wire.API.UserEvent +import Wire.Events import Wire.NotificationSubsystem import Wire.Rpc import Wire.Sem.Logger qualified as Log import Wire.Sem.Paging qualified as P import Wire.Sem.Paging.Cassandra (InternalPaging) -import Wire.UserEvents ----------------------------------------------------------------------------- -- Event Handlers -onUserEvent :: +sendUserEvent :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member TinyLog r, @@ -121,12 +119,11 @@ onUserEvent :: Maybe ConnId -> UserEvent -> Sem r () -onUserEvent orig conn e = - updateSearchIndex orig e - *> dispatchNotifications orig conn e +sendUserEvent orig conn e = + dispatchNotifications orig conn e *> embed (journalEvent orig e) -runUserEvents :: +runEvents :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member TinyLog r, @@ -134,10 +131,11 @@ runUserEvents :: Member (Input UTCTime) r, Member (ConnectionStore InternalPaging) r ) => - InterpreterFor UserEvents r -runUserEvents = interpret \case + InterpreterFor Events r +runEvents = interpret \case -- FUTUREWORK(mangoiv): should this be in another module? - GenerateUserEvent uid mconnid event -> onUserEvent uid mconnid event + GenerateUserEvent uid mconnid event -> sendUserEvent uid mconnid event + GeneratePropertyEvent uid connid event -> onPropertyEvent uid connid event onConnectionEvent :: (Member NotificationSubsystem r) => @@ -151,7 +149,7 @@ onConnectionEvent :: onConnectionEvent orig conn evt = do let from = ucFrom (ucConn evt) notify - (singleton $ ConnectionEvent evt) + (ConnectionEvent evt) orig V2.RouteAny conn @@ -167,7 +165,7 @@ onPropertyEvent :: Sem r () onPropertyEvent orig conn e = notify - (singleton $ PropertyEvent e) + (PropertyEvent e) orig V2.RouteDirect (Just conn) @@ -191,35 +189,6 @@ onClientEvent orig conn e = do & pushApsData .~ toApsData event ] -updateSearchIndex :: - (Member (Embed HttpClientIO) r) => - UserId -> - UserEvent -> - Sem r () -updateSearchIndex orig e = embed $ case e of - -- no-ops - UserCreated {} -> pure () - UserIdentityUpdated UserIdentityUpdatedData {..} -> do - when (isJust eiuEmail) $ Search.reindex orig - UserIdentityRemoved {} -> pure () - UserLegalHoldDisabled {} -> pure () - UserLegalHoldEnabled {} -> pure () - LegalHoldClientRequested {} -> pure () - UserSuspended {} -> Search.reindex orig - UserResumed {} -> Search.reindex orig - UserActivated {} -> Search.reindex orig - UserDeleted {} -> Search.reindex orig - UserUpdated UserUpdatedData {..} -> do - let interesting = - or - [ isJust eupName, - isJust eupAccentId, - isJust eupHandle, - isJust eupManagedBy, - isJust eupSSOId || eupSSOIdRemoved - ] - when interesting $ Search.reindex orig - journalEvent :: (MonadReader Env m, MonadIO m) => UserId -> UserEvent -> m () journalEvent orig e = case e of UserActivated acc -> @@ -275,7 +244,7 @@ dispatchNotifications orig conn e = case e of notifyUserDeletionLocals orig conn event notifyUserDeletionRemotes orig where - event = singleton $ UserEvent e + event = UserEvent e notifyUserDeletionLocals :: forall r. @@ -286,7 +255,7 @@ notifyUserDeletionLocals :: ) => UserId -> Maybe ConnId -> - List1 Event -> + Event -> Sem r () notifyUserDeletionLocals deleted conn event = do luid <- qualifyLocal' deleted @@ -356,7 +325,7 @@ notifyUserDeletionRemotes deleted = do pure () Just rangedUcs -> do luidDeleted <- qualifyLocal' deleted - embed $ notifyUserDeleted luidDeleted (qualifyAs ucs ((fmap (fmap (qUnqualified . ucTo))) rangedUcs)) + embed $ notifyUserDeleted luidDeleted (qualifyAs ucs (mapRange (qUnqualified . ucTo) rangedUcs)) -- also sent connection cancelled events to the connections that are pending let remotePendingConnections = qualifyAs ucs <$> filter ((==) Sent . ucStatus) (fromRange rangedUcs) forM_ remotePendingConnections $ sendCancelledEvent luidDeleted @@ -374,7 +343,7 @@ notifyUserDeletionRemotes deleted = do -- | (Asynchronously) notifies other users of events. notify :: (Member NotificationSubsystem r) => - List1 Event -> + Event -> -- | Origin user, TODO: Delete UserId -> -- | Push routing strategy. @@ -384,18 +353,18 @@ notify :: -- | Users to notify. Sem r (NonEmpty UserId) -> Sem r () -notify (toList -> events) orig route conn recipients = do +notify event orig route conn recipients = do rs <- (\u -> Recipient u RecipientClientsAll) <$$> recipients - let pushes = flip map events $ \event -> + let push = newPush1 (Just orig) (toJSONObject event) rs & pushConn .~ conn & pushRoute .~ route & pushApsData .~ toApsData event - void $ pushNotificationsAsync pushes + void $ pushNotificationAsync push notifySelf :: (Member NotificationSubsystem r) => - List1 Event -> + Event -> -- | Origin user. UserId -> -- | Push routing strategy. @@ -403,8 +372,8 @@ notifySelf :: -- | Origin device connection, if any. Maybe ConnId -> Sem r () -notifySelf events orig route conn = - notify events orig route conn (pure (orig :| [])) +notifySelf event orig route conn = + notify event orig route conn (pure (orig :| [])) notifyContacts :: forall r. @@ -412,7 +381,7 @@ notifyContacts :: Member NotificationSubsystem r, Member TinyLog r ) => - List1 Event -> + Event -> -- | Origin user. UserId -> -- | Push routing strategy. @@ -420,8 +389,8 @@ notifyContacts :: -- | Origin device connection, if any. Maybe ConnId -> Sem r () -notifyContacts events orig route conn = do - notify events orig route conn $ +notifyContacts event orig route conn = do + notify event orig route conn $ (:|) orig <$> liftA2 (++) contacts teamContacts where contacts :: Sem r [UserId] @@ -563,7 +532,7 @@ blockConv lusr qcnv = do upsertOne2OneConversation :: ( MonadReader Env m, - MonadIO m, + MonadUnliftIO m, MonadMask m, MonadHttp m, HasRequestId m diff --git a/services/brig/src/Brig/IO/Journal.hs b/services/brig/src/Brig/IO/Journal.hs index 274a784092c..ccaba403926 100644 --- a/services/brig/src/Brig/IO/Journal.hs +++ b/services/brig/src/Brig/IO/Journal.hs @@ -50,20 +50,20 @@ import Wire.API.User userActivate :: (MonadReader Env m, MonadIO m) => User -> m () userActivate u@User {..} = journalEvent UserEvent'USER_ACTIVATE (userId u) (userEmail u) (Just userLocale) userTeam (Just userDisplayName) -userUpdate :: (MonadReader Env m, MonadIO m) => UserId -> Maybe Email -> Maybe Locale -> Maybe Name -> m () +userUpdate :: (MonadReader Env m, MonadIO m) => UserId -> Maybe EmailAddress -> Maybe Locale -> Maybe Name -> m () userUpdate uid em loc = journalEvent UserEvent'USER_UPDATE uid em loc Nothing -userEmailRemove :: (MonadReader Env m, MonadIO m) => UserId -> Email -> m () +userEmailRemove :: (MonadReader Env m, MonadIO m) => UserId -> EmailAddress -> m () userEmailRemove uid em = journalEvent UserEvent'USER_EMAIL_REMOVE uid (Just em) Nothing Nothing Nothing userDelete :: (MonadReader Env m, MonadIO m) => UserId -> m () userDelete uid = journalEvent UserEvent'USER_DELETE uid Nothing Nothing Nothing Nothing -journalEvent :: (MonadReader Env m, MonadIO m) => UserEvent'EventType -> UserId -> Maybe Email -> Maybe Locale -> Maybe TeamId -> Maybe Name -> m () +journalEvent :: (MonadReader Env m, MonadIO m) => UserEvent'EventType -> UserId -> Maybe EmailAddress -> Maybe Locale -> Maybe TeamId -> Maybe Name -> m () journalEvent typ uid em loc tid nm = -- this may be the only place that uses awsEnv from brig Env. refactor it to use the -- DeleteQueue effect instead? - view awsEnv >>= \env -> for_ (view AWS.userJournalQueue env) $ \queue -> do + asks (.awsEnv) >>= \env -> for_ (view AWS.userJournalQueue env) $ \queue -> do ts <- now rnd <- liftIO nextRandom let userEvent :: UserEvent = diff --git a/services/brig/src/Brig/Index/Eval.hs b/services/brig/src/Brig/Index/Eval.hs index c19d000c5d9..64dd9ebca59 100644 --- a/services/brig/src/Brig/Index/Eval.hs +++ b/services/brig/src/Brig/Index/Eval.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -23,13 +21,13 @@ module Brig.Index.Eval where import Brig.App (initHttpManagerWithTLSConfig, mkIndexEnv) -import Brig.Index.Migrations import Brig.Index.Options import Brig.Options import Brig.User.Search.Index -import Cassandra qualified as C +import Cassandra (Client, runClient) import Cassandra.Options import Cassandra.Util (defInitCassandra) +import Control.Exception (throwIO) import Control.Lens import Control.Monad.Catch import Control.Retry @@ -37,11 +35,121 @@ import Data.Aeson (FromJSON) import Data.Aeson qualified as Aeson import Data.ByteString.Lazy.UTF8 qualified as UTF8 import Data.Credentials (Credentials (..)) +import Data.Id import Database.Bloodhound qualified as ES +import Database.Bloodhound.Internal.Client (BHEnv (..)) import Imports +import Polysemy +import Polysemy.Embed (runEmbedded) +import Polysemy.Error +import Polysemy.TinyLog hiding (Logger) import System.Logger qualified as Log -import System.Logger.Class (Logger, MonadLogger (..)) +import System.Logger.Class (Logger) import Util.Options (initCredentials) +import Wire.API.Federation.Client (FederatorClient) +import Wire.API.Federation.Error +import Wire.BlockListStore (BlockListStore) +import Wire.BlockListStore.Cassandra +import Wire.FederationAPIAccess +import Wire.FederationAPIAccess.Interpreter (noFederationAPIAccess) +import Wire.FederationConfigStore (FederationConfigStore) +import Wire.FederationConfigStore.Cassandra (interpretFederationDomainConfig) +import Wire.GalleyAPIAccess +import Wire.GalleyAPIAccess.Rpc +import Wire.IndexedUserStore +import Wire.IndexedUserStore.Bulk (IndexedUserStoreBulk) +import Wire.IndexedUserStore.Bulk qualified as IndexedUserStoreBulk +import Wire.IndexedUserStore.Bulk.ElasticSearch (interpretIndexedUserStoreBulk) +import Wire.IndexedUserStore.ElasticSearch +import Wire.IndexedUserStore.MigrationStore (IndexedUserMigrationStore) +import Wire.IndexedUserStore.MigrationStore.ElasticSearch +import Wire.ParseException +import Wire.Rpc +import Wire.Sem.Concurrency +import Wire.Sem.Concurrency.IO +import Wire.Sem.Logger.TinyLog +import Wire.Sem.Metrics +import Wire.Sem.Metrics.IO +import Wire.UserKeyStore (UserKeyStore) +import Wire.UserKeyStore.Cassandra +import Wire.UserSearch.Migration (MigrationException) +import Wire.UserStore +import Wire.UserStore.Cassandra +import Wire.UserSubsystem.Error + +type BrigIndexEffectStack = + [ IndexedUserStoreBulk, + UserKeyStore, + BlockListStore, + Error UserSubsystemError, + FederationAPIAccess FederatorClient, + Error FederationError, + UserStore, + IndexedUserStore, + Error IndexedUserStoreError, + IndexedUserMigrationStore, + Error MigrationException, + FederationConfigStore, + GalleyAPIAccess, + Error ParseException, + Rpc, + Metrics, + TinyLog, + Concurrency 'Unsafe, + Embed IO, + Final IO + ] + +runSem :: ESConnectionSettings -> CassandraSettings -> Endpoint -> Logger -> Sem BrigIndexEffectStack a -> IO a +runSem esConn cas galleyEndpoint logger action = do + mgr <- initHttpManagerWithTLSConfig esConn.esInsecureSkipVerifyTls esConn.esCaCert + mEsCreds :: Maybe Credentials <- for esConn.esCredentials initCredentials + casClient <- defInitCassandra (toCassandraOpts cas) logger + let bhEnv = + BHEnv + { bhServer = toESServer esConn.esServer, + bhManager = mgr, + bhRequestHook = maybe pure (\creds -> ES.basicAuthHook (ES.EsUsername creds.username) (ES.EsPassword creds.password)) mEsCreds + } + indexedUserStoreConfig = + IndexedUserStoreConfig + { conn = + ESConn + { indexName = esConn.esIndex, + env = bhEnv + }, + additionalConn = Nothing + } + reqId = (RequestId "brig-index") + runFinal + . embedToFinal + . unsafelyPerformConcurrency + . loggerToTinyLogReqId reqId logger + . ignoreMetrics + . runRpcWithHttp mgr reqId + . throwErrorToIOFinal @ParseException + . interpretGalleyAPIAccessToRpc mempty galleyEndpoint + . runEmbedded (runClient casClient) + . interpretFederationDomainConfig casClient Nothing mempty + . raiseUnder @(Embed Client) + . throwErrorToIOFinal @MigrationException + . interpretIndexedUserMigrationStoreES bhEnv + . throwErrorToIOFinal @IndexedUserStoreError + . interpretIndexedUserStoreES indexedUserStoreConfig + . interpretUserStoreCassandra casClient + . throwErrorToIOFinal @FederationError + . noFederationAPIAccess + . throwErrorToIOFinal @UserSubsystemError + . interpretBlockListStoreToCassandra casClient + . interpretUserKeyStoreCassandra casClient + . interpretIndexedUserStoreBulk + $ action + +throwErrorToIOFinal :: (Exception e, Member (Final IO) r) => InterpreterFor (Error e) r +throwErrorToIOFinal action = do + runError action >>= \case + Left e -> embedFinal $ throwIO e + Right a -> pure a runCommand :: Logger -> Command -> IO () runCommand l = \case @@ -52,18 +160,17 @@ runCommand l = \case e <- initIndex (es ^. esConnection) galley runIndexIO e $ resetIndex (mkCreateIndexSettings es) Reindex es cas galley -> do - e <- initIndex (es ^. esConnection) galley - c <- initDb cas - runReindexIO e c reindexAll + runSem (es ^. esConnection) cas galley l $ + IndexedUserStoreBulk.syncAllUsers ReindexSameOrNewer es cas galley -> do - e <- initIndex (es ^. esConnection) galley - c <- initDb cas - runReindexIO e c reindexAllIfSameOrNewer + runSem (es ^. esConnection) cas galley l $ + IndexedUserStoreBulk.forceSyncAllUsers UpdateMapping esConn galley -> do e <- initIndex esConn galley runIndexIO e updateMapping Migrate es cas galley -> do - migrate l es cas galley + runSem (es ^. esConnection) cas galley l $ + IndexedUserStoreBulk.migrateData ReindexFromAnotherIndex reindexSettings -> do mgr <- initHttpManagerWithTLSConfig @@ -87,7 +194,7 @@ runCommand l = \case Log.info l $ Log.msg ("Reindexing" :: ByteString) . Log.field "from" (show src) . Log.field "to" (show dest) eitherTaskNodeId <- ES.reindexAsync $ ES.mkReindexRequest src dest case eitherTaskNodeId of - Left err -> throwM $ ReindexFromAnotherIndexError $ "Error occurred while running reindex: " <> show err + Left e -> throwM $ ReindexFromAnotherIndexError $ "Error occurred while running reindex: " <> show e Right taskNodeId -> do Log.info l $ Log.field "taskNodeId" (show taskNodeId) waitForTaskToComplete @ES.ReindexResponse timeoutSeconds taskNodeId @@ -116,8 +223,6 @@ runCommand l = \case let env = ES.mkBHEnv (toESServer esURI) mgr in maybe env (\(creds :: Credentials) -> env {ES.bhRequestHook = ES.basicAuthHook (ES.EsUsername creds.username) (ES.EsPassword creds.password)}) mCreds - initDb cas = defInitCassandra (toCassandraOpts cas) l - waitForTaskToComplete :: forall a m. (ES.MonadBH m, MonadThrow m, FromJSON a) => Int -> ES.TaskNodeId -> m () waitForTaskToComplete timeoutSeconds taskNodeId = do -- Delay is 0.1 seconds, so retries are limited to timeoutSeconds * 10 @@ -144,32 +249,3 @@ newtype ReindexFromAnotherIndexError = ReindexFromAnotherIndexError String deriving (Show) instance Exception ReindexFromAnotherIndexError - --------------------------------------------------------------------------------- --- ReindexIO command monad - -newtype ReindexIO a = ReindexIO (ReaderT C.ClientState IndexIO a) - deriving - ( Functor, - Applicative, - Monad, - MonadIO, - MonadReader C.ClientState, - MonadThrow, - MonadCatch - ) - -runReindexIO :: IndexEnv -> C.ClientState -> ReindexIO a -> IO a -runReindexIO ixe cas (ReindexIO ma) = runIndexIO ixe (runReaderT ma cas) - -instance MonadIndexIO ReindexIO where - liftIndexIO = ReindexIO . ReaderT . const - -instance C.MonadClient ReindexIO where - liftClient ma = ask >>= \e -> C.runClient e ma - localState = local - -instance MonadLogger ReindexIO where - log lvl msg = do - l <- ReindexIO . lift $ asks idxLogger - Log.log l lvl msg diff --git a/services/brig/src/Brig/Index/Migrations.hs b/services/brig/src/Brig/Index/Migrations.hs deleted file mode 100644 index 2fbb8ce5455..00000000000 --- a/services/brig/src/Brig/Index/Migrations.hs +++ /dev/null @@ -1,173 +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 Brig.Index.Migrations - ( migrate, - ) -where - -import Brig.App (initHttpManagerWithTLSConfig) -import Brig.Index.Migrations.Types -import Brig.Index.Options qualified as Opts -import Brig.User.Search.Index qualified as Search -import Cassandra.Util (defInitCassandra) -import Control.Lens (to, view, (^.)) -import Control.Monad.Catch (MonadThrow, catchAll, finally, throwM) -import Data.Aeson (Value, object, (.=)) -import Data.Credentials (Credentials (..)) -import Data.Text qualified as Text -import Database.Bloodhound qualified as ES -import Imports -import Network.HTTP.Client qualified as HTTP -import System.Logger.Class (Logger) -import System.Logger.Class qualified as Log -import System.Logger.Extended (runWithLogger) -import Util.Options qualified as Options - -migrate :: Logger -> Opts.ElasticSettings -> Opts.CassandraSettings -> Options.Endpoint -> IO () -migrate l es cas galleyEndpoint = do - env <- mkEnv l es cas galleyEndpoint - finally (go env `catchAll` logAndThrowAgain) (cleanup env) - where - go :: Env -> IO () - go env = - runMigrationAction env $ do - failIfIndexAbsent (es ^. Opts.esConnection . to Opts.esIndex) - createMigrationsIndexIfNotPresent - runMigration expectedMigrationVersion - - logAndThrowAgain :: forall a. SomeException -> IO a - logAndThrowAgain e = do - runWithLogger l $ - Log.err $ - Log.msg (Log.val "Migration failed with exception") . Log.field "exception" (show e) - throwM e - --- | Increase this number any time you want to force reindexing. -expectedMigrationVersion :: MigrationVersion -expectedMigrationVersion = MigrationVersion 6 - -indexName :: ES.IndexName -indexName = ES.IndexName "wire_brig_migrations" - -indexMappingName :: ES.MappingName -indexMappingName = ES.MappingName "wire_brig_migrations" - -indexMapping :: Value -indexMapping = - object - [ "properties" - .= object - ["migration_version" .= object ["index" .= True, "type" .= ("integer" :: Text)]] - ] - -mkEnv :: Logger -> Opts.ElasticSettings -> Opts.CassandraSettings -> Options.Endpoint -> IO Env -mkEnv l es cas galleyEndpoint = do - env <- do - esMgr <- initHttpManagerWithTLSConfig (es ^. Opts.esConnection . to Opts.esInsecureSkipVerifyTls) (es ^. Opts.esConnection . to Opts.esCaCert) - pure $ ES.mkBHEnv (Opts.toESServer (es ^. Opts.esConnection . to Opts.esServer)) esMgr - mCreds <- for (es ^. Opts.esConnection . to Opts.esCredentials) Options.initCredentials - let envWithAuth = maybe env (\(creds :: Credentials) -> env {ES.bhRequestHook = ES.basicAuthHook (ES.EsUsername creds.username) (ES.EsPassword creds.password)}) mCreds - rpcMgr <- HTTP.newManager HTTP.defaultManagerSettings - Env envWithAuth - <$> initCassandra - <*> initLogger - <*> pure (view (Opts.esConnection . to Opts.esIndex) es) - <*> pure mCreds - <*> pure rpcMgr - <*> pure galleyEndpoint - where - initCassandra = defInitCassandra (Opts.toCassandraOpts cas) l - - initLogger = pure l - -createMigrationsIndexIfNotPresent :: (MonadThrow m, ES.MonadBH m, Log.MonadLogger m) => m () -createMigrationsIndexIfNotPresent = - do - unlessM (ES.indexExists indexName) $ do - Log.info $ - Log.msg (Log.val "Creating migrations index, used for tracking which migrations have run") - ES.createIndexWith [] 1 indexName - >>= throwIfNotCreated CreateMigrationIndexFailed - ES.putMapping indexName indexMappingName indexMapping - >>= throwIfNotCreated PutMappingFailed - where - throwIfNotCreated err response = - unless (ES.isSuccess response) $ - throwM $ - err (show response) - -failIfIndexAbsent :: (MonadThrow m, ES.MonadBH m) => ES.IndexName -> m () -failIfIndexAbsent targetIndex = - unlessM - (ES.indexExists targetIndex) - (throwM $ TargetIndexAbsent targetIndex) - --- | Runs only the migrations which need to run -runMigration :: MigrationVersion -> MigrationActionT IO () -runMigration expectedVersion = do - foundVersion <- latestMigrationVersion - if expectedVersion > foundVersion - then do - Log.info $ - Log.msg (Log.val "Migration necessary.") - . Log.field "expectedVersion" expectedVersion - . Log.field "foundVersion" foundVersion - Search.reindexAllIfSameOrNewer - persistVersion expectedVersion - else do - Log.info $ - Log.msg (Log.val "No migration necessary.") - . Log.field "expectedVersion" expectedVersion - . Log.field "foundVersion" foundVersion - -persistVersion :: (MonadThrow m, MonadIO m) => MigrationVersion -> MigrationActionT m () -persistVersion v = - let docId = ES.DocId . Text.pack . show $ migrationVersion v - in do - persistResponse <- ES.indexDocument indexName indexMappingName ES.defaultIndexDocumentSettings v docId - if ES.isCreated persistResponse - then do - Log.info $ - Log.msg (Log.val "Migration success recorded") - . Log.field "migrationVersion" v - else throwM $ PersistVersionFailed v $ show persistResponse - --- | Which version is the table space currently running on? -latestMigrationVersion :: (MonadThrow m, MonadIO m) => MigrationActionT m MigrationVersion -latestMigrationVersion = do - resp <- ES.parseEsResponse =<< ES.searchByIndex indexName (ES.mkSearch Nothing Nothing) - result <- either (throwM . FetchMigrationVersionsFailed . show) pure resp - let versions = map ES.hitSource $ ES.hits . ES.searchHits $ result - case versions of - [] -> - pure $ MigrationVersion 0 - vs -> - if any isNothing vs - then throwM $ VersionSourceMissing result - else pure $ maximum $ catMaybes vs - -data MigrationException - = CreateMigrationIndexFailed String - | FetchMigrationVersionsFailed String - | PersistVersionFailed MigrationVersion String - | PutMappingFailed String - | TargetIndexAbsent ES.IndexName - | VersionSourceMissing (ES.SearchResult MigrationVersion) - deriving (Show) - -instance Exception MigrationException diff --git a/services/brig/src/Brig/Index/Migrations/Types.hs b/services/brig/src/Brig/Index/Migrations/Types.hs deleted file mode 100644 index 7854ce67aae..00000000000 --- a/services/brig/src/Brig/Index/Migrations/Types.hs +++ /dev/null @@ -1,100 +0,0 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE RecordWildCards #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Brig.Index.Migrations.Types where - -import Brig.User.Search.Index qualified as Search -import Cassandra qualified as C -import Control.Monad.Catch (MonadThrow) -import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.=)) -import Data.Credentials (Credentials) -import Database.Bloodhound qualified as ES -import Imports -import Network.HTTP.Client (Manager) -import Numeric.Natural (Natural) -import System.Logger qualified as Logger -import System.Logger.Class (MonadLogger (..), ToBytes (..)) -import Util.Options (Endpoint) - -newtype MigrationVersion = MigrationVersion {migrationVersion :: Natural} - deriving (Show, Eq, Ord) - -instance ToJSON MigrationVersion where - toJSON (MigrationVersion v) = object ["migration_version" .= v] - -instance FromJSON MigrationVersion where - parseJSON = withObject "MigrationVersion" $ \o -> MigrationVersion <$> o .: "migration_version" - -instance ToBytes MigrationVersion where - bytes = bytes . toInteger . migrationVersion - -newtype MigrationActionT m a = MigrationActionT {unMigrationAction :: ReaderT Env m a} - deriving - ( Functor, - Applicative, - Monad, - MonadIO, - MonadThrow, - MonadReader Env - ) - -instance MonadTrans MigrationActionT where - lift = MigrationActionT . lift - -instance (MonadIO m, MonadThrow m) => C.MonadClient (MigrationActionT m) where - liftClient = liftCassandra - localState f = local (\env -> env {cassandraClientState = f $ cassandraClientState env}) - -instance (MonadIO m) => MonadLogger (MigrationActionT m) where - log level f = do - env <- ask - Logger.log (logger env) level f - -instance (MonadIO m) => Search.MonadIndexIO (MigrationActionT m) where - liftIndexIO m = do - Env {..} <- ask - let indexEnv = Search.IndexEnv logger bhEnv Nothing searchIndex Nothing Nothing galleyEndpoint httpManager searchIndexCredentials - Search.runIndexIO indexEnv m - -instance (MonadIO m) => ES.MonadBH (MigrationActionT m) where - getBHEnv = bhEnv <$> ask - -data Env = Env - { bhEnv :: ES.BHEnv, - cassandraClientState :: C.ClientState, - logger :: Logger.Logger, - searchIndex :: ES.IndexName, - searchIndexCredentials :: Maybe Credentials, - httpManager :: Manager, - galleyEndpoint :: Endpoint - } - -runMigrationAction :: Env -> MigrationActionT m a -> m a -runMigrationAction env action = - runReaderT (unMigrationAction action) env - -liftCassandra :: (MonadIO m) => C.Client a -> MigrationActionT m a -liftCassandra m = do - env <- ask - lift $ C.runClient (cassandraClientState env) m - -cleanup :: (MonadIO m) => Env -> m () -cleanup env = do - C.shutdown (cassandraClientState env) diff --git a/services/brig/src/Brig/Index/Options.hs b/services/brig/src/Brig/Index/Options.hs index c0fe469f0ff..f9f382c0043 100644 --- a/services/brig/src/Brig/Index/Options.hs +++ b/services/brig/src/Brig/Index/Options.hs @@ -113,10 +113,10 @@ makeLenses ''ReindexFromAnotherIndexSettings toCassandraOpts :: CassandraSettings -> CassandraOpts toCassandraOpts cas = CassandraOpts - { _endpoint = Endpoint (Text.pack (cas ^. cHost)) (cas ^. cPort), - _keyspace = C.unKeyspace (cas ^. cKeyspace), - _filterNodesByDatacentre = Nothing, - _tlsCa = cas ^. cTlsCa + { endpoint = Endpoint (Text.pack (cas ^. cHost)) (cas ^. cPort), + keyspace = C.unKeyspace (cas ^. cKeyspace), + filterNodesByDatacentre = Nothing, + tlsCa = cas ^. cTlsCa } mkCreateIndexSettings :: ElasticSettings -> CreateIndexSettings diff --git a/services/brig/src/Brig/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index 912d5241c01..d9c6f32abd7 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -19,45 +19,45 @@ module Brig.InternalEvent.Process (onEvent) where import Brig.API.User qualified as API import Brig.App -import Brig.Effects.ConnectionStore import Brig.IO.Intra (rmClient) import Brig.IO.Intra qualified as Intra import Brig.InternalEvent.Types -import Brig.Options (defDeleteThrottleMillis, setDeleteThrottleMillis) +import Brig.Options (defDeleteThrottleMillis, deleteThrottleMillis) import Brig.Provider.API qualified as API -import Control.Lens (view) import Control.Monad.Catch import Data.ByteString.Conversion import Data.Qualified (Local) -import Data.Time.Clock (UTCTime) import Imports import Polysemy -import Polysemy.Conc +import Polysemy.Conc hiding (Events) import Polysemy.Input (Input) import Polysemy.Time import Polysemy.TinyLog as Log import System.Logger.Class (field, msg, val, (~~)) import Wire.API.UserEvent +import Wire.Events (Events) import Wire.NotificationSubsystem +import Wire.PropertySubsystem import Wire.Sem.Delay -import Wire.Sem.Paging.Cassandra (InternalPaging) import Wire.UserKeyStore import Wire.UserStore (UserStore) +import Wire.UserSubsystem -- | Handle an internal event. -- -- Has a one-minute timeout that should be enough for anything that it does. onEvent :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, + ( Member NotificationSubsystem r, Member TinyLog r, + Member (Embed HttpClientIO) r, Member Delay r, Member Race r, Member (Input (Local ())) r, Member UserKeyStore r, - Member (Input UTCTime) r, Member UserStore r, - Member (ConnectionStore InternalPaging) r + Member PropertySubsystem r, + Member UserSubsystem r, + Member Events r ) => InternalNotification -> Sem r () @@ -69,11 +69,12 @@ onEvent n = handleTimeout $ case n of Log.info $ msg (val "Processing user delete event") ~~ field "user" (toByteString uid) - embed (API.lookupAccount uid) >>= mapM_ API.deleteAccount + luid <- qualifyLocal' uid + getAccountNoFilter luid >>= mapM_ API.deleteAccount -- As user deletions are expensive resource-wise in the context of -- bulk user deletions (e.g. during team deletions), -- wait 'delay' ms before processing the next event - deleteThrottleMillis <- embed $ fromMaybe defDeleteThrottleMillis . setDeleteThrottleMillis <$> view settings + deleteThrottleMillis <- embed $ fromMaybe defDeleteThrottleMillis <$> asks (.settings.deleteThrottleMillis) delay (1000 * deleteThrottleMillis) DeleteService pid sid -> do Log.info $ diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 66e4ea9d69e..04da3707661 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -1,6 +1,7 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} -- Disabling to stop errors on Getters {-# OPTIONS_GHC -Wno-redundant-constraints #-} @@ -27,13 +28,12 @@ import Brig.Queue.Types (QueueOpts (..)) import Brig.User.Auth.Cookie.Limit import Brig.ZAuth qualified as ZAuth import Control.Applicative -import Control.Lens qualified as Lens -import Data.Aeson (defaultOptions, fieldLabelModifier, genericParseJSON) -import Data.Aeson qualified as A -import Data.Aeson qualified as Aeson -import Data.Aeson.Types (typeMismatch) +import Control.Lens hiding (Level, element, enum) +import Data.Aeson +import Data.Aeson.Types qualified as A import Data.Char qualified as Char import Data.Code qualified as Code +import Data.Default import Data.Domain (Domain (..)) import Data.Id import Data.LanguageCodes (ISO639_1 (EN)) @@ -41,38 +41,23 @@ import Data.Misc (HttpsUrl) import Data.Nonce import Data.Range import Data.Schema -import Data.Scientific (toBoundedInteger) import Data.Text qualified as Text import Data.Text.Encoding qualified as Text -import Data.Time.Clock (DiffTime, NominalDiffTime, secondsToDiffTime) -import Data.Yaml (FromJSON (..), ToJSON (..), (.:), (.:?)) -import Data.Yaml qualified as Y import Database.Bloodhound.Types qualified as ES -import Galley.Types.Teams (unImplicitLockStatus) import Imports import Network.AMQP.Extended import Network.DNS qualified as DNS import System.Logger.Extended (Level, LogFormat) import Util.Options +import Util.SuffixNamer +import Util.Timeout import Wire.API.Allowlists (AllowlistEmailDomains (..)) import Wire.API.Routes.FederationDomainConfig import Wire.API.Routes.Version -import Wire.API.Team.Feature qualified as Public +import Wire.API.Team.Feature import Wire.API.User -import Wire.Arbitrary (Arbitrary, arbitrary) import Wire.EmailSending.SMTP (SMTPConnType (..)) -newtype Timeout = Timeout - { timeoutDiff :: NominalDiffTime - } - deriving newtype (Eq, Enum, Ord, Num, Real, Fractional, RealFrac, Show) - -instance Read Timeout where - readsPrec i s = - case readsPrec i s of - [(x :: Int, s')] -> [(Timeout (fromIntegral x), s')] - _ -> [] - data ElasticSearchOpts = ElasticSearchOpts { -- | ElasticSearch URL url :: !ES.Server, @@ -156,28 +141,26 @@ data EmailSMTPOpts = EmailSMTPOpts instance FromJSON EmailSMTPOpts data StompOpts = StompOpts - { stompHost :: !Text, - stompPort :: !Int, - stompTls :: !Bool + { host :: !Text, + port :: !Int, + tls :: !Bool } deriving (Show, Generic) -instance FromJSON StompOpts - data InternalEventsOpts = InternalEventsOpts { internalEventsQueue :: !QueueOpts } deriving (Show) instance FromJSON InternalEventsOpts where - parseJSON = Y.withObject "InternalEventsOpts" $ \o -> - InternalEventsOpts <$> parseJSON (Y.Object o) + parseJSON = withObject "InternalEventsOpts" $ \o -> + InternalEventsOpts <$> parseJSON (Object o) data EmailSMSGeneralOpts = EmailSMSGeneralOpts { -- | Email, SMS, ... template directory templateDir :: !FilePath, -- | Email sender address - emailSender :: !Email, + emailSender :: !EmailAddress, -- | Twilio sender identifier (sender phone number in E.104 format) -- or twilio messaging sender ID - see -- https://www.twilio.com/docs/sms/send-messages#use-an-alphanumeric-sender-id @@ -229,7 +212,7 @@ data ProviderOpts = ProviderOpts -- | Approval URL template approvalUrl :: !Text, -- | Approval email recipient - approvalTo :: !Email, + approvalTo :: !EmailAddress, -- | Password reset URL template providerPwResetUrl :: !Text } @@ -240,6 +223,8 @@ instance FromJSON ProviderOpts data TeamOpts = TeamOpts { -- | Team Invitation URL template tInvitationUrl :: !Text, + -- | Existing User Invitation URL template + tExistingUserInvitationUrl :: !Text, -- | Team Activation URL template tActivationUrl :: !Text, -- | Team Creator Welcome URL @@ -327,12 +312,12 @@ data TurnOpts = TurnOpts deriving (Show) instance FromJSON TurnOpts where - parseJSON = A.withObject "TurnOpts" $ \o -> do + parseJSON = withObject "TurnOpts" $ \o -> do sourceName <- o .: "serversSource" source <- case sourceName of - "files" -> TurnSourceFiles <$> A.parseJSON (A.Object o) - "dns" -> TurnSourceDNS <$> A.parseJSON (A.Object o) + "files" -> TurnSourceFiles <$> parseJSON (Object o) + "dns" -> TurnSourceDNS <$> parseJSON (Object o) _ -> fail $ "TurnOpts: Invalid sourceType, expected one of [files, dns] but got: " <> Text.unpack sourceName TurnOpts source <$> o .: "secret" @@ -351,7 +336,7 @@ data TurnServersFiles = TurnServersFiles deriving (Show) instance FromJSON TurnServersFiles where - parseJSON = A.withObject "TurnServersFiles" $ \o -> + parseJSON = withObject "TurnServersFiles" $ \o -> TurnServersFiles <$> o .: "servers" <*> o .: "serversV2" @@ -363,7 +348,7 @@ data TurnDnsOpts = TurnDnsOpts deriving (Show) instance FromJSON TurnDnsOpts where - parseJSON = A.withObject "TurnDnsOpts" $ \o -> + parseJSON = withObject "TurnDnsOpts" $ \o -> TurnDnsOpts <$> (asciiOnly =<< o .: "baseDomain") <*> o .:? "discoveryIntervalSeconds" @@ -404,13 +389,13 @@ data Opts = Opts -- | SFT Federation multiSFT :: !(Maybe Bool), -- | RabbitMQ settings, required when federation is enabled. - rabbitmq :: !(Maybe RabbitMqOpts), + rabbitmq :: !(Maybe AmqpEndpoint), -- | AWS settings aws :: !AWSOpts, -- | Enable Random Prekey Strategy randomPrekeys :: !(Maybe Bool), -- | STOMP broker settings - stomp :: !(Maybe StompOpts), + stompOptions :: !(Maybe StompOpts), -- Email & SMS -- | Email and SMS settings @@ -442,80 +427,80 @@ data Opts = Opts -- | SFT Settings sft :: !(Maybe SFTOptions), -- | Runtime settings - optSettings :: !Settings + settings :: !Settings } deriving (Show, Generic) -- | Options that persist as runtime settings. data Settings = Settings { -- | Activation timeout, in seconds - setActivationTimeout :: !Timeout, + activationTimeout :: !Timeout, -- | Default verification code timeout, in seconds - -- use `setVerificationTimeout` as the getter function which always provides a default value - setVerificationCodeTimeoutInternal :: !(Maybe Code.Timeout), + -- use `verificationTimeout` as the getter function which always provides a default value + verificationCodeTimeoutInternal :: !(Maybe Code.Timeout), -- | Team invitation timeout, in seconds - setTeamInvitationTimeout :: !Timeout, + teamInvitationTimeout :: !Timeout, -- | Check for expired users every so often, in seconds - setExpiredUserCleanupTimeout :: !(Maybe Timeout), + expiredUserCleanupTimeout :: !(Maybe Timeout), -- | STOMP broker credentials - setStomp :: !(Maybe FilePathSecrets), + stomp :: !(Maybe FilePathSecrets), -- | Whitelist of allowed emails/phones - setAllowlistEmailDomains :: !(Maybe AllowlistEmailDomains), + allowlistEmailDomains :: !(Maybe AllowlistEmailDomains), -- | Max. number of sent/accepted -- connections per user - setUserMaxConnections :: !Int64, + userMaxConnections :: !Int64, -- | Max. number of permanent clients per user - setUserMaxPermClients :: !(Maybe Int), + userMaxPermClients :: !(Maybe Int), -- | Whether to allow plain HTTP transmission -- of cookies (for testing purposes only) - setCookieInsecure :: !Bool, + cookieInsecure :: !Bool, -- | Minimum age of a user cookie before -- it is renewed during token refresh - setUserCookieRenewAge :: !Integer, + userCookieRenewAge :: !Integer, -- | Max. # of cookies per user and cookie type - setUserCookieLimit :: !Int, - -- | Throttling settings (not to be confused + userCookieLimit :: !Int, + -- | Throttling tings (not to be confused -- with 'LoginRetryOpts') - setUserCookieThrottle :: !CookieThrottle, + userCookieThrottle :: !CookieThrottle, -- | Block user from logging in -- for m minutes after n failed -- logins - setLimitFailedLogins :: !(Maybe LimitFailedLogins), + limitFailedLogins :: !(Maybe LimitFailedLogins), -- | If last cookie renewal is too long ago, -- suspend the user. - setSuspendInactiveUsers :: !(Maybe SuspendInactiveUsers), + suspendInactiveUsers :: !(Maybe SuspendInactiveUsers), -- | Max size of rich info (number of chars in -- field names and values), should be in sync -- with Spar - setRichInfoLimit :: !Int, + richInfoLimit :: !Int, -- | Default locale to use when selecting templates - -- use `setDefaultTemplateLocale` as the getter function which always provides a default value - setDefaultTemplateLocaleInternal :: !(Maybe Locale), + -- use `defaultTemplateLocale` as the getter function which always provides a default value + defaultTemplateLocaleInternal :: !(Maybe Locale), -- | Default locale to use for users - -- use `setDefaultUserLocale` as the getter function which always provides a default value - setDefaultUserLocaleInternal :: !(Maybe Locale), + -- use `defaultUserLocale` as the getter function which always provides a default value + defaultUserLocaleInternal :: !(Maybe Locale), -- | Max. # of members in a team. -- NOTE: This must be in sync with galley - setMaxTeamSize :: !Word32, + maxTeamSize :: !Word32, -- | Max. # of members in a conversation. -- NOTE: This must be in sync with galley - setMaxConvSize :: !Word16, + maxConvSize :: !Word16, -- | Filter ONLY services with -- the given provider id - setProviderSearchFilter :: !(Maybe ProviderId), + providerSearchFilter :: !(Maybe ProviderId), -- | Whether to expose user emails and to whom - setEmailVisibility :: !EmailVisibilityConfig, - setPropertyMaxKeyLen :: !(Maybe Int64), - setPropertyMaxValueLen :: !(Maybe Int64), + emailVisibility :: !EmailVisibilityConfig, + propertyMaxKeyLen :: !(Maybe Int64), + propertyMaxValueLen :: !(Maybe Int64), -- | How long, in milliseconds, to wait -- in between processing delete events -- from the internal delete queue - setDeleteThrottleMillis :: !(Maybe Int), + deleteThrottleMillis :: !(Maybe Int), -- | When true, search only -- returns users from the same team - setSearchSameTeamOnly :: !(Maybe Bool), + searchSameTeamOnly :: !(Maybe Bool), -- | FederationDomain is required, even when not wanting to federate with other backends - -- (in that case the 'setFederationStrategy' can be set to `allowNone` below, or to + -- (in that case the 'federationStrategy' can be set to `allowNone` below, or to -- `allowDynamic` while keeping the list of allowed domains empty, see -- https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections) -- Federation domain is used to qualify local IDs and handles, @@ -524,20 +509,20 @@ data Settings = Settings -- >>> _wire-server-federator._tcp. -- Once set, DO NOT change it: if you do, existing users may have a broken experience and/or stop working. -- Remember to keep it the same in all services. - setFederationDomain :: !Domain, + federationDomain :: !Domain, -- | See https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections -- default: AllowNone - setFederationStrategy :: !(Maybe FederationStrategy), - -- | 'setFederationDomainConfigs' is introduced in + federationStrategy :: !(Maybe FederationStrategy), + -- | 'federationDomainConfigs' is introduced in -- https://github.com/wireapp/wire-server/pull/3260 for the sole purpose of transitioning -- to dynamic federation remote configuration. See -- https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections -- for details. -- default: [] - setFederationDomainConfigs :: !(Maybe [ImplicitNoFederationRestriction]), + federationDomainConfigs :: !(Maybe [ImplicitNoFederationRestriction]), -- | In seconds. Default: 10 seconds. Values <1 are silently replaced by 1. See -- https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections - setFederationDomainConfigsUpdateFreq :: !(Maybe Int), + federationDomainConfigsUpdateFreq :: !(Maybe Int), -- | The amount of time in milliseconds to wait after reading from an SQS queue -- returns no message, before asking for messages from SQS again. -- defaults to 'defSqsThrottleMillis'. @@ -546,60 +531,62 @@ data Settings = Settings -- ensures that there is only one request every 20 seconds. -- However, that parameter is not honoured when using fake-sqs -- (where throttling can thus make sense) - setSqsThrottleMillis :: !(Maybe Int), + sqsThrottleMillis :: !(Maybe Int), -- | Do not allow certain user creation flows. -- docs/reference/user/registration.md {#RefRestrictRegistration}. - setRestrictUserCreation :: !(Maybe Bool), - -- | The analog to `Galley.Options.setFeatureFlags`. See 'AccountFeatureConfigs'. - setFeatureFlags :: !(Maybe AccountFeatureConfigs), + restrictUserCreation :: !(Maybe Bool), + -- | The analog to `Galley.Options.featureFlags`. See 'AccountFeatureConfigs'. + featureFlags :: !(Maybe UserFeatureFlags), -- | Customer extensions. Read 'CustomerExtensions' docs carefully! - setCustomerExtensions :: !(Maybe CustomerExtensions), + customerExtensions :: !(Maybe CustomerExtensions), -- | When set; instead of using SRV lookups to discover SFTs the calls -- config will always return this entry. This is useful in Kubernetes -- where SFTs are deployed behind a load-balancer. In the long-run the SRV -- fetching logic can go away completely - setSftStaticUrl :: !(Maybe HttpsUrl), + sftStaticUrl :: !(Maybe HttpsUrl), -- | When set the /calls/config/v2 endpoint will include all the - -- loadbalanced servers of `setSftStaticUrl` under the @sft_servers_all@ - -- field. The default setting is to exclude and omit the field from the + -- loadbalanced servers of `sftStaticUrl` under the @sft_servers_all@ + -- field. The default ting is to exclude and omit the field from the -- response. - setSftListAllServers :: Maybe ListAllSFTServers, - setEnableMLS :: Maybe Bool, - setKeyPackageMaximumLifetime :: Maybe NominalDiffTime, + sftListAllServers :: Maybe ListAllSFTServers, + enableMLS :: Maybe Bool, + keyPackageMaximumLifetime :: Maybe NominalDiffTime, -- | Disabled versions are not advertised and are completely disabled. - setDisabledAPIVersions :: !(Set VersionExp), + disabledAPIVersions :: !(Set VersionExp), -- | Minimum delay in seconds between consecutive attempts to generate a new verification code. - -- use `set2FACodeGenerationDelaySecs` as the getter function which always provides a default value - set2FACodeGenerationDelaySecsInternal :: !(Maybe Int), + -- use `2FACodeGenerationDelaySecs` as the getter function which always provides a default value + twoFACodeGenerationDelaySecsInternal :: !(Maybe Int), -- | The time-to-live of a nonce in seconds. - -- use `setNonceTtlSecs` as the getter function which always provides a default value - setNonceTtlSecsInternal :: !(Maybe NonceTtlSecs), + -- use `nonceTtlSecs` as the getter function which always provides a default value + nonceTtlSecsInternal :: !(Maybe NonceTtlSecs), -- | The maximum number of seconds of clock skew the implementation of generate_dpop_access_token in jwt-tools will allow - -- use `setDpopMaxSkewSecs` as the getter function which always provides a default value - setDpopMaxSkewSecsInternal :: !(Maybe Word16), + -- use `dpopMaxSkewSecs` as the getter function which always provides a default value + dpopMaxSkewSecsInternal :: !(Maybe Word16), -- | The expiration time of a JWT DPoP token in seconds. - -- use `setDpopTokenExpirationTimeSecs` as the getter function which always provides a default value - setDpopTokenExpirationTimeSecsInternal :: !(Maybe Word64), + -- use `dpopTokenExpirationTimeSecs` as the getter function which always provides a default value + dpopTokenExpirationTimeSecsInternal :: !(Maybe Word64), -- | Path to a .pem file containing the server's public key and private key -- e.g. to sign JWT tokens - setPublicKeyBundle :: !(Maybe FilePath), + publicKeyBundle :: !(Maybe FilePath), -- | Path to the public and private JSON web key pair used to sign OAuth access tokens - setOAuthJwkKeyPair :: !(Maybe FilePath), + oAuthJwkKeyPair :: !(Maybe FilePath), -- | The expiration time of an OAuth access token in seconds. - -- use `setOAuthAccessTokenExpirationTimeSecs` as the getter function which always provides a default value - setOAuthAccessTokenExpirationTimeSecsInternal :: !(Maybe Word64), + -- use `oAuthAccessTokenExpirationTimeSecs` as the getter function which always provides a default value + oAuthAccessTokenExpirationTimeSecsInternal :: !(Maybe Word64), -- | The expiration time of an OAuth authorization code in seconds. - -- use `setOAuthAuthorizationCodeExpirationTimeSecs` as the getter function which always provides a default value - setOAuthAuthorizationCodeExpirationTimeSecsInternal :: !(Maybe Word64), + -- use `oAuthAuthorizationCodeExpirationTimeSecs` as the getter function which always provides a default value + oAuthAuthorizationCodeExpirationTimeSecsInternal :: !(Maybe Word64), -- | En-/Disable OAuth - -- use `setOAuthEnabled` as the getter function which always provides a default value - setOAuthEnabledInternal :: !(Maybe Bool), + -- use `oAuthEnabled` as the getter function which always provides a default value + oAuthEnabledInternal :: !(Maybe Bool), -- | The expiration time of an OAuth refresh token in seconds. - -- use `setOAuthRefreshTokenExpirationTimeSecs` as the getter function which always provides a default value - setOAuthRefreshTokenExpirationTimeSecsInternal :: !(Maybe Word64), + -- use `oAuthRefreshTokenExpirationTimeSecs` as the getter function which always provides a default value + oAuthRefreshTokenExpirationTimeSecsInternal :: !(Maybe Word64), -- | The maximum number of active OAuth refresh tokens a user is allowed to have. - -- use `setOAuthMaxActiveRefreshTokens` as the getter function which always provides a default value - setOAuthMaxActiveRefreshTokensInternal :: !(Maybe Word32) + -- use `oAuthMaxActiveRefreshTokens` as the getter function which always provides a default value + oAuthMaxActiveRefreshTokensInternal :: !(Maybe Word32), + -- | Options to override the default Argon2id settings for specific operators. + passwordHashingOptions :: !(PasswordHashingOptions) } deriving (Show, Generic) @@ -609,155 +596,117 @@ newtype ImplicitNoFederationRestriction = ImplicitNoFederationRestriction instance FromJSON ImplicitNoFederationRestriction where parseJSON = - Aeson.withObject + withObject "ImplicitNoFederationRestriction" ( \obj -> do - domain <- obj Aeson..: "domain" - searchPolicy <- obj Aeson..: "search_policy" + domain <- obj .: "domain" + searchPolicy <- obj .: "search_policy" pure . ImplicitNoFederationRestriction $ FederationDomainConfig domain searchPolicy FederationRestrictionAllowAll ) -defaultTemplateLocale :: Locale -defaultTemplateLocale = Locale (Language EN) Nothing - -defaultUserLocale :: Locale -defaultUserLocale = defaultTemplateLocale - -setDefaultUserLocale :: Settings -> Locale -setDefaultUserLocale = fromMaybe defaultUserLocale . setDefaultUserLocaleInternal +defaultLocale :: Locale +defaultLocale = Locale (Language EN) Nothing -defVerificationTimeout :: Code.Timeout -defVerificationTimeout = Code.Timeout (60 * 10) -- 10 minutes +defaultUserLocale :: Settings -> Locale +defaultUserLocale = fromMaybe defaultLocale . defaultUserLocaleInternal -setVerificationTimeout :: Settings -> Code.Timeout -setVerificationTimeout = fromMaybe defVerificationTimeout . setVerificationCodeTimeoutInternal +defaultTemplateLocale :: Settings -> Locale +defaultTemplateLocale = fromMaybe defaultLocale . defaultTemplateLocaleInternal -setDefaultTemplateLocale :: Settings -> Locale -setDefaultTemplateLocale = fromMaybe defaultTemplateLocale . setDefaultTemplateLocaleInternal +verificationTimeout :: Settings -> Code.Timeout +verificationTimeout = fromMaybe defVerificationTimeout . verificationCodeTimeoutInternal + where + defVerificationTimeout :: Code.Timeout + defVerificationTimeout = Code.Timeout (60 * 10) -- 10 minutes -def2FACodeGenerationDelaySecs :: Int -def2FACodeGenerationDelaySecs = 5 * 60 -- 5 minutes +twoFACodeGenerationDelaySecs :: Settings -> Int +twoFACodeGenerationDelaySecs = fromMaybe def2FACodeGenerationDelaySecs . twoFACodeGenerationDelaySecsInternal + where + def2FACodeGenerationDelaySecs :: Int + def2FACodeGenerationDelaySecs = 5 * 60 -- 5 minutes -set2FACodeGenerationDelaySecs :: Settings -> Int -set2FACodeGenerationDelaySecs = fromMaybe def2FACodeGenerationDelaySecs . set2FACodeGenerationDelaySecsInternal - -defaultNonceTtlSecs :: NonceTtlSecs -defaultNonceTtlSecs = NonceTtlSecs $ 5 * 60 -- 5 minutes - -setNonceTtlSecs :: Settings -> NonceTtlSecs -setNonceTtlSecs = fromMaybe defaultNonceTtlSecs . setNonceTtlSecsInternal - -defaultDpopMaxSkewSecs :: Word16 -defaultDpopMaxSkewSecs = 1 +nonceTtlSecs :: Settings -> NonceTtlSecs +nonceTtlSecs = fromMaybe defaultNonceTtlSecs . nonceTtlSecsInternal + where + defaultNonceTtlSecs :: NonceTtlSecs + defaultNonceTtlSecs = NonceTtlSecs $ 5 * 60 -- 5 minutes setDpopMaxSkewSecs :: Settings -> Word16 -setDpopMaxSkewSecs = fromMaybe defaultDpopMaxSkewSecs . setDpopMaxSkewSecsInternal - -defaultDpopTokenExpirationTimeSecs :: Word64 -defaultDpopTokenExpirationTimeSecs = 30 - -setDpopTokenExpirationTimeSecs :: Settings -> Word64 -setDpopTokenExpirationTimeSecs = fromMaybe defaultDpopTokenExpirationTimeSecs . setDpopTokenExpirationTimeSecsInternal - -defaultOAuthAccessTokenExpirationTimeSecs :: Word64 -defaultOAuthAccessTokenExpirationTimeSecs = 60 * 60 * 24 * 7 * 3 -- 3 weeks - -setOAuthAccessTokenExpirationTimeSecs :: Settings -> Word64 -setOAuthAccessTokenExpirationTimeSecs = fromMaybe defaultOAuthAccessTokenExpirationTimeSecs . setOAuthAccessTokenExpirationTimeSecsInternal - -defaultOAuthAuthorizationCodeExpirationTimeSecs :: Word64 -defaultOAuthAuthorizationCodeExpirationTimeSecs = 300 -- 5 minutes - -setOAuthAuthorizationCodeExpirationTimeSecs :: Settings -> Word64 -setOAuthAuthorizationCodeExpirationTimeSecs = fromMaybe defaultOAuthAuthorizationCodeExpirationTimeSecs . setOAuthAuthorizationCodeExpirationTimeSecsInternal - -defaultOAuthEnabled :: Bool -defaultOAuthEnabled = False - -setOAuthEnabled :: Settings -> Bool -setOAuthEnabled = fromMaybe defaultOAuthEnabled . setOAuthEnabledInternal - -defaultOAuthRefreshTokenExpirationTimeSecs :: Word64 -defaultOAuthRefreshTokenExpirationTimeSecs = 60 * 60 * 24 * 7 * 4 * 6 -- 24 weeks - -setOAuthRefreshTokenExpirationTimeSecs :: Settings -> Word64 -setOAuthRefreshTokenExpirationTimeSecs = fromMaybe defaultOAuthRefreshTokenExpirationTimeSecs . setOAuthRefreshTokenExpirationTimeSecsInternal - -defaultOAuthMaxActiveRefreshTokens :: Word32 -defaultOAuthMaxActiveRefreshTokens = 10 - -setOAuthMaxActiveRefreshTokens :: Settings -> Word32 -setOAuthMaxActiveRefreshTokens = fromMaybe defaultOAuthMaxActiveRefreshTokens . setOAuthMaxActiveRefreshTokensInternal - --- | The analog to `GT.FeatureFlags`. This type tracks only the things that we need to --- express our current cloud business logic. --- --- FUTUREWORK: it would be nice to have a system of feature configs that allows to coherently --- express arbitrary logic across personal and team accounts, teams, and instances; including --- default values for new records, default for records that have a NULL value (eg., because --- they are grandfathered), and feature-specific extra data (eg., TLL for self-deleting --- messages). For now, we have something quick & simple. -data AccountFeatureConfigs = AccountFeatureConfigs - { afcConferenceCallingDefNew :: !(Public.ImplicitLockStatus Public.ConferenceCallingConfig), - afcConferenceCallingDefNull :: !(Public.ImplicitLockStatus Public.ConferenceCallingConfig) +setDpopMaxSkewSecs = fromMaybe defaultDpopMaxSkewSecs . dpopMaxSkewSecsInternal + where + defaultDpopMaxSkewSecs :: Word16 + defaultDpopMaxSkewSecs = 1 + +dpopTokenExpirationTimeSecs :: Settings -> Word64 +dpopTokenExpirationTimeSecs = fromMaybe defaultDpopTokenExpirationTimeSecs . dpopTokenExpirationTimeSecsInternal + where + defaultDpopTokenExpirationTimeSecs :: Word64 + defaultDpopTokenExpirationTimeSecs = 30 + +oAuthAccessTokenExpirationTimeSecs :: Settings -> Word64 +oAuthAccessTokenExpirationTimeSecs = fromMaybe defaultOAuthAccessTokenExpirationTimeSecs . oAuthAccessTokenExpirationTimeSecsInternal + where + defaultOAuthAccessTokenExpirationTimeSecs :: Word64 + defaultOAuthAccessTokenExpirationTimeSecs = 60 * 60 * 24 * 7 * 3 -- 3 weeks + +oAuthAuthorizationCodeExpirationTimeSecs :: Settings -> Word64 +oAuthAuthorizationCodeExpirationTimeSecs = fromMaybe defaultOAuthAuthorizationCodeExpirationTimeSecs . oAuthAuthorizationCodeExpirationTimeSecsInternal + where + defaultOAuthAuthorizationCodeExpirationTimeSecs :: Word64 + defaultOAuthAuthorizationCodeExpirationTimeSecs = 300 -- 5 minutes + +oAuthEnabled :: Settings -> Bool +oAuthEnabled = fromMaybe defaultOAuthEnabled . oAuthEnabledInternal + where + defaultOAuthEnabled :: Bool + defaultOAuthEnabled = False + +oAuthRefreshTokenExpirationTimeSecs :: Settings -> Word64 +oAuthRefreshTokenExpirationTimeSecs = fromMaybe defaultOAuthRefreshTokenExpirationTimeSecs . oAuthRefreshTokenExpirationTimeSecsInternal + where + defaultOAuthRefreshTokenExpirationTimeSecs :: Word64 + defaultOAuthRefreshTokenExpirationTimeSecs = 60 * 60 * 24 * 7 * 4 * 6 -- 24 weeks + +oAuthMaxActiveRefreshTokens :: Settings -> Word32 +oAuthMaxActiveRefreshTokens = fromMaybe defaultOAuthMaxActiveRefreshTokens . oAuthMaxActiveRefreshTokensInternal + where + defaultOAuthMaxActiveRefreshTokens :: Word32 + defaultOAuthMaxActiveRefreshTokens = 10 + +-- | The analog to `FeatureFlags`. At the moment, only status flags for +-- conferenceCalling are stored. +data UserFeatureFlags = UserFeatureFlags + { conferenceCalling :: UserFeature ConferenceCallingConfig } - deriving (Show, Eq, Generic) + deriving (Eq, Ord, Show) -instance Arbitrary AccountFeatureConfigs where - arbitrary = AccountFeatureConfigs <$> fmap unlocked arbitrary <*> fmap unlocked arbitrary - where - unlocked :: Public.ImplicitLockStatus a -> Public.ImplicitLockStatus a - unlocked = Public.ImplicitLockStatus . Public.setLockStatus Public.LockStatusUnlocked . Public._unImplicitLockStatus - -instance FromJSON AccountFeatureConfigs where - parseJSON = - Aeson.withObject - "AccountFeatureConfigs" - ( \obj -> do - confCallInit <- obj Aeson..: "conferenceCalling" - Aeson.withObject - "conferenceCalling" - ( \obj' -> do - AccountFeatureConfigs - <$> obj' Aeson..: "defaultForNew" - <*> obj' Aeson..: "defaultForNull" - ) - confCallInit - ) - -instance ToJSON AccountFeatureConfigs where - toJSON - AccountFeatureConfigs - { afcConferenceCallingDefNew, - afcConferenceCallingDefNull - } = - Aeson.object - [ "conferenceCalling" - Aeson..= Aeson.object - [ "defaultForNew" Aeson..= afcConferenceCallingDefNew, - "defaultForNull" Aeson..= afcConferenceCallingDefNull - ] - ] +instance FromJSON UserFeatureFlags where + parseJSON = withObject "UserFeatureFlags" $ \obj -> do + UserFeatureFlags + <$> obj .:? "conferenceCalling" .!= def -getAfcConferenceCallingDefNewMaybe :: Lens.Getter Settings (Maybe (Public.WithStatus Public.ConferenceCallingConfig)) -getAfcConferenceCallingDefNewMaybe = Lens.to (Lens.^? (Lens.to setFeatureFlags . Lens._Just . Lens.to afcConferenceCallingDefNew . unImplicitLockStatus)) +data family UserFeature cfg -getAfcConferenceCallingDefNullMaybe :: Lens.Getter Settings (Maybe (Public.WithStatus Public.ConferenceCallingConfig)) -getAfcConferenceCallingDefNullMaybe = Lens.to (Lens.^? (Lens.to setFeatureFlags . Lens._Just . Lens.to afcConferenceCallingDefNull . unImplicitLockStatus)) +data instance UserFeature ConferenceCallingConfig = ConferenceCallingUserStatus + { -- | This will be set as the status of the feature for newly created users. + forNew :: Maybe FeatureStatus, + -- | How an unset status for this feature should be interpreted. + forNull :: FeatureStatus + } + deriving (Eq, Ord, Show) -getAfcConferenceCallingDefNew :: Lens.Getter Settings (Public.WithStatus Public.ConferenceCallingConfig) -getAfcConferenceCallingDefNew = Lens.to (Public._unImplicitLockStatus . afcConferenceCallingDefNew . fromMaybe defAccountFeatureConfigs . setFeatureFlags) +instance Default (UserFeature ConferenceCallingConfig) where + def = ConferenceCallingUserStatus Nothing FeatureStatusEnabled -getAfcConferenceCallingDefNull :: Lens.Getter Settings (Public.WithStatus Public.ConferenceCallingConfig) -getAfcConferenceCallingDefNull = Lens.to (Public._unImplicitLockStatus . afcConferenceCallingDefNull . fromMaybe defAccountFeatureConfigs . setFeatureFlags) +instance FromJSON (UserFeature ConferenceCallingConfig) where + parseJSON = withObject "UserFeatureConferenceCalling" $ \obj -> do + ConferenceCallingUserStatus + <$> A.explicitParseFieldMaybe parseUserFeatureStatus obj "defaultForNew" + <*> A.explicitParseFieldMaybe parseUserFeatureStatus obj "defaultForNull" .!= forNull def -defAccountFeatureConfigs :: AccountFeatureConfigs -defAccountFeatureConfigs = - AccountFeatureConfigs - { afcConferenceCallingDefNew = Public.ImplicitLockStatus Public.defFeatureStatus, - afcConferenceCallingDefNull = Public.ImplicitLockStatus Public.defFeatureStatus - } +parseUserFeatureStatus :: A.Value -> A.Parser FeatureStatus +parseUserFeatureStatus = withObject "UserFeatureStatus" $ \obj -> obj .: "status" -- | Customer extensions naturally are covered by the AGPL like everything else, but use them -- at your own risk! If you use the default server config and do not set @@ -814,7 +763,7 @@ data SFTOptions = SFTOptions deriving (Show, Generic) instance FromJSON SFTOptions where - parseJSON = Y.withObject "SFTOptions" $ \o -> + parseJSON = withObject "SFTOptions" $ \o -> SFTOptions <$> (asciiOnly =<< o .: "sftBaseDomain") <*> (mapM asciiOnly =<< o .:? "sftSRVServiceName") @@ -829,12 +778,12 @@ data SFTTokenOptions = SFTTokenOptions deriving (Show, Generic) instance FromJSON SFTTokenOptions where - parseJSON = Y.withObject "SFTTokenOptions" $ \o -> + parseJSON = withObject "SFTTokenOptions" $ \o -> SFTTokenOptions <$> (o .: "ttl") <*> (o .: "secret") -asciiOnly :: Text -> Y.Parser ByteString +asciiOnly :: Text -> A.Parser ByteString asciiOnly t = if Text.all Char.isAscii t then pure $ Text.encodeUtf8 t @@ -864,15 +813,10 @@ defSrvDiscoveryIntervalSeconds = secondsToDiffTime 10 defSftListLength :: Range 1 100 Int defSftListLength = unsafeRange 5 -instance FromJSON Timeout where - parseJSON (Y.Number n) = - let defaultV = 3600 - bounded = toBoundedInteger n :: Maybe Int64 - in pure $ - Timeout $ - fromIntegral @Int $ - maybe defaultV fromIntegral bounded - parseJSON v = typeMismatch "activationTimeout" v +-- | Convert a word to title case by capitalising the first letter +capitalise :: String -> String +capitalise [] = [] +capitalise (c : cs) = toUpper c : cs instance FromJSON Settings where parseJSON = genericParseJSON customOptions @@ -880,70 +824,44 @@ instance FromJSON Settings where customOptions = defaultOptions { fieldLabelModifier = \case - "setDefaultUserLocaleInternal" -> "setDefaultUserLocale" - "setDefaultTemplateLocaleInternal" -> "setDefaultTemplateLocale" - "setVerificationCodeTimeoutInternal" -> "setVerificationTimeout" - "set2FACodeGenerationDelaySecsInternal" -> "set2FACodeGenerationDelaySecs" - "setNonceTtlSecsInternal" -> "setNonceTtlSecs" - "setDpopMaxSkewSecsInternal" -> "setDpopMaxSkewSecs" - "setDpopTokenExpirationTimeSecsInternal" -> "setDpopTokenExpirationTimeSecs" - "setOAuthAuthorizationCodeExpirationTimeSecsInternal" -> "setOAuthAuthorizationCodeExpirationTimeSecs" - "setOAuthAccessTokenExpirationTimeSecsInternal" -> "setOAuthAccessTokenExpirationTimeSecs" - "setOAuthEnabledInternal" -> "setOAuthEnabled" - "setOAuthRefreshTokenExpirationTimeSecsInternal" -> "setOAuthRefreshTokenExpirationTimeSecs" - "setOAuthMaxActiveRefreshTokensInternal" -> "setOAuthMaxActiveRefreshTokens" + "defaultUserLocaleInternal" -> "setDefaultUserLocale" + "defaultTemplateLocaleInternal" -> "setDefaultTemplateLocale" + "verificationCodeTimeoutInternal" -> "setVerificationTimeout" + "twoFACodeGenerationDelaySecsInternal" -> "set2FACodeGenerationDelaySecs" + "nonceTtlSecsInternal" -> "setNonceTtlSecs" + "dpopMaxSkewSecsInternal" -> "setDpopMaxSkewSecs" + "dpopTokenExpirationTimeSecsInternal" -> "setDpopTokenExpirationTimeSecs" + "oAuthAuthorizationCodeExpirationTimeSecsInternal" -> "setOAuthAuthorizationCodeExpirationTimeSecs" + "oAuthAccessTokenExpirationTimeSecsInternal" -> "setOAuthAccessTokenExpirationTimeSecs" + "oAuthEnabledInternal" -> "setOAuthEnabled" + "oAuthRefreshTokenExpirationTimeSecsInternal" -> "setOAuthRefreshTokenExpirationTimeSecs" + "oAuthMaxActiveRefreshTokensInternal" -> "setOAuthMaxActiveRefreshTokens" + other -> "set" <> capitalise other + } + +instance FromJSON Opts where + parseJSON = genericParseJSON customOptions + where + customOptions = + defaultOptions + { fieldLabelModifier = \case + "settings" -> "optSettings" + "stompOptions" -> "stomp" other -> other } -instance FromJSON Opts - --- TODO: Does it make sense to generate lens'es for all? -Lens.makeLensesFor - [ ("optSettings", "optionSettings"), - ("elasticsearch", "elasticsearchL"), - ("sft", "sftL"), - ("turn", "turnL"), - ("multiSFT", "multiSFTL") - ] - ''Opts - -Lens.makeLensesFor - [ ("setEmailVisibility", "emailVisibility"), - ("setPropertyMaxKeyLen", "propertyMaxKeyLen"), - ("setPropertyMaxValueLen", "propertyMaxValueLen"), - ("setSearchSameTeamOnly", "searchSameTeamOnly"), - ("setUserMaxPermClients", "userMaxPermClients"), - ("setFederationDomain", "federationDomain"), - ("setSqsThrottleMillis", "sqsThrottleMillis"), - ("setSftStaticUrl", "sftStaticUrl"), - ("setSftListAllServers", "sftListAllServers"), - ("setFederationDomainConfigs", "federationDomainConfigs"), - ("setFederationStrategy", "federationStrategy"), - ("setRestrictUserCreation", "restrictUserCreation"), - ("setEnableMLS", "enableMLS"), - ("setOAuthEnabledInternal", "oauthEnabledInternal"), - ("setOAuthAuthorizationCodeExpirationTimeSecsInternal", "oauthAuthorizationCodeExpirationTimeSecsInternal"), - ("setOAuthAccessTokenExpirationTimeSecsInternal", "oauthAccessTokenExpirationTimeSecsInternal"), - ("setDisabledAPIVersions", "disabledAPIVersions"), - ("setOAuthRefreshTokenExpirationTimeSecsInternal", "oauthRefreshTokenExpirationTimeSecsInternal"), - ("setOAuthMaxActiveRefreshTokensInternal", "oauthMaxActiveRefreshTokensInternal"), - ("setAllowlistEmailDomains", "allowlistEmailDomains"), - ("setAllowlistPhonePrefixes", "allowlistPhonePrefixes") - ] - ''Settings - -Lens.makeLensesFor - [ ("url", "urlL"), - ("index", "indexL"), - ("caCert", "caCertL"), - ("insecureSkipVerifyTls", "insecureSkipVerifyTlsL"), - ("additionalWriteIndex", "additionalWriteIndexL"), - ("additionalWriteIndexUrl", "additionalWriteIndexUrlL"), - ("additionalCaCert", "additionalCaCertL"), - ("additionalInsecureSkipVerifyTls", "additionalInsecureSkipVerifyTlsL") - ] - ''ElasticSearchOpts - -Lens.makeLensesFor [("sftBaseDomain", "sftBaseDomainL")] ''SFTOptions - -Lens.makeLensesFor [("serversSource", "serversSourceL")] ''TurnOpts +instance FromJSON StompOpts where + parseJSON = genericParseJSON customOptions + where + customOptions = + defaultOptions + { fieldLabelModifier = \a -> "stom" <> capitalise a + } + +makeLensesWith (lensRules & lensField .~ suffixNamer) ''Opts + +makeLensesWith (lensRules & lensField .~ suffixNamer) ''Settings + +makeLensesWith (lensRules & lensField .~ suffixNamer) ''ElasticSearchOpts + +makeLensesWith (lensRules & lensField .~ suffixNamer) ''TurnOpts diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index c9e13f86cdf..162a1109060 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -42,13 +42,11 @@ import Brig.Provider.DB (ServiceConn (..)) import Brig.Provider.DB qualified as DB import Brig.Provider.Email import Brig.Provider.RPC qualified as RPC -import Brig.Team.Util -import Brig.Types.User import Brig.ZAuth qualified as ZAuth import Cassandra (MonadClient) import Control.Error (throwE) import Control.Exception.Enclosed (handleAny) -import Control.Lens (view, (^.)) +import Control.Lens ((^.)) import Control.Monad.Catch (MonadMask) import Control.Monad.Except import Data.ByteString.Conversion @@ -58,12 +56,17 @@ import Data.CommaSeparatedList (CommaSeparatedList (fromCommaSeparatedList)) import Data.Conduit (runConduit, (.|)) import Data.Conduit.List qualified as C import Data.Hashable (hash) +import Data.HavePendingInvitations import Data.Id import Data.LegalHold import Data.List qualified as List import Data.List1 (maybeList1) import Data.Map.Strict qualified as Map -import Data.Misc (Fingerprint (..), FutureWork (FutureWork), Rsa) +import Data.Misc + ( Fingerprint (Fingerprint), + FutureWork (FutureWork), + Rsa, + ) import Data.Qualified import Data.Range import Data.Set qualified as Set @@ -81,6 +84,7 @@ import OpenSSL.PEM qualified as SSL import OpenSSL.RSA qualified as SSL import OpenSSL.Random (randBytes) import Polysemy +import Polysemy.Error import Servant (ServerT, (:<|>) (..)) import Ssl.Util qualified as SSL import System.Logger.Class (MonadLogger) @@ -88,6 +92,7 @@ import UnliftIO.Async (pooledMapConcurrentlyN_) import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Bot import Wire.API.Conversation.Bot qualified as Public +import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Brig qualified as E @@ -110,20 +115,24 @@ import Wire.API.Routes.Public.Brig.Services (ServicesAPI) import Wire.API.Team.Feature qualified as Feature import Wire.API.Team.LegalHold (LegalholdProtectee (UnprotectedBot)) import Wire.API.Team.Permission -import Wire.API.User hiding (cpNewPassword, cpOldPassword) +import Wire.API.User import Wire.API.User qualified as Public (UserProfile, mkUserProfile) import Wire.API.User.Auth import Wire.API.User.Client import Wire.API.User.Client qualified as Public (Client, ClientCapability (ClientSupportsLegalholdImplicitConsent), PubClient (..), UserClientPrekeyMap, UserClients, userClients) import Wire.API.User.Client.Prekey qualified as Public (PrekeyId) -import Wire.API.User.Identity qualified as Public (Email) +import Wire.AuthenticationSubsystem as Authentication import Wire.DeleteQueue import Wire.EmailSending (EmailSending) import Wire.Error import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess +import Wire.HashPassword (HashPassword) +import Wire.HashPassword qualified as HashPassword import Wire.Sem.Concurrency (Concurrency, ConcurrencySafety (Unsafe)) import Wire.UserKeyStore (mkEmailKey) +import Wire.UserSubsystem +import Wire.UserSubsystem.Error import Wire.VerificationCode as VerificationCode import Wire.VerificationCodeGen import Wire.VerificationCodeSubsystem @@ -131,24 +140,31 @@ import Wire.VerificationCodeSubsystem botAPI :: ( Member GalleyAPIAccess r, Member (Concurrency 'Unsafe) r, - Member DeleteQueue r + Member DeleteQueue r, + Member AuthenticationSubsystem r ) => ServerT BotAPI (Handler r) botAPI = - Named @"add-bot" addBot + Named @"add-bot@v6" addBot + :<|> Named @"add-bot" addBot + :<|> Named @"remove-bot@v6" removeBot :<|> Named @"remove-bot" removeBot :<|> Named @"bot-get-self" botGetSelf :<|> Named @"bot-delete-self" botDeleteSelf :<|> Named @"bot-list-prekeys" botListPrekeys :<|> Named @"bot-update-prekeys" botUpdatePrekeys - :<|> Named @"bot-get-client-v5" botGetClient + :<|> Named @"bot-get-client-v6" botGetClient :<|> Named @"bot-get-client" botGetClient :<|> Named @"bot-claim-users-prekeys" botClaimUsersPrekeys :<|> Named @"bot-list-users" botListUserProfiles :<|> Named @"bot-get-user-clients" botGetUserClients servicesAPI :: - (Member GalleyAPIAccess r, Member DeleteQueue r) => + ( Member GalleyAPIAccess r, + Member AuthenticationSubsystem r, + Member DeleteQueue r, + Member (Error UserSubsystemError) r + ) => ServerT ServicesAPI (Handler r) servicesAPI = Named @"post-provider-services" addService @@ -164,7 +180,14 @@ servicesAPI = :<|> Named @"get-whitelisted-services-by-team-id" searchTeamServiceProfiles :<|> Named @"post-team-whitelist-by-team-id" updateServiceWhitelist -providerAPI :: (Member GalleyAPIAccess r, Member EmailSending r, Member VerificationCodeSubsystem r) => ServerT ProviderAPI (Handler r) +providerAPI :: + ( Member GalleyAPIAccess r, + Member AuthenticationSubsystem r, + Member EmailSending r, + Member HashPassword r, + Member VerificationCodeSubsystem r + ) => + ServerT ProviderAPI (Handler r) providerAPI = Named @"provider-register" newAccount :<|> Named @"provider-activate" activateAccountKey @@ -178,29 +201,40 @@ providerAPI = :<|> Named @"provider-get-account" getAccount :<|> Named @"provider-get-profile" getProviderProfile -internalProviderAPI :: (Member GalleyAPIAccess r, Member VerificationCodeSubsystem r) => ServerT BrigIRoutes.ProviderAPI (Handler r) -internalProviderAPI = Named @"get-provider-activation-code" getActivationCodeH +internalProviderAPI :: + ( Member GalleyAPIAccess r, + Member VerificationCodeSubsystem r + ) => + ServerT BrigIRoutes.ProviderAPI (Handler r) +internalProviderAPI = Named @"get-provider-activation-code" getActivationCode -------------------------------------------------------------------------------- -- Public API (Unauthenticated) -newAccount :: (Member GalleyAPIAccess r, Member EmailSending r, Member VerificationCodeSubsystem r) => Public.NewProvider -> (Handler r) Public.NewProviderResponse +newAccount :: + ( Member GalleyAPIAccess r, + Member EmailSending r, + Member HashPassword r, + Member VerificationCodeSubsystem r + ) => + Public.NewProvider -> + (Handler r) Public.NewProviderResponse newAccount new = do guardSecondFactorDisabled Nothing - email <- case validateEmail (Public.newProviderEmail new) of - Right em -> pure em - Left _ -> throwStd (errorToWai @'E.InvalidEmail) - let name = Public.newProviderName new - let pass = Public.newProviderPassword new - let descr = fromRange (Public.newProviderDescr new) - let url = Public.newProviderUrl new + let email = new.newProviderEmail + let name = new.newProviderName + let pass = new.newProviderPassword + let descr = fromRange new.newProviderDescr + let url = new.newProviderUrl let emailKey = mkEmailKey email wrapClientE (DB.lookupKey emailKey) >>= mapM_ (const $ throwStd emailExists) (safePass, newPass) <- case pass of - Just newPass -> (,Nothing) <$> mkSafePasswordScrypt newPass + Just newPass -> do + hashed <- lift . liftSem $ HashPassword.hashPassword6 newPass + pure (hashed, Nothing) Nothing -> do newPass <- genPassword - safePass <- mkSafePasswordScrypt newPass + safePass <- lift . liftSem $ HashPassword.hashPassword8 newPass pure (safePass, Just newPass) pid <- wrapClientE $ DB.insertAccount name safePass url descr let gen = mkVerificationCodeGen email @@ -213,14 +247,21 @@ newAccount new = do (Timeout (3600 * 24)) -- 24h (Just (toUUID pid)) let key = codeKey code - let val = codeValue code - lift $ sendActivationMail name email key val False + let value = codeValue code + lift $ sendActivationMail name email key value False pure $ Public.NewProviderResponse pid newPass -activateAccountKey :: (Member GalleyAPIAccess r, Member EmailSending r, Member VerificationCodeSubsystem r) => Code.Key -> Code.Value -> (Handler r) (Maybe Public.ProviderActivationResponse) -activateAccountKey key val = do +activateAccountKey :: + ( Member GalleyAPIAccess r, + Member EmailSending r, + Member VerificationCodeSubsystem r + ) => + Code.Key -> + Code.Value -> + (Handler r) (Maybe Public.ProviderActivationResponse) +activateAccountKey key value = do guardSecondFactorDisabled Nothing - c <- (lift . liftSem $ verifyCode key IdentityVerification val) >>= maybeInvalidCode + c <- (lift . liftSem $ verifyCode key IdentityVerification value) >>= maybeInvalidCode (pid, email) <- case (codeAccount c, Just (codeFor c)) of (Just p, Just e) -> pure (Id p, e) _ -> throwStd (errorToWai @'E.InvalidCode) @@ -240,26 +281,34 @@ activateAccountKey key val = do lift $ sendApprovalConfirmMail name email pure . Just $ Public.ProviderActivationResponse email -getActivationCodeH :: (Member GalleyAPIAccess r, Member VerificationCodeSubsystem r) => Public.Email -> (Handler r) Code.KeyValuePair -getActivationCodeH e = do +getActivationCode :: + ( Member GalleyAPIAccess r, + Member VerificationCodeSubsystem r + ) => + EmailAddress -> + (Handler r) Code.KeyValuePair +getActivationCode email = do guardSecondFactorDisabled Nothing - email <- case validateEmail e of - Right em -> pure em - Left _ -> throwStd (errorToWai @'E.InvalidEmail) let gen = mkVerificationCodeGen email code <- lift . liftSem $ internalLookupCode gen.genKey IdentityVerification maybe (throwStd activationKeyNotFound) (pure . codeToKeyValuePair) code -login :: (Member GalleyAPIAccess r) => ProviderLogin -> Handler r ProviderTokenCookie +login :: + ( Member GalleyAPIAccess r, + Member AuthenticationSubsystem r + ) => + ProviderLogin -> + Handler r ProviderTokenCookie login l = do guardSecondFactorDisabled Nothing - pid <- wrapClientE (DB.lookupKey (mkEmailKey (providerLoginEmail l))) >>= maybeBadCredentials - pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials - unless (verifyPassword (providerLoginPassword l) pass) $ - throwStd (errorToWai @'E.BadCredentials) + pid <- + wrapClientE (DB.lookupKey (mkEmailKey (providerLoginEmail l))) + >>= maybeBadCredentials + unlessM (fst <$> (lift . liftSem $ Authentication.verifyProviderPassword pid l.providerLoginPassword)) do + throwStd (errorToWai @E.BadCredentials) token <- ZAuth.newProviderToken pid - s <- view settings - pure $ ProviderTokenCookie (ProviderToken token) (not (setCookieInsecure s)) + s <- asks (.settings) + pure $ ProviderTokenCookie (ProviderToken token) (not s.cookieInsecure) beginPasswordReset :: (Member GalleyAPIAccess r, Member EmailSending r, Member VerificationCodeSubsystem r) => Public.PasswordReset -> (Handler r) () beginPasswordReset (Public.PasswordReset target) = do @@ -268,22 +317,29 @@ beginPasswordReset (Public.PasswordReset target) = do let gen = mkVerificationCodeGen target (lift . liftSem $ createCode gen VerificationCode.PasswordReset (Retries 3) (Timeout 3600) (Just (toUUID pid))) >>= \case Left (CodeAlreadyExists code) -> + -- FUTUREWORK: use subsystem error throwE $ pwResetError (PasswordResetInProgress $ Just code.codeTTL) Right code -> lift $ sendPasswordResetMail target (code.codeKey) (code.codeValue) -completePasswordReset :: (Member GalleyAPIAccess r, Member VerificationCodeSubsystem r) => Public.CompletePasswordReset -> (Handler r) () -completePasswordReset (Public.CompletePasswordReset key val newpwd) = do +completePasswordReset :: + ( Member GalleyAPIAccess r, + Member AuthenticationSubsystem r, + Member VerificationCodeSubsystem r, + Member HashPassword r + ) => + Public.CompletePasswordReset -> + (Handler r) () +completePasswordReset (Public.CompletePasswordReset key value newpwd) = do guardSecondFactorDisabled Nothing - code <- (lift . liftSem $ verifyCode key VerificationCode.PasswordReset val) >>= maybeInvalidCode + code <- (lift . liftSem $ verifyCode key VerificationCode.PasswordReset value) >>= maybeInvalidCode case Id <$> code.codeAccount of - Nothing -> throwStd (errorToWai @'E.InvalidPasswordResetCode) + Nothing -> throwStd (errorToWai @E.InvalidPasswordResetCode) Just pid -> do - oldpass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials - when (verifyPassword newpwd oldpass) $ do - throwStd (errorToWai @'E.ResetPasswordMustDiffer) - wrapClientE $ do - DB.updateAccountPassword pid newpwd + whenM (fst <$> (lift . liftSem $ Authentication.verifyProviderPassword pid newpwd)) do + throwStd (errorToWai @E.ResetPasswordMustDiffer) + hashedPwd <- lift . liftSem $ HashPassword.hashPassword6 newpwd + wrapClientE $ DB.updateAccountPassword pid hashedPwd lift . liftSem $ deleteCode key VerificationCode.PasswordReset -------------------------------------------------------------------------------- @@ -305,12 +361,16 @@ updateAccountProfile pid upd = do (updateProviderUrl upd) (updateProviderDescr upd) -updateAccountEmail :: (Member GalleyAPIAccess r, Member EmailSending r, Member VerificationCodeSubsystem r) => ProviderId -> Public.EmailUpdate -> (Handler r) () -updateAccountEmail pid (Public.EmailUpdate new) = do +updateAccountEmail :: + ( Member GalleyAPIAccess r, + Member EmailSending r, + Member VerificationCodeSubsystem r + ) => + ProviderId -> + Public.EmailUpdate -> + (Handler r) () +updateAccountEmail pid (Public.EmailUpdate email) = do guardSecondFactorDisabled Nothing - email <- case validateEmail new of - Right em -> pure em - Left _ -> throwStd (errorToWai @'E.InvalidEmail) let emailKey = mkEmailKey email wrapClientE (DB.lookupKey emailKey) >>= mapM_ (const $ throwStd emailExists) let gen = mkVerificationCodeGen email @@ -324,15 +384,22 @@ updateAccountEmail pid (Public.EmailUpdate new) = do (Just (toUUID pid)) lift $ sendActivationMail (Name "name") email code.codeKey code.codeValue True -updateAccountPassword :: (Member GalleyAPIAccess r) => ProviderId -> Public.PasswordChange -> (Handler r) () +updateAccountPassword :: + ( Member GalleyAPIAccess r, + Member AuthenticationSubsystem r, + Member HashPassword r + ) => + ProviderId -> + Public.PasswordChange -> + (Handler r) () updateAccountPassword pid upd = do guardSecondFactorDisabled Nothing - pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials - unless (verifyPassword (oldPassword upd) pass) $ - throwStd (errorToWai @'E.BadCredentials) - when (verifyPassword (newPassword upd) pass) $ - throwStd (errorToWai @'E.ResetPasswordMustDiffer) - wrapClientE $ DB.updateAccountPassword pid (newPassword upd) + unlessM (fst <$> (lift . liftSem $ Authentication.verifyProviderPassword pid upd.oldPassword)) do + throwStd (errorToWai @E.BadCredentials) + whenM (fst <$> (lift . liftSem $ Authentication.verifyProviderPassword pid upd.newPassword)) do + throwStd (errorToWai @E.ResetPasswordMustDiffer) + hashedPwd <- lift . liftSem $ HashPassword.hashPassword6 upd.newPassword + wrapClientE $ DB.updateAccountPassword pid hashedPwd addService :: (Member GalleyAPIAccess r) => @@ -404,16 +471,17 @@ updateService pid sid upd = do (serviceEnabled svc) updateServiceConn :: - (Member GalleyAPIAccess r) => + ( Member GalleyAPIAccess r, + Member AuthenticationSubsystem r + ) => ProviderId -> ServiceId -> Public.UpdateServiceConn -> Handler r () updateServiceConn pid sid upd = do guardSecondFactorDisabled Nothing - pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials - unless (verifyPassword (updateServiceConnPassword upd) pass) $ - throwStd (errorToWai @'E.BadCredentials) + unlessM (fst <$> (lift . liftSem $ Authentication.verifyProviderPassword pid upd.updateServiceConnPassword)) $ + throwStd (errorToWai @E.BadCredentials) scon <- wrapClientE (DB.lookupServiceConn pid sid) >>= maybeServiceNotFound svc <- wrapClientE (DB.lookupServiceProfile pid sid) >>= maybeServiceNotFound let newBaseUrl = updateServiceConnUrl upd @@ -452,6 +520,7 @@ updateServiceConn pid sid upd = do -- delete the service. See 'finishDeleteService'. deleteService :: ( Member GalleyAPIAccess r, + Member AuthenticationSubsystem r, Member DeleteQueue r ) => ProviderId -> @@ -460,10 +529,8 @@ deleteService :: (Handler r) () deleteService pid sid del = do guardSecondFactorDisabled Nothing - pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials - -- We don't care about pwd status when deleting things - unless (verifyPassword (deleteServicePassword del) pass) $ - throwStd (errorToWai @'E.BadCredentials) + unlessM (fst <$> (lift . liftSem $ Authentication.verifyProviderPassword pid del.deleteServicePassword)) do + throwStd (errorToWai @E.BadCredentials) _ <- wrapClientE (DB.lookupService pid sid) >>= maybeServiceNotFound -- Disable the service wrapClientE $ DB.updateServiceConn pid sid Nothing Nothing Nothing (Just False) @@ -496,7 +563,8 @@ finishDeleteService pid sid = do kick (bid, cid, _) = deleteBot (botUserId bid) Nothing bid cid deleteAccount :: - ( Member GalleyAPIAccess r + ( Member GalleyAPIAccess r, + Member AuthenticationSubsystem r ) => ProviderId -> Public.DeleteProvider -> @@ -504,10 +572,9 @@ deleteAccount :: deleteAccount pid del = do guardSecondFactorDisabled Nothing prov <- wrapClientE (DB.lookupAccount pid) >>= maybeInvalidProvider - pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials - -- We don't care about pwd status when deleting things - unless (verifyPassword (deleteProviderPassword del) pass) $ - throwStd (errorToWai @'E.BadCredentials) + -- We don't care about pwd update status (scrypt, argon2id etc) when deleting things + unlessM (fst <$> (lift . liftSem $ Authentication.verifyProviderPassword pid del.deleteProviderPassword)) do + throwStd (errorToWai @E.BadCredentials) svcs <- wrapClientE $ DB.listServices pid forM_ svcs $ \svc -> do let sid = serviceId svc @@ -545,11 +612,12 @@ searchServiceProfiles _ Nothing (Just start) mSize = do guardSecondFactorDisabled Nothing prefix :: Range 1 128 Text <- rangeChecked start let size = fromMaybe (unsafeRange 20) mSize - wrapClientE . DB.paginateServiceNames (Just prefix) (fromRange size) . setProviderSearchFilter =<< view settings + wrapClientE . DB.paginateServiceNames (Just prefix) (fromRange size) =<< asks (.settings.providerSearchFilter) searchServiceProfiles _ (Just tags) start mSize = do guardSecondFactorDisabled Nothing let size = fromMaybe (unsafeRange 20) mSize - (wrapClientE . DB.paginateServiceTags tags start (fromRange size)) . setProviderSearchFilter =<< view settings + (wrapClientE . DB.paginateServiceTags tags start (fromRange size)) + =<< asks (.settings.providerSearchFilter) searchServiceProfiles _ Nothing Nothing _ = do guardSecondFactorDisabled Nothing throwStd $ badRequest "At least `tags` or `start` must be provided." @@ -581,14 +649,23 @@ getServiceTagList _ = do where allTags = [(minBound :: Public.ServiceTag) ..] -updateServiceWhitelist :: (Member GalleyAPIAccess r) => UserId -> ConnId -> TeamId -> Public.UpdateServiceWhitelist -> (Handler r) UpdateServiceWhitelistResp +updateServiceWhitelist :: + ( Member GalleyAPIAccess r, + Member (Error UserSubsystemError) r + ) => + UserId -> + ConnId -> + TeamId -> + Public.UpdateServiceWhitelist -> + (Handler r) UpdateServiceWhitelistResp updateServiceWhitelist uid con tid upd = do + -- Preconditions guardSecondFactorDisabled (Just uid) + guardMLSNotDefault let pid = updateServiceWhitelistProvider upd sid = updateServiceWhitelistService upd newWhitelisted = updateServiceWhitelistStatus upd - -- Preconditions - ensurePermissions uid tid (Set.toList serviceWhitelistPermissions) + lift . liftSem $ ensurePermissions uid tid (Set.toList serviceWhitelistPermissions) _ <- wrapClientE (DB.lookupService pid sid) >>= maybeServiceNotFound -- Add to various tables whitelisted <- wrapClientE $ DB.getServiceWhitelistStatus tid pid sid @@ -614,11 +691,27 @@ updateServiceWhitelist uid con tid upd = do ) wrapClientE $ DB.deleteServiceWhitelist (Just tid) pid sid pure UpdateServiceWhitelistRespChanged + where + guardMLSNotDefault = lift . liftSem $ do + feat <- GalleyAPIAccess.getFeatureConfigForTeam @_ @Feature.MLSConfig tid + let defProtocol = feat.config.mlsDefaultProtocol + case defProtocol of + ProtocolProteusTag -> pure () + ProtocolMLSTag -> throw UserSubsystemMLSServicesNotAllowed + ProtocolMixedTag -> throw UserSubsystemMLSServicesNotAllowed -------------------------------------------------------------------------------- -- Bot API -addBot :: (Member GalleyAPIAccess r) => UserId -> ConnId -> ConvId -> Public.AddBot -> (Handler r) Public.AddBotResponse +addBot :: + ( Member GalleyAPIAccess r, + Member AuthenticationSubsystem r + ) => + UserId -> + ConnId -> + ConvId -> + Public.AddBot -> + (Handler r) Public.AddBotResponse addBot zuid zcon cid add = do guardSecondFactorDisabled (Just zuid) zusr <- lift (wrapClient $ User.lookupUser NoPendingInvitations zuid) >>= maybeInvalidUser @@ -638,7 +731,7 @@ addBot zuid zcon cid add = do let mems = cnvMembers cnv unless (cnvType cnv == RegularConv) $ throwStd (errorToWai @'E.InvalidConversation) - maxSize <- fromIntegral . setMaxConvSize <$> view settings + maxSize <- fromIntegral <$> asks (.settings.maxConvSize) unless (length (cmOthers mems) < maxSize - 1) $ throwStd (errorToWai @'E.TooManyConversationMembers) -- For team conversations: bots are not allowed in @@ -669,25 +762,33 @@ addBot zuid zcon cid add = do let botReq = NewBotRequest bid bcl busr bcnv btk bloc rs <- RPC.createBot scon botReq !>> StdError . serviceError -- Insert the bot user and client - locale <- Opt.setDefaultUserLocale <$> view settings + locale <- Opt.defaultUserLocale <$> asks (.settings) let name = fromMaybe (serviceProfileName svp) (Ext.rsNewBotName rs) let assets = fromMaybe (serviceProfileAssets svp) (Ext.rsNewBotAssets rs) let colour = fromMaybe defaultAccentId (Ext.rsNewBotColour rs) let pict = Pict [] -- Legacy let sref = newServiceRef sid pid - let usr = User (Qualified (botUserId bid) domain) Nothing name pict assets colour False locale (Just sref) Nothing Nothing Nothing ManagedByWire defSupportedProtocols + let usr = User (Qualified (botUserId bid) domain) Nothing Nothing name Nothing pict assets colour Active locale (Just sref) Nothing Nothing Nothing ManagedByWire defSupportedProtocols let newClt = (newClient PermanentClientType (Ext.rsNewBotLastPrekey rs)) { newClientPrekeys = Ext.rsNewBotPrekeys rs } - lift $ wrapClient $ User.insertAccount (UserAccount usr Active) (Just (cid, cnvTeam cnv)) Nothing True - maxPermClients <- fromMaybe Opt.defUserMaxPermClients . Opt.setUserMaxPermClients <$> view settings + lift $ wrapClient $ User.insertAccount usr (Just (cid, cnvTeam cnv)) Nothing True + maxPermClients <- fromMaybe Opt.defUserMaxPermClients <$> asks (.settings.userMaxPermClients) (clt, _, _) <- do _ <- do -- if we want to protect bots against lh, 'addClient' cannot just send lh capability -- implicitly in the next line. pure $ FutureWork @'UnprotectedBot undefined - wrapClientE (User.addClient (botUserId bid) bcl newClt maxPermClients (Just $ Set.singleton Public.ClientSupportsLegalholdImplicitConsent)) + lbid <- qualifyLocal (botUserId bid) + ( User.addClient + lbid + bcl + newClt + maxPermClients + ( Just $ ClientCapabilityList $ Set.singleton Public.ClientSupportsLegalholdImplicitConsent + ) + ) !>> const (StdError $ badGatewayWith "MalformedPrekeys") -- Add the bot to the conversation @@ -768,7 +869,7 @@ botClaimUsersPrekeys :: Handler r Public.UserClientPrekeyMap botClaimUsersPrekeys _ body = do guardSecondFactorDisabled Nothing - maxSize <- fromIntegral . setMaxConvSize <$> view settings + maxSize <- fromIntegral <$> asks (.settings.maxConvSize) when (Map.size (Public.userClients body) > maxSize) $ throwStd (errorToWai @'E.TooManyClients) Client.claimLocalMultiPrekeyBundles UnprotectedBot body !>> clientError @@ -804,13 +905,17 @@ guardSecondFactorDisabled :: Maybe UserId -> ExceptT HttpError (AppT r) () guardSecondFactorDisabled mbUserId = do - enabled <- lift $ liftSem $ (==) Feature.FeatureStatusEnabled . Feature.wsStatus . Feature.afcSndFactorPasswordChallenge <$> GalleyAPIAccess.getAllFeatureConfigsForUser mbUserId - when enabled $ (throwStd (errorToWai @'E.AccessDenied)) + feat <- lift $ liftSem $ GalleyAPIAccess.getAllTeamFeaturesForUser mbUserId + let enabled = + (Feature.npProject @Feature.SndFactorPasswordChallengeConfig feat).status + == Feature.FeatureStatusEnabled + when enabled do + throwStd $ errorToWai @'E.AccessDenied minRsaKeySize :: Int minRsaKeySize = 256 -- Bytes (= 2048 bits) -activate :: ProviderId -> Maybe Public.Email -> Public.Email -> (Handler r) () +activate :: ProviderId -> Maybe EmailAddress -> EmailAddress -> (Handler r) () activate pid old new = do let emailKey = mkEmailKey new taken <- maybe False (/= pid) <$> wrapClientE (DB.lookupKey emailKey) @@ -822,6 +927,7 @@ deleteBot :: ( MonadHttp m, MonadReader Env m, MonadMask m, + MonadUnliftIO m, HasRequestId m, MonadLogger m, MonadClient m diff --git a/services/brig/src/Brig/Provider/DB.hs b/services/brig/src/Brig/Provider/DB.hs index 98d237c9565..60a26fb0063 100644 --- a/services/brig/src/Brig/Provider/DB.hs +++ b/services/brig/src/Brig/Provider/DB.hs @@ -29,7 +29,7 @@ import Data.Set qualified as Set import Data.Text qualified as Text import Imports import UnliftIO (mapConcurrently) -import Wire.API.Password +import Wire.API.Password as Password import Wire.API.Provider import Wire.API.Provider.Service hiding (updateServiceTags) import Wire.API.Provider.Service.Tag @@ -81,10 +81,10 @@ updateAccountProfile p name url descr = retry x5 . batch $ do lookupAccountData :: (MonadClient m) => ProviderId -> - m (Maybe (Name, Maybe Email, HttpsUrl, Text)) + m (Maybe (Name, Maybe EmailAddress, HttpsUrl, Text)) lookupAccountData p = retry x1 $ query1 cql $ params LocalQuorum (Identity p) where - cql :: PrepQuery R (Identity ProviderId) (Name, Maybe Email, HttpsUrl, Text) + cql :: PrepQuery R (Identity ProviderId) (Name, Maybe EmailAddress, HttpsUrl, Text) cql = "SELECT name, email, url, descr FROM provider WHERE id = ?" lookupAccount :: @@ -93,7 +93,7 @@ lookupAccount :: m (Maybe Provider) lookupAccount p = (>>= mk) <$> lookupAccountData p where - mk :: (Name, Maybe Email, HttpsUrl, Text) -> Maybe Provider + mk :: (Name, Maybe EmailAddress, HttpsUrl, Text) -> Maybe Provider mk (_, Nothing, _, _) = Nothing mk (n, Just e, u, d) = Just $! Provider p n e u d @@ -103,19 +103,6 @@ lookupAccountProfile :: m (Maybe ProviderProfile) lookupAccountProfile p = fmap ProviderProfile <$> lookupAccount p -lookupPassword :: - (MonadClient m) => - ProviderId -> - m (Maybe Password) -lookupPassword p = - fmap (fmap runIdentity) $ - retry x1 $ - query1 cql $ - params LocalQuorum (Identity p) - where - cql :: PrepQuery R (Identity ProviderId) (Identity Password) - cql = "SELECT password FROM provider WHERE id = ?" - deleteAccount :: (MonadClient m) => ProviderId -> @@ -128,14 +115,13 @@ deleteAccount pid = retry x5 $ write cql $ params LocalQuorum (Identity pid) updateAccountPassword :: (MonadClient m) => ProviderId -> - PlainTextPassword6 -> + Password -> m () updateAccountPassword pid pwd = do - p <- liftIO $ mkSafePasswordScrypt pwd - retry x5 $ write cql $ params LocalQuorum (p, pid) + retry x5 $ write cql $ params LocalQuorum (pwd, pid) where cql :: PrepQuery W (Password, ProviderId) () - cql = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE provider SET password = ? where id = ?" + cql = "UPDATE provider SET password = ? where id = ?" -------------------------------------------------------------------------------- -- Unique (Natural) Keys @@ -159,7 +145,7 @@ insertKey p old new = retry x5 . batch $ do cqlKeyDelete :: PrepQuery W (Identity Text) () cqlKeyDelete = "DELETE FROM provider_keys WHERE key = ?" - cqlEmail :: PrepQuery W (Email, ProviderId) () + cqlEmail :: PrepQuery W (EmailAddress, ProviderId) () cqlEmail = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE provider SET email = ? WHERE id = ?" lookupKey :: diff --git a/services/brig/src/Brig/Provider/Email.hs b/services/brig/src/Brig/Provider/Email.hs index 1b8f329c240..b5c168d4e33 100644 --- a/services/brig/src/Brig/Provider/Email.hs +++ b/services/brig/src/Brig/Provider/Email.hs @@ -19,7 +19,6 @@ module Brig.Provider.Email ( sendActivationMail, - sendApprovalRequestMail, sendApprovalConfirmMail, sendPasswordResetMail, ) @@ -27,18 +26,14 @@ where import Brig.App import Brig.Provider.Template -import Control.Lens (view) -import Data.ByteString.Conversion import Data.Code qualified as Code import Data.Range import Data.Text (pack) import Data.Text.Ascii qualified as Ascii -import Data.Text.Encoding qualified as Text import Data.Text.Lazy qualified as LT import Imports import Network.Mail.Mime import Polysemy -import Wire.API.Provider import Wire.API.User import Wire.EmailSending import Wire.EmailSubsystem.Interpreter (mkMimeAddress) @@ -47,10 +42,10 @@ import Wire.EmailSubsystem.Template (TemplateBranding, renderHtmlWithBranding, r ------------------------------------------------------------------------------- -- Activation Email -sendActivationMail :: (Member EmailSending r) => Name -> Email -> Code.Key -> Code.Value -> Bool -> (AppT r) () +sendActivationMail :: (Member EmailSending r) => Name -> EmailAddress -> Code.Key -> Code.Value -> Bool -> (AppT r) () sendActivationMail name email key code update = do - tpl <- selectTemplate update . snd <$> providerTemplates Nothing - branding <- view templateBranding + tpl <- selectTemplate update . snd <$> providerTemplatesWithLocale Nothing + branding <- asks (.templateBranding) let mail = ActivationEmail email name key code liftSem $ sendMail $ renderActivationMail mail tpl branding where @@ -58,7 +53,7 @@ sendActivationMail name email key code update = do selectTemplate False = activationEmail data ActivationEmail = ActivationEmail - { acmTo :: !Email, + { acmTo :: !EmailAddress, acmName :: !Name, acmKey :: !Code.Key, acmCode :: !Code.Value @@ -96,69 +91,18 @@ renderActivationUrl t (Code.Key k) (Code.Value v) branding = replace "code" = Ascii.toText (fromRange v) replace x = x --------------------------------------------------------------------------------- --- Approval Request Email - -sendApprovalRequestMail :: (Member EmailSending r) => Name -> Email -> HttpsUrl -> Text -> Code.Key -> Code.Value -> (AppT r) () -sendApprovalRequestMail name email url descr key val = do - tpl <- approvalRequestEmail . snd <$> providerTemplates Nothing - branding <- view templateBranding - let mail = ApprovalRequestEmail email name url descr key val - liftSem $ sendMail $ renderApprovalRequestMail mail tpl branding - -data ApprovalRequestEmail = ApprovalRequestEmail - { aprTo :: !Email, - aprName :: !Name, - aprUrl :: !HttpsUrl, - aprDescr :: !Text, - aprKey :: !Code.Key, - aprCode :: !Code.Value - } - -renderApprovalRequestMail :: ApprovalRequestEmail -> ApprovalRequestEmailTemplate -> TemplateBranding -> Mail -renderApprovalRequestMail ApprovalRequestEmail {..} ApprovalRequestEmailTemplate {..} branding = - (emptyMail from) - { mailTo = [to], - mailHeaders = - [ ("Subject", LT.toStrict subj), - ("X-Zeta-Purpose", "ProviderApprovalRequest") - ], - mailParts = [[plainPart txt, htmlPart html]] - } - where - from = Address (Just approvalRequestEmailSenderName) (fromEmail approvalRequestEmailSender) - to = Address (Just "Provider Approval Staff") (fromEmail approvalRequestEmailTo) - txt = renderTextWithBranding approvalRequestEmailBodyText replace branding - html = renderHtmlWithBranding approvalRequestEmailBodyHtml replace branding - subj = renderTextWithBranding approvalRequestEmailSubject replace branding - replace "email" = fromEmail aprTo - replace "name" = fromName aprName - replace "url" = Text.decodeUtf8 (toByteString' aprUrl) - replace "description" = aprDescr - replace "approvalUrl" = renderApprovalUrl approvalRequestEmailUrl aprKey aprCode branding - replace x = x - --- TODO: Unify with renderActivationUrl -renderApprovalUrl :: Template -> Code.Key -> Code.Value -> TemplateBranding -> Text -renderApprovalUrl t (Code.Key k) (Code.Value v) branding = - LT.toStrict $ renderTextWithBranding t replace branding - where - replace "key" = Ascii.toText (fromRange k) - replace "code" = Ascii.toText (fromRange v) - replace x = x - -------------------------------------------------------------------------------- -- Approval Confirmation Email -sendApprovalConfirmMail :: (Member EmailSending r) => Name -> Email -> (AppT r) () +sendApprovalConfirmMail :: (Member EmailSending r) => Name -> EmailAddress -> (AppT r) () sendApprovalConfirmMail name email = do - tpl <- approvalConfirmEmail . snd <$> providerTemplates Nothing - branding <- view templateBranding + tpl <- approvalConfirmEmail . snd <$> providerTemplatesWithLocale Nothing + branding <- asks (.templateBranding) let mail = ApprovalConfirmEmail email name liftSem $ sendMail $ renderApprovalConfirmMail mail tpl branding data ApprovalConfirmEmail = ApprovalConfirmEmail - { apcTo :: !Email, + { apcTo :: !EmailAddress, apcName :: !Name } @@ -186,15 +130,15 @@ renderApprovalConfirmMail ApprovalConfirmEmail {..} ApprovalConfirmEmailTemplate -------------------------------------------------------------------------------- -- Password Reset Email -sendPasswordResetMail :: (Member EmailSending r) => Email -> Code.Key -> Code.Value -> (AppT r) () +sendPasswordResetMail :: (Member EmailSending r) => EmailAddress -> Code.Key -> Code.Value -> (AppT r) () sendPasswordResetMail to key code = do - tpl <- passwordResetEmail . snd <$> providerTemplates Nothing - branding <- view templateBranding + tpl <- passwordResetEmail . snd <$> providerTemplatesWithLocale Nothing + branding <- asks (.templateBranding) let mail = PasswordResetEmail to key code liftSem $ sendMail $ renderPwResetMail mail tpl branding data PasswordResetEmail = PasswordResetEmail - { pwrTo :: !Email, + { pwrTo :: !EmailAddress, pwrKey :: !Code.Key, pwrCode :: !Code.Value } diff --git a/services/brig/src/Brig/Provider/RPC.hs b/services/brig/src/Brig/Provider/RPC.hs index f8abba06133..7c1e710f9cb 100644 --- a/services/brig/src/Brig/Provider/RPC.hs +++ b/services/brig/src/Brig/Provider/RPC.hs @@ -35,7 +35,7 @@ import Brig.App import Brig.Provider.DB (ServiceConn (..)) import Brig.RPC import Control.Error -import Control.Lens (set, view, (^.)) +import Control.Lens (set, (^.)) import Control.Monad.Catch import Control.Retry (recovering) import Data.Aeson @@ -74,7 +74,7 @@ data ServiceError createBot :: ServiceConn -> NewBotRequest -> ExceptT ServiceError (AppT r) NewBotResponse createBot scon new = do let fprs = toList (sconFingerprints scon) - (man, verifyFingerprints) <- view extGetManager + (man, verifyFingerprints) <- asks (.extGetManager) extHandleAll onExc $ do rs <- lift $ wrapHttp $ @@ -164,7 +164,7 @@ setServiceConn scon = do -- | Remove service connection information from galley. removeServiceConn :: ( MonadReader Env m, - MonadIO m, + MonadUnliftIO m, MonadMask m, MonadHttp m, HasRequestId m, @@ -220,7 +220,7 @@ addBotMember zusr zcon conv bot clt pid sid = do removeBotMember :: ( MonadHttp m, MonadReader Env m, - MonadIO m, + MonadUnliftIO m, MonadMask m, HasRequestId m, MonadLogger m diff --git a/services/brig/src/Brig/Provider/Template.hs b/services/brig/src/Brig/Provider/Template.hs index 951ff9add7e..2c50f22fcd6 100644 --- a/services/brig/src/Brig/Provider/Template.hs +++ b/services/brig/src/Brig/Provider/Template.hs @@ -51,16 +51,16 @@ data ApprovalRequestEmailTemplate = ApprovalRequestEmailTemplate approvalRequestEmailSubject :: !Template, approvalRequestEmailBodyText :: !Template, approvalRequestEmailBodyHtml :: !Template, - approvalRequestEmailSender :: !Email, + approvalRequestEmailSender :: !EmailAddress, approvalRequestEmailSenderName :: !Text, - approvalRequestEmailTo :: !Email + approvalRequestEmailTo :: !EmailAddress } data ApprovalConfirmEmailTemplate = ApprovalConfirmEmailTemplate { approvalConfirmEmailSubject :: !Template, approvalConfirmEmailBodyText :: !Template, approvalConfirmEmailBodyHtml :: !Template, - approvalConfirmEmailSender :: !Email, + approvalConfirmEmailSender :: !EmailAddress, approvalConfirmEmailSenderName :: !Text, approvalConfirmEmailHomeUrl :: !HttpsUrl } @@ -115,13 +115,13 @@ loadProviderTemplates o = readLocalesDir defLocale (templateDir gOptions) "provi <*> readText fp "email/sender.txt" ) where - maybeUrl = fromByteString $ encodeUtf8 $ homeUrl pOptions - gOptions = general $ emailSMS o - pOptions = provider $ emailSMS o - defLocale = setDefaultTemplateLocale (optSettings o) - readTemplate = readTemplateWithDefault (templateDir gOptions) defLocale "provider" - readText = readTextWithDefault (templateDir gOptions) defLocale "provider" + maybeUrl = fromByteString . encodeUtf8 $ pOptions.homeUrl + gOptions = o.emailSMS.general + pOptions = o.emailSMS.provider + defLocale = defaultTemplateLocale o.settings + readTemplate = readTemplateWithDefault gOptions.templateDir defLocale "provider" + readText = readTextWithDefault gOptions.templateDir defLocale "provider" -- URL templates - activationUrl' = template $ providerActivationUrl pOptions - approvalUrl' = template $ approvalUrl pOptions - pwResetUrl' = template $ providerPwResetUrl pOptions + activationUrl' = template pOptions.providerActivationUrl + approvalUrl' = template pOptions.approvalUrl + pwResetUrl' = template pOptions.providerPwResetUrl diff --git a/services/brig/src/Brig/Queue.hs b/services/brig/src/Brig/Queue.hs index a5e7d4d275b..3772b57fc08 100644 --- a/services/brig/src/Brig/Queue.hs +++ b/services/brig/src/Brig/Queue.hs @@ -18,68 +18,19 @@ -- | Working with remote queues (like Amazon SQS). module Brig.Queue ( module Brig.Queue.Types, - enqueue, listen, ) where -import Amazonka.SQS.Lens (sendMessageResponse_mD5OfMessageBody) import Brig.AWS qualified as AWS -import Brig.App import Brig.DeleteQueue.Interpreter (QueueEnv (..)) import Brig.Queue.Stomp qualified as Stomp import Brig.Queue.Types -import Control.Exception (ErrorCall (..)) -import Control.Lens (view, (^.)) import Control.Monad.Catch import Data.Aeson -import Data.ByteString.Base16 qualified as B16 -import Data.ByteString.Lazy qualified as BL -import Data.Text.Encoding qualified as T import Imports -import OpenSSL.EVP.Digest (Digest, digestLBS) import System.Logger.Class as Log hiding (settings) --- Note [queue refactoring] --- ~~~~~~~~~~~~~~~~ --- --- The way we deal with queues is not the best. There is at least one piece of --- technical debt here: --- --- 1. 'Queue' is currently used only for the internal events queue, even --- though we have queues in other places (and not only in Brig). We --- should move 'Brig.Queue' out of Brig and use it elsewhere too. - --- | Enqueue a message. --- --- Throws an error in case of failure. -enqueue :: - ( MonadReader Env m, - ToJSON a, - MonadIO m, - MonadLogger m, - MonadThrow m - ) => - QueueEnv -> - a -> - m () -enqueue (StompQueueEnv env queue) message = - Stomp.enqueue env queue message -enqueue (SqsQueueEnv env _ queue) message = do - let body = encode message - bodyMD5 <- digest <$> view digestMD5 <*> pure body - resp <- AWS.execute env (AWS.enqueueStandard queue body) - unless (resp ^. sendMessageResponse_mD5OfMessageBody == Just bodyMD5) $ do - Log.err $ - msg (val "Returned hash (MD5) doesn't match message hash") - . field "SqsQueue" (show queue) - . field "returned_hash" (show (resp ^. sendMessageResponse_mD5OfMessageBody)) - . field "message_hash" (show (Just bodyMD5)) - throwM (ErrorCall "The server couldn't access a queue") - where - digest :: Digest -> BL.ByteString -> Text - digest d = T.decodeLatin1 . B16.encode . digestLBS d - -- | Forever listen to messages coming from a queue and execute a callback -- for each incoming message. -- diff --git a/services/brig/src/Brig/Queue/Stomp.hs b/services/brig/src/Brig/Queue/Stomp.hs index d6d8c3abfca..631f790013a 100644 --- a/services/brig/src/Brig/Queue/Stomp.hs +++ b/services/brig/src/Brig/Queue/Stomp.hs @@ -76,10 +76,10 @@ mkEnv o cred = Env { broker = Broker - { host = Opts.stompHost o, - port = Opts.stompPort o, + { host = o.host, + port = o.port, auth = Just cred, - tls = Opts.stompTls o + tls = o.tls } } diff --git a/services/brig/src/Brig/RPC.hs b/services/brig/src/Brig/RPC.hs index c421ad468d2..bd95ce10263 100644 --- a/services/brig/src/Brig/RPC.hs +++ b/services/brig/src/Brig/RPC.hs @@ -22,7 +22,6 @@ import Bilge import Bilge.RPC import Bilge.Retry import Brig.App -import Control.Lens import Control.Monad.Catch import Control.Retry import Data.Aeson @@ -41,32 +40,32 @@ decodeBody :: (Typeable a, FromJSON a, MonadThrow m) => Text -> Response (Maybe decodeBody ctx = responseJsonThrow (ParseException ctx) cargoholdRequest :: - (MonadReader Env m, MonadIO m, MonadMask m, MonadHttp m, HasRequestId m) => + (MonadReader Env m, MonadUnliftIO m, MonadMask m, MonadHttp m, HasRequestId m) => StdMethod -> (Request -> Request) -> m (Response (Maybe BL.ByteString)) cargoholdRequest = serviceRequest "cargohold" cargohold galleyRequest :: - (MonadReader Env m, MonadIO m, MonadMask m, MonadHttp m, HasRequestId m) => + (MonadReader Env m, MonadUnliftIO m, MonadMask m, MonadHttp m, HasRequestId m) => StdMethod -> (Request -> Request) -> m (Response (Maybe BL.ByteString)) galleyRequest = serviceRequest "galley" galley serviceRequest :: - (MonadReader Env m, MonadIO m, MonadMask m, MonadHttp m, HasRequestId m) => + (MonadReader Env m, MonadUnliftIO m, MonadMask m, MonadHttp m, HasRequestId m) => LT.Text -> - Control.Lens.Getting Request Env Request -> + (Env -> Request) -> StdMethod -> (Request -> Request) -> m (Response (Maybe BL.ByteString)) serviceRequest nm svc m r = do - service <- view svc + service <- asks svc serviceRequestImpl nm service m r serviceRequestImpl :: - (MonadIO m, MonadMask m, MonadHttp m, HasRequestId m) => + (MonadUnliftIO m, MonadMask m, MonadHttp m, HasRequestId m) => LT.Text -> Request -> StdMethod -> diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index 5f713dd5edb..2bd43d585e1 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -15,11 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Brig.Run - ( run, - mkApp, - ) -where +module Brig.Run (run, mkApp) where import AWS.Util (readAuthExpiration) import Brig.API.Federation @@ -27,7 +23,6 @@ import Brig.API.Handler import Brig.API.Internal qualified as IAPI import Brig.API.Public import Brig.API.User qualified as API -import Brig.AWS (amazonkaEnv, sesQueue) import Brig.AWS qualified as AWS import Brig.AWS.SesNotification qualified as SesNotification import Brig.App @@ -41,7 +36,7 @@ import Brig.Queue qualified as Queue import Brig.Version import Control.Concurrent.Async qualified as Async import Control.Exception.Safe (catchAny) -import Control.Lens (view, (.~), (^.)) +import Control.Lens ((.~)) import Control.Monad.Catch (MonadCatch, finally) import Control.Monad.Random (randomRIO) import Data.Aeson qualified as Aeson @@ -59,12 +54,15 @@ import Network.Wai.Middleware.Gzip qualified as GZip import Network.Wai.Utilities.Request import Network.Wai.Utilities.Server import Network.Wai.Utilities.Server qualified as Server +import OpenTelemetry.Instrumentation.Wai qualified as Otel +import OpenTelemetry.Trace as Otel import Polysemy (Member) import Servant (Context ((:.)), (:<|>) (..)) import Servant qualified import System.Logger (msg, val, (.=), (~~)) import System.Logger.Class (MonadLogger, err) import Util.Options +import Util.Timeout import Wire.API.Routes.API import Wire.API.Routes.Internal.Brig qualified as IAPI import Wire.API.Routes.Public.Brig @@ -72,6 +70,7 @@ import Wire.API.Routes.Version import Wire.API.Routes.Version.Wai import Wire.API.User (AccountStatus (PendingInvitation)) import Wire.DeleteQueue +import Wire.OpenTelemetry (withTracer) import Wire.Sem.Paging qualified as P import Wire.UserStore @@ -80,71 +79,85 @@ import Wire.UserStore -- thread terminates for any reason. -- https://github.com/zinfra/backend-issues/issues/1647 run :: Opts -> IO () -run o = do - (app, e) <- mkApp o +run opts = withTracer \tracer -> do + (app, e) <- mkApp opts s <- Server.newSettings (server e) internalEventListener <- Async.async $ runBrigToIO e $ wrapHttpClient $ - Queue.listen (e ^. internalEvents) $ + Queue.listen e.internalEvents $ liftIO . runBrigToIO e . liftSem . Internal.onEvent - let throttleMillis = fromMaybe defSqsThrottleMillis $ setSqsThrottleMillis (optSettings o) - emailListener <- for (e ^. awsEnv . sesQueue) $ \q -> + let throttleMillis = fromMaybe defSqsThrottleMillis opts.settings.sqsThrottleMillis + emailListener <- for e.awsEnv._sesQueue $ \q -> Async.async $ - AWS.execute (e ^. awsEnv) $ + AWS.execute e.awsEnv $ AWS.listen throttleMillis q (runBrigToIO e . SesNotification.onEvent) - sftDiscovery <- forM (e ^. sftEnv) $ Async.async . Calling.startSFTServiceDiscovery (e ^. applog) - turnDiscovery <- Calling.startTurnDiscovery (e ^. applog) (e ^. fsWatcher) (e ^. turnEnv) + sftDiscovery <- forM e.sftEnv $ Async.async . Calling.startSFTServiceDiscovery e.appLogger + turnDiscovery <- Calling.startTurnDiscovery e.appLogger e.fsWatcher e.turnEnv authMetrics <- Async.async (runBrigToIO e collectAuthMetrics) pendingActivationCleanupAsync <- Async.async (runBrigToIO e pendingActivationCleanup) - runSettingsWithShutdown s app Nothing `finally` do - mapM_ Async.cancel emailListener - Async.cancel internalEventListener - mapM_ Async.cancel sftDiscovery - Async.cancel pendingActivationCleanupAsync - mapM_ Async.cancel turnDiscovery - Async.cancel authMetrics + inSpan tracer "brig" defaultSpanArguments {kind = Otel.Server} (runSettingsWithShutdown s app Nothing) `finally` do + Async.cancelMany $ + [internalEventListener, pendingActivationCleanupAsync, authMetrics] + <> catMaybes [emailListener, sftDiscovery] + <> turnDiscovery closeEnv e where - endpoint' = brig o - server e = defaultServer (unpack $ endpoint' ^. host) (endpoint' ^. port) (e ^. applog) + brig = opts.brig + server e = defaultServer (unpack $ brig.host) brig.port e.appLogger mkApp :: Opts -> IO (Wai.Application, Env) -mkApp o = do - e <- newEnv o - pure (middleware e $ servantApp e, e) +mkApp opts = do + e <- newEnv opts + otelMiddleware <- Otel.newOpenTelemetryWaiMiddleware + pure (otelMiddleware . middleware e $ servantApp e, e) where middleware :: Env -> Wai.Middleware middleware e = - -- this rewrites the request, so it must be at the top (i.e. applied last) - versionMiddleware (e ^. disabledVersions) + -- these rewrite the request, so they must be at the top (i.e. applied last) + versionMiddleware e.disabledVersions + . internalHandleCompatibilityMiddleware -- this also rewrites the request - . requestIdMiddleware (e ^. applog) defaultRequestIdHeaderName + . requestIdMiddleware e.appLogger defaultRequestIdHeaderName . Metrics.servantPrometheusMiddleware (Proxy @ServantCombinedAPI) . GZip.gunzip . GZip.gzip GZip.def - . catchErrors (e ^. applog) defaultRequestIdHeaderName + . catchErrors e.appLogger defaultRequestIdHeaderName - -- the servant API wraps the one defined using wai-routing servantApp :: Env -> Wai.Application - servantApp e0 req cont = do + servantApp e req cont = do let rid = getRequestId defaultRequestIdHeaderName req - let e = requestId .~ rid $ e0 - let localDomain = view (settings . federationDomain) e + let env = requestIdLens .~ rid $ e + let localDomain = env.settings.federationDomain Servant.serveWithContext (Proxy @ServantCombinedAPI) (customFormatters :. localDomain :. Servant.EmptyContext) ( docsAPI - :<|> hoistServerWithDomain @BrigAPI (toServantHandler e) servantSitemap - :<|> hoistServerWithDomain @IAPI.API (toServantHandler e) IAPI.servantSitemap - :<|> hoistServerWithDomain @FederationAPI (toServantHandler e) federationSitemap - :<|> hoistServerWithDomain @VersionAPI (toServantHandler e) versionAPI + :<|> hoistServerWithDomain @BrigAPI (toServantHandler env) servantSitemap + :<|> hoistServerWithDomain @IAPI.API (toServantHandler env) IAPI.servantSitemap + :<|> hoistServerWithDomain @FederationAPI (toServantHandler env) federationSitemap + :<|> hoistServerWithDomain @VersionAPI (toServantHandler env) versionAPI ) req cont +-- FUTUREWORK: this rewrites /i/users/handles to /i/handles, for backward +-- compatibility with the old endpoint path during deployment. Once the new +-- endpoint has been deployed, this middleware can be removed. +internalHandleCompatibilityMiddleware :: Wai.Middleware +internalHandleCompatibilityMiddleware app req k = + app + ( case Wai.pathInfo req of + ("i" : "users" : "handles" : rest) -> + req + { Wai.pathInfo = ("i" : "handles" : rest) + } + _ -> req + ) + k + type ServantCombinedAPI = ( DocsAPI :<|> BrigAPI @@ -187,7 +200,7 @@ pendingActivationCleanup :: AppT r () pendingActivationCleanup = do safeForever "pendingActivationCleanup" $ do - now <- liftIO =<< view currentTime + now <- liftIO =<< asks (.currentTime) forExpirationsPaged $ \exps -> do uids <- for exps $ \(UserPendingActivation uid expiresAt) -> do @@ -234,7 +247,7 @@ pendingActivationCleanup = do threadDelayRandom :: (AppT r) () threadDelayRandom = do - cleanupTimeout <- fromMaybe (hours 24) . setExpiredUserCleanupTimeout <$> view settings + cleanupTimeout <- fromMaybe (hours 24) <$> asks (.settings.expiredUserCleanupTimeout) let d = realToFrac cleanupTimeout randomSecs :: Int <- liftIO (round <$> randomRIO @Double (0.5 * d, d)) threadDelay (randomSecs * 1_000_000) @@ -244,7 +257,7 @@ pendingActivationCleanup = do collectAuthMetrics :: forall r. AppT r () collectAuthMetrics = do - env <- view (awsEnv . amazonkaEnv) + env <- asks (.awsEnv._amazonkaEnv) liftIO $ forever $ do mbRemaining <- readAuthExpiration env diff --git a/services/brig/src/Brig/Schema/Run.hs b/services/brig/src/Brig/Schema/Run.hs index 049a51e5f5f..173c5b15bc0 100644 --- a/services/brig/src/Brig/Schema/Run.hs +++ b/services/brig/src/Brig/Schema/Run.hs @@ -1,6 +1,6 @@ -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2022 Wire Swiss GmbH +-- Copyright (C) 2024 Wire Swiss GmbH -- -- This program is free software: you can redistribute it and/or modify it under -- the terms of the GNU Affero General Public License as published by the Free @@ -56,6 +56,12 @@ import Brig.Schema.V78_ClientLastActive qualified as V78_ClientLastActive import Brig.Schema.V79_ConnectionRemoteIndex qualified as V79_ConnectionRemoteIndex import Brig.Schema.V80_KeyPackageCiphersuite qualified as V80_KeyPackageCiphersuite import Brig.Schema.V81_AddFederationRemoteTeams qualified as V81_AddFederationRemoteTeams +import Brig.Schema.V82_DropPhoneColumn qualified as V82_DropPhoneColumn +import Brig.Schema.V83_AddTextStatus qualified as V83_AddTextStatus +import Brig.Schema.V84_DropTeamInvitationPhone qualified as V84_DropTeamInvitationPhone +import Brig.Schema.V85_DropUserKeysHashed qualified as V85_DropUserKeysHashed +import Brig.Schema.V86_WriteTimeBumper qualified as V86_WriteTimeBumper +import Brig.Schema.V87_DropInvitationTables qualified as V87_DropInvitationTables import Cassandra.MigrateSchema (migrateSchema) import Cassandra.Schema import Control.Exception (finally) @@ -118,9 +124,13 @@ migrations = V78_ClientLastActive.migration, V79_ConnectionRemoteIndex.migration, V80_KeyPackageCiphersuite.migration, - V81_AddFederationRemoteTeams.migration + V81_AddFederationRemoteTeams.migration, + V82_DropPhoneColumn.migration, + V83_AddTextStatus.migration, + V84_DropTeamInvitationPhone.migration, + V85_DropUserKeysHashed.migration, + V86_WriteTimeBumper.migration, + V87_DropInvitationTables.migration -- FUTUREWORK: undo V41 (searchable flag); we stopped using it in -- https://github.com/wireapp/wire-server/pull/964 - -- - -- FUTUREWORK after July 2023: integrate V_FUTUREWORK here. ] diff --git a/services/brig/src/Brig/Schema/V82_DropPhoneColumn.hs b/services/brig/src/Brig/Schema/V82_DropPhoneColumn.hs new file mode 100644 index 00000000000..dc086e04a60 --- /dev/null +++ b/services/brig/src/Brig/Schema/V82_DropPhoneColumn.hs @@ -0,0 +1,35 @@ +{-# LANGUAGE QuasiQuotes #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Schema.V82_DropPhoneColumn + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 82 "Drop phone column from user table" $ do + schema' + [r| + ALTER TABLE user DROP phone + |] diff --git a/services/brig/src/Brig/Schema/V83_AddTextStatus.hs b/services/brig/src/Brig/Schema/V83_AddTextStatus.hs new file mode 100644 index 00000000000..3b92c2a127e --- /dev/null +++ b/services/brig/src/Brig/Schema/V83_AddTextStatus.hs @@ -0,0 +1,35 @@ +{-# LANGUAGE QuasiQuotes #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Schema.V83_AddTextStatus + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 83 "Add field for textStatus to user table" $ + schema' + [r| ALTER TABLE user ADD ( + text_status text + ) |] diff --git a/services/brig/src/Brig/Schema/V84_DropTeamInvitationPhone.hs b/services/brig/src/Brig/Schema/V84_DropTeamInvitationPhone.hs new file mode 100644 index 00000000000..02158bbb447 --- /dev/null +++ b/services/brig/src/Brig/Schema/V84_DropTeamInvitationPhone.hs @@ -0,0 +1,33 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2024 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Schema.V84_DropTeamInvitationPhone + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 84 "Drop phone column from team_invitation table" $ do + schema' + [r| + ALTER TABLE team_invitation DROP phone + |] diff --git a/services/brig/src/Brig/Schema/V_FUTUREWORK.hs b/services/brig/src/Brig/Schema/V85_DropUserKeysHashed.hs similarity index 82% rename from services/brig/src/Brig/Schema/V_FUTUREWORK.hs rename to services/brig/src/Brig/Schema/V85_DropUserKeysHashed.hs index d4e00c4ec19..22e4879a247 100644 --- a/services/brig/src/Brig/Schema/V_FUTUREWORK.hs +++ b/services/brig/src/Brig/Schema/V85_DropUserKeysHashed.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 Brig.Schema.V_FUTUREWORK +module Brig.Schema.V85_DropUserKeysHashed ( migration, ) where @@ -36,13 +36,9 @@ import Text.RawString.QQ -- backwards-incompatbile schema migration docs in -- https://docs.wire.com/developer/developer/cassandra-interaction.html?highlight=backwards+incompatbile#backwards-incompatible-schema-changes -- --- FUTUREWORK: remove futurework_number and replace its usage by the next matching number after July 2023, rename this module with a version number, and --- integrate it inside Main.hs and App.hs -futureworkNumber :: Int32 -futureworkNumber = undefined migration :: Migration -migration = Migration futureworkNumber "Drop deprecated user_keys_hashed table" $ do +migration = Migration 85 "Drop deprecated user_keys_hashed table" $ do schema' [r| DROP TABLE IF EXISTS user_keys_hash diff --git a/services/brig/src/Brig/Schema/V86_WriteTimeBumper.hs b/services/brig/src/Brig/Schema/V86_WriteTimeBumper.hs new file mode 100644 index 00000000000..e579517ac94 --- /dev/null +++ b/services/brig/src/Brig/Schema/V86_WriteTimeBumper.hs @@ -0,0 +1,36 @@ +{-# LANGUAGE QuasiQuotes #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Schema.V86_WriteTimeBumper + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + -- See 'WriteTimeBumper' for more explanations. + Migration 86 "Add field for keeping track of time of nulling another field" $ + schema' + [r| ALTER TABLE user ADD ( + write_time_bumper int + ) |] diff --git a/services/brig/src/Brig/Schema/V87_DropInvitationTables.hs b/services/brig/src/Brig/Schema/V87_DropInvitationTables.hs new file mode 100644 index 00000000000..d0366ffb113 --- /dev/null +++ b/services/brig/src/Brig/Schema/V87_DropInvitationTables.hs @@ -0,0 +1,35 @@ +{-# LANGUAGE QuasiQuotes #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Schema.V87_DropInvitationTables + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 87 "Drop unused invitation tables" $ do + schema' + [r| DROP TABLE IF EXISTS invitation |] + schema' + [r| DROP TABLE IF EXISTS invitation_info; |] diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 900506d6bd7..e6fcd9f0d43 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -21,47 +21,42 @@ module Brig.Team.API getInvitationCode, suspendTeam, unsuspendTeam, - teamSize, createInvitationViaScim, ) where import Brig.API.Error import Brig.API.Handler -import Brig.API.User (createUserInviteViaScim, fetchUserIdentity) +import Brig.API.User (createUserInviteViaScim) import Brig.API.User qualified as API import Brig.API.Util (logEmail, logInvitationCode) -import Brig.App -import Brig.Effects.BlacklistStore (BlacklistStore) -import Brig.Effects.BlacklistStore qualified as BlacklistStore -import Brig.Effects.ConnectionStore (ConnectionStore) +import Brig.App as App import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) -import Brig.Options (setMaxTeamSize, setTeamInvitationTimeout) -import Brig.Team.DB qualified as DB -import Brig.Team.Email -import Brig.Team.Util (ensurePermissionToAddUser, ensurePermissions) import Brig.Types.Team (TeamSize) -import Brig.User.Search.TeamSize qualified as TeamSize import Control.Lens (view, (^.)) -import Control.Monad.Trans.Except (mapExceptT) -import Data.ByteString.Conversion (toByteString, toByteString') +import Control.Monad.Trans.Except +import Data.ByteString.Conversion (toByteString) import Data.Id import Data.List1 qualified as List1 -import Data.Qualified (Local) +import Data.Qualified import Data.Range +import Data.Text.Ascii +import Data.Text.Encoding (encodeUtf8) import Data.Text.Lazy qualified as LT -import Data.Time.Clock (UTCTime) +import Data.Text.Lazy qualified as Text import Imports hiding (head) -import Network.Wai.Utilities hiding (code, message) +import Network.Wai.Utilities hiding (Error, code, message) import Polysemy -import Polysemy.Input (Input) +import Polysemy.Error +import Polysemy.Input (Input, input) import Polysemy.TinyLog (TinyLog) +import Polysemy.TinyLog qualified as Log import Servant hiding (Handler, JSON, addHeader) -import System.Logger.Class qualified as Log import System.Logger.Message as Log +import URI.ByteString (Absolute, URIRef, laxURIParserOptions, parseURI) import Util.Logging (logFunction, logTeam) import Wire.API.Error -import Wire.API.Error.Brig qualified as E +import Wire.API.Error.Brig import Wire.API.Routes.Internal.Brig (FoundInvitationCode (FoundInvitationCode)) import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Routes.Named @@ -73,127 +68,95 @@ import Wire.API.Team.Member (teamMembers) import Wire.API.Team.Member qualified as Teams import Wire.API.Team.Permission (Perm (AddTeamMember)) import Wire.API.Team.Role -import Wire.API.Team.Role qualified as Public import Wire.API.User hiding (fromEmail) -import Wire.API.User qualified as Public -import Wire.API.User.Identity qualified as Email -import Wire.EmailSending (EmailSending) +import Wire.BlockListStore +import Wire.EmailSubsystem.Template import Wire.Error +import Wire.Events (Events) import Wire.GalleyAPIAccess (GalleyAPIAccess, ShowOrHideInvitationUrl (..)) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess -import Wire.NotificationSubsystem +import Wire.IndexedUserStore (IndexedUserStore, getTeamSize) +import Wire.InvitationStore (InvitationStore (..), PaginatedResult (..), StoredInvitation (..)) +import Wire.InvitationStore qualified as Store import Wire.Sem.Concurrency -import Wire.Sem.Paging.Cassandra (InternalPaging) +import Wire.TeamInvitationSubsystem import Wire.UserKeyStore import Wire.UserSubsystem +import Wire.UserSubsystem.Error servantAPI :: - ( Member BlacklistStore r, - Member GalleyAPIAccess r, - Member UserKeyStore r, + ( Member GalleyAPIAccess r, + Member TeamInvitationSubsystem r, Member UserSubsystem r, - Member EmailSending r + Member Store.InvitationStore r, + Member TinyLog r, + Member (Input TeamTemplates) r, + Member (Input (Local ())) r, + Member (Error UserSubsystemError) r, + Member IndexedUserStore r ) => ServerT TeamsAPI (Handler r) servantAPI = - Named @"send-team-invitation" createInvitationPublicH - :<|> Named @"get-team-invitations" listInvitations - :<|> Named @"get-team-invitation" getInvitation - :<|> Named @"delete-team-invitation" deleteInvitation - :<|> Named @"get-team-invitation-info" getInvitationByCode - :<|> Named @"head-team-invitations" headInvitationByEmail - :<|> Named @"get-team-size" teamSizePublic + Named @"send-team-invitation" (\luid tid invreq -> lift . liftSem $ inviteUser luid tid invreq) + :<|> Named @"get-team-invitations" (\u t inv s -> lift . liftSem $ listInvitations u t inv s) + :<|> Named @"get-team-invitation" (\u t inv -> lift . liftSem $ getInvitation u t inv) + :<|> Named @"delete-team-invitation" (\u t inv -> lift . liftSem $ deleteInvitation u t inv) + :<|> Named @"get-team-invitation-info" (lift . liftSem . getInvitationByCode) + :<|> Named @"head-team-invitations" (lift . liftSem . headInvitationByEmail) + :<|> Named @"get-team-size" (\uid tid -> lift . liftSem $ teamSizePublic uid tid) + :<|> Named @"accept-team-invitation" (\luid req -> lift $ liftSem $ acceptTeamInvitation luid req.password req.code) -teamSizePublic :: (Member GalleyAPIAccess r) => UserId -> TeamId -> (Handler r) TeamSize +teamSizePublic :: + ( Member GalleyAPIAccess r, + Member (Error UserSubsystemError) r, + Member IndexedUserStore r + ) => + UserId -> + TeamId -> + Sem r TeamSize teamSizePublic uid tid = do - ensurePermissions uid tid [AddTeamMember] -- limit this to team admins to reduce risk of involuntary DOS attacks - teamSize tid - -teamSize :: TeamId -> (Handler r) TeamSize -teamSize t = lift $ TeamSize.teamSize t - -getInvitationCode :: TeamId -> InvitationId -> (Handler r) FoundInvitationCode -getInvitationCode t r = do - code <- lift . wrapClient $ DB.lookupInvitationCode t r - maybe (throwStd $ errorToWai @'E.InvalidInvitationCode) (pure . FoundInvitationCode) code + -- limit this to team admins to reduce risk of involuntary DOS attacks + ensurePermissions uid tid [AddTeamMember] + getTeamSize tid -createInvitationPublicH :: - ( Member BlacklistStore r, - Member GalleyAPIAccess r, - Member UserKeyStore r, - Member UserSubsystem r, - Member EmailSending r +getInvitationCode :: + ( Member Store.InvitationStore r, + Member (Error UserSubsystemError) r ) => - UserId -> TeamId -> - Public.InvitationRequest -> - Handler r (Public.Invitation, Public.InvitationLocation) -createInvitationPublicH uid tid body = do - inv <- createInvitationPublic uid tid body - pure (inv, loc inv) - where - loc :: Invitation -> InvitationLocation - loc inv = - InvitationLocation $ "/teams/" <> toByteString' tid <> "/invitations/" <> toByteString' (inInvitation inv) + InvitationId -> + Sem r FoundInvitationCode +getInvitationCode t r = do + inv <- Store.lookupInvitation t r + maybe (throw UserSubsystemInvalidInvitationCode) (pure . FoundInvitationCode . (.code)) inv data CreateInvitationInviter = CreateInvitationInviter { inviterUid :: UserId, - inviterEmail :: Email + inviterEmail :: EmailAddress } deriving (Eq, Show) -createInvitationPublic :: - ( Member BlacklistStore r, - Member GalleyAPIAccess r, - Member UserKeyStore r, - Member UserSubsystem r, - Member EmailSending r - ) => - UserId -> - TeamId -> - Public.InvitationRequest -> - Handler r Public.Invitation -createInvitationPublic uid tid body = do - let inviteeRole = fromMaybe defaultRole . irRole $ body - inviter <- do - let inviteePerms = Teams.rolePermissions inviteeRole - idt <- maybe (throwStd (errorToWai @'E.NoIdentity)) pure =<< lift (fetchUserIdentity uid) - from <- maybe (throwStd (errorToWai @'E.NoEmail)) pure (emailIdentity idt) - ensurePermissionToAddUser uid tid inviteePerms - pure $ CreateInvitationInviter uid from - - let context = - logFunction "Brig.Team.API.createInvitationPublic" - . logTeam tid - . logEmail (irInviteeEmail body) - - fst - <$> logInvitationRequest - context - (createInvitation' tid Nothing inviteeRole (Just (inviterUid inviter)) (inviterEmail inviter) body) - createInvitationViaScim :: - ( Member BlacklistStore r, - Member GalleyAPIAccess r, + ( Member BlockListStore r, Member UserKeyStore r, Member (UserPendingActivationStore p) r, Member TinyLog r, - Member EmailSending r + Member TeamInvitationSubsystem r, + Member (Input (Local ())) r ) => TeamId -> NewUserScimInvitation -> - (Handler r) UserAccount -createInvitationViaScim tid newUser@(NewUserScimInvitation _tid uid loc name email role) = do + Handler r User +createInvitationViaScim tid newUser@(NewUserScimInvitation _tid _uid@(Id (Id -> invId)) _eid loc name email role) = do env <- ask let inviteeRole = role - fromEmail = env ^. emailSender + fromEmail = env.emailSender invreq = InvitationRequest - { irLocale = loc, - irRole = Nothing, -- (unused, it's in the type for 'createInvitationPublicH') - irInviteeName = Just name, - irInviteeEmail = email, - irInviteePhone = Nothing + { locale = loc, + role = Nothing, -- (unused, it's in the type for 'createInvitationV5') + inviteeName = Just name, + inviteeEmail = email } context = @@ -201,152 +164,257 @@ createInvitationViaScim tid newUser@(NewUserScimInvitation _tid uid loc name ema . logTeam tid . logEmail email + localNothing <- lift . liftSem $ qualifyLocal' Nothing void $ logInvitationRequest context $ - createInvitation' tid (Just uid) inviteeRole Nothing fromEmail invreq + lift $ + liftSem $ + internalCreateInvitation tid (Just invId) inviteeRole localNothing fromEmail invreq createUserInviteViaScim newUser -logInvitationRequest :: (Msg -> Msg) -> (Handler r) (Invitation, InvitationCode) -> (Handler r) (Invitation, InvitationCode) +logInvitationRequest :: (Member TinyLog r) => (Msg -> Msg) -> (Handler r) (Invitation, InvitationCode) -> Handler r (Invitation, InvitationCode) logInvitationRequest context action = - flip mapExceptT action $ \action' -> do + flip mapExceptT action \action' -> do eith <- action' case eith of Left err' -> do - Log.warn $ - context - . Log.msg @Text - ( "Failed to create invitation, label: " - <> (LT.toStrict . errorLabel) err' - ) + liftSem $ + Log.warn $ + context + . Log.msg @Text + ( "Failed to create invitation, label: " + <> (LT.toStrict . errorLabel) err' + ) pure (Left err') - Right result@(_, code) -> do + Right result@(_, code) -> liftSem do Log.info $ (context . logInvitationCode code) . Log.msg @Text "Successfully created invitation" pure (Right result) -createInvitation' :: - ( Member BlacklistStore r, - Member GalleyAPIAccess r, - Member UserKeyStore r, - Member EmailSending r +deleteInvitation :: + ( Member GalleyAPIAccess r, + Member InvitationStore r, + Member (Error UserSubsystemError) r ) => + UserId -> TeamId -> - Maybe UserId -> - Public.Role -> - Maybe UserId -> - Email -> - Public.InvitationRequest -> - Handler r (Public.Invitation, Public.InvitationCode) -createInvitation' tid mUid inviteeRole mbInviterUid fromEmail body = do - -- FUTUREWORK: These validations are nearly copy+paste from accountCreation and - -- sendActivationCode. Refactor this to a single place + InvitationId -> + Sem r () +deleteInvitation uid tid iid = do + ensurePermissions uid tid [AddTeamMember] + Store.deleteInvitation tid iid - -- Validate e-mail - inviteeEmail <- either (const $ throwStd (errorToWai @'E.InvalidEmail)) pure (Email.validateEmail (irInviteeEmail body)) - let uke = mkEmailKey inviteeEmail - blacklistedEm <- lift $ liftSem $ BlacklistStore.exists uke - when blacklistedEm $ - throwStd blacklistedEmail - emailTaken <- lift $ liftSem $ isJust <$> lookupKey uke - when emailTaken $ - throwStd emailExists +listInvitations :: + forall r. + ( Member GalleyAPIAccess r, + Member TinyLog r, + Member InvitationStore r, + Member (Input TeamTemplates) r, + Member (Input (Local ())) r, + Member UserSubsystem r, + Member (Error UserSubsystemError) r + ) => + UserId -> + TeamId -> + Maybe InvitationId -> + Maybe (Range 1 500 Int32) -> + Sem r Public.InvitationList +listInvitations uid tid startingId mSize = do + ensurePermissions uid tid [AddTeamMember] + showInvitationUrl <- GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid + let toInvitations is = mapM (toInvitationHack showInvitationUrl) is + Store.lookupInvitationsPaginated mSize tid startingId >>= \case + PaginatedResultHasMore storedInvs -> do + invs <- toInvitations storedInvs + pure $ InvitationList invs True + PaginatedResult storedInvs -> do + invs <- toInvitations storedInvs + pure $ InvitationList invs False + where + -- To create the correct team invitation URL, we need to detect whether the invited account already exists. + -- Optimization: if url is not to be shown, do not check for existing personal user. + toInvitationHack :: ShowOrHideInvitationUrl -> StoredInvitation -> Sem r Invitation + toInvitationHack HideInvitationUrl si = toInvitation False HideInvitationUrl si -- isPersonalUserMigration is always ignored here + toInvitationHack ShowInvitationUrl si = do + isPersonalUserMigration <- isPersonalUser (mkEmailKey si.email) + toInvitation isPersonalUserMigration ShowInvitationUrl si - maxSize <- setMaxTeamSize <$> view settings - pending <- lift $ wrapClient $ DB.countInvitations tid - when (fromIntegral pending >= maxSize) $ - throwStd (errorToWai @'E.TooManyTeamInvitations) +-- | brig used to not store the role, so for migration we allow this to be empty and fill in the +-- default here. +toInvitation :: + ( Member TinyLog r, + Member (Input TeamTemplates) r + ) => + Bool -> + ShowOrHideInvitationUrl -> + StoredInvitation -> + Sem r Invitation +toInvitation isPersonalUserMigration showUrl storedInv = do + url <- + if isPersonalUserMigration + then mkInviteUrlPersonalUser showUrl storedInv.teamId storedInv.code + else mkInviteUrl showUrl storedInv.teamId storedInv.code + pure $ + Invitation + { team = storedInv.teamId, + role = fromMaybe defaultRole storedInv.role, + invitationId = storedInv.invitationId, + createdAt = storedInv.createdAt, + createdBy = storedInv.createdBy, + inviteeEmail = storedInv.email, + inviteeName = storedInv.name, + inviteeUrl = url + } - let locale = irLocale body - let inviteeName = irInviteeName body - showInvitationUrl <- lift $ liftSem $ GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid +getInviteUrl :: + forall r. + (Member TinyLog r) => + InvitationEmailTemplate -> + TeamId -> + AsciiText Base64Url -> + Sem r (Maybe (URIRef Absolute)) +getInviteUrl (invitationEmailUrl -> template) team code = do + let branding = id -- url is not branded + let url = Text.toStrict $ renderTextWithBranding template replace branding + parseHttpsUrl url + where + replace "team" = idToText team + replace "code" = toText code + replace x = x - lift $ do - iid <- maybe (liftIO DB.mkInvitationId) (pure . Id . toUUID) mUid - now <- liftIO =<< view currentTime - timeout <- setTeamInvitationTimeout <$> view settings - (newInv, code) <- - wrapClient $ - DB.insertInvitation - showInvitationUrl - iid - tid - inviteeRole - now - mbInviterUid - inviteeEmail - inviteeName - Nothing -- ignore phone - timeout - (newInv, code) <$ sendInvitationMail inviteeEmail tid fromEmail code locale + parseHttpsUrl :: Text -> Sem r (Maybe (URIRef Absolute)) + parseHttpsUrl url = + either (\e -> Nothing <$ logError url e) (pure . Just) $ + parseURI laxURIParserOptions (encodeUtf8 url) -deleteInvitation :: (Member GalleyAPIAccess r) => UserId -> TeamId -> InvitationId -> (Handler r) () -deleteInvitation uid tid iid = do - ensurePermissions uid tid [AddTeamMember] - lift $ wrapClient $ DB.deleteInvitation tid iid + logError url e = + Log.err $ + Log.msg @Text "Unable to create invitation url. Please check configuration." + . Log.field "url" url + . Log.field "error" (show e) -listInvitations :: (Member GalleyAPIAccess r) => UserId -> TeamId -> Maybe InvitationId -> Maybe (Range 1 500 Int32) -> (Handler r) Public.InvitationList -listInvitations uid tid start mSize = do - ensurePermissions uid tid [AddTeamMember] - showInvitationUrl <- lift $ liftSem $ GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid - rs <- lift $ wrapClient $ DB.lookupInvitations showInvitationUrl tid start (fromMaybe (unsafeRange 100) mSize) - pure $! Public.InvitationList (DB.resultList rs) (DB.resultHasMore rs) +mkInviteUrl :: + ( Member TinyLog r, + Member (Input TeamTemplates) r + ) => + ShowOrHideInvitationUrl -> + TeamId -> + InvitationCode -> + Sem r (Maybe (URIRef Absolute)) +mkInviteUrl HideInvitationUrl _ _ = pure Nothing +mkInviteUrl ShowInvitationUrl team (InvitationCode c) = do + template <- invitationEmail <$> input + getInviteUrl template team c -getInvitation :: (Member GalleyAPIAccess r) => UserId -> TeamId -> InvitationId -> (Handler r) (Maybe Public.Invitation) +mkInviteUrlPersonalUser :: + ( Member TinyLog r, + Member (Input TeamTemplates) r + ) => + ShowOrHideInvitationUrl -> + TeamId -> + InvitationCode -> + Sem r (Maybe (URIRef Absolute)) +mkInviteUrlPersonalUser HideInvitationUrl _ _ = pure Nothing +mkInviteUrlPersonalUser ShowInvitationUrl team (InvitationCode c) = do + template <- existingUserInvitationEmail <$> input + getInviteUrl template team c + +getInvitation :: + ( Member GalleyAPIAccess r, + Member InvitationStore r, + Member TinyLog r, + Member (Input TeamTemplates) r, + Member (Error UserSubsystemError) r + ) => + UserId -> + TeamId -> + InvitationId -> + Sem r (Maybe Public.Invitation) getInvitation uid tid iid = do ensurePermissions uid tid [AddTeamMember] - showInvitationUrl <- lift $ liftSem $ GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid - lift $ wrapClient $ DB.lookupInvitation showInvitationUrl tid iid -getInvitationByCode :: Public.InvitationCode -> (Handler r) Public.Invitation + invitationM <- Store.lookupInvitation tid iid + case invitationM of + Nothing -> pure Nothing + Just invitation -> do + showInvitationUrl <- GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid + maybeUrl <- mkInviteUrl showInvitationUrl tid invitation.code + pure $ Just (Store.invitationFromStored maybeUrl invitation) + +isPersonalUser :: (Member UserSubsystem r, Member (Input (Local ())) r) => EmailKey -> Sem r Bool +isPersonalUser uke = do + mAccount <- getLocalUserAccountByUserKey =<< qualifyLocal' uke + pure $ case mAccount of + -- this can e.g. happen if the key is claimed but the account is not yet created + Nothing -> False + Just account -> account.userStatus == Active && isNothing account.userTeam + +getInvitationByCode :: + ( Member Store.InvitationStore r, + Member (Error UserSubsystemError) r + ) => + InvitationCode -> + Sem r Public.Invitation getInvitationByCode c = do - inv <- lift . wrapClient $ DB.lookupInvitationByCode HideInvitationUrl c - maybe (throwStd $ errorToWai @'E.InvalidInvitationCode) pure inv + inv <- Store.lookupInvitationByCode c + maybe (throw UserSubsystemInvalidInvitationCode) (pure . Store.invitationFromStored Nothing) inv -headInvitationByEmail :: Email -> (Handler r) Public.HeadInvitationByEmailResult -headInvitationByEmail e = do - lift $ - wrapClient $ - DB.lookupInvitationInfoByEmail e <&> \case - DB.InvitationByEmail _ -> Public.InvitationByEmail - DB.InvitationByEmailNotFound -> Public.InvitationByEmailNotFound - DB.InvitationByEmailMoreThanOne -> Public.InvitationByEmailMoreThanOne +headInvitationByEmail :: + (Member InvitationStore r, Member TinyLog r) => + EmailAddress -> + Sem r Public.HeadInvitationByEmailResult +headInvitationByEmail email = + Store.lookupInvitationsByEmail email >>= \case + [] -> pure Public.InvitationByEmailNotFound + [_code] -> pure Public.InvitationByEmail + (_ : _ : _) -> do + Log.info $ + Log.msg (Log.val "team_invitation_email: multiple pending invites from different teams for the same email") + . Log.field "email" (show email) + pure Public.InvitationByEmailMoreThanOne --- | FUTUREWORK: This should also respond with status 409 in case of --- @DB.InvitationByEmailMoreThanOne@. Refactor so that 'headInvitationByEmailH' and --- 'getInvitationByEmailH' are almost the same thing. -getInvitationByEmail :: Email -> (Handler r) Public.Invitation +-- | FUTUREWORK: Refactor so that 'headInvitationByEmail' and +-- 'getInvitationByEmail' are almost the same thing. +getInvitationByEmail :: + (Member Store.InvitationStore r) => + EmailAddress -> + (Handler r) Public.Invitation getInvitationByEmail email = do - inv <- lift $ wrapClient $ DB.lookupInvitationByEmail HideInvitationUrl email - maybe (throwStd (notFound "Invitation not found")) pure inv + inv <- do + invs <- lift . liftSem $ Store.lookupInvitationsByEmail email + case invs of + [] -> pure Nothing + [inv] -> pure . Just $ inv + (_ : _ : _) -> throwStd $ errorToWai @'ConflictingInvitations + maybe (throwStd (notFound "Invitation not found")) (pure . Store.invitationFromStored Nothing) inv suspendTeam :: ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member (Concurrency 'Unsafe) r, Member GalleyAPIAccess r, + Member UserSubsystem r, + Member Events r, Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member InvitationStore r ) => TeamId -> (Handler r) NoContent suspendTeam tid = do - Log.info $ Log.msg (Log.val "Team suspended") ~~ Log.field "team" (toByteString tid) + lift $ liftSem $ Log.info $ Log.msg (Log.val "Team suspended") ~~ Log.field "team" (toByteString tid) + -- Update the status of all users from the given team changeTeamAccountStatuses tid Suspended - lift $ wrapClient $ DB.deleteInvitations tid - lift $ liftSem $ GalleyAPIAccess.changeTeamStatus tid Team.Suspended Nothing + lift . liftSem $ do + Store.deleteAllTeamInvitations tid + -- RPC to galley to change team status there + GalleyAPIAccess.changeTeamStatus tid Team.Suspended Nothing pure NoContent unsuspendTeam :: ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member (Concurrency 'Unsafe) r, Member GalleyAPIAccess r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r ) => TeamId -> (Handler r) NoContent @@ -360,13 +428,10 @@ unsuspendTeam tid = do changeTeamAccountStatuses :: ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member (Concurrency 'Unsafe) r, Member GalleyAPIAccess r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r ) => TeamId -> AccountStatus -> diff --git a/services/brig/src/Brig/Team/DB.hs b/services/brig/src/Brig/Team/DB.hs deleted file mode 100644 index a31875142c1..00000000000 --- a/services/brig/src/Brig/Team/DB.hs +++ /dev/null @@ -1,325 +0,0 @@ -{-# LANGUAGE RecordWildCards #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Brig.Team.DB - ( module T, - countInvitations, - insertInvitation, - deleteInvitation, - deleteInvitations, - lookupInvitation, - lookupInvitationCode, - lookupInvitations, - lookupInvitationByCode, - lookupInvitationInfo, - lookupInvitationInfoByEmail, - lookupInvitationByEmail, - mkInvitationCode, - mkInvitationId, - InvitationByEmail (..), - InvitationInfo (..), - ) -where - -import Brig.App as App -import Brig.Data.Types as T -import Brig.Options -import Brig.Team.Template -import Cassandra as C -import Control.Lens (view) -import Data.Conduit (runConduit, (.|)) -import Data.Conduit.List qualified as C -import Data.Id -import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) -import Data.Range -import Data.Text.Ascii (encodeBase64Url, toText) -import Data.Text.Encoding -import Data.Text.Lazy (toStrict) -import Data.Time.Clock -import Data.UUID.V4 -import Imports -import OpenSSL.Random (randBytes) -import System.Logger.Class qualified as Log -import URI.ByteString -import UnliftIO.Async (pooledMapConcurrentlyN_) -import Wire.API.Team.Invitation hiding (HeadInvitationByEmailResult (..)) -import Wire.API.Team.Role -import Wire.API.User -import Wire.EmailSubsystem.Template (renderTextWithBranding) -import Wire.GalleyAPIAccess (ShowOrHideInvitationUrl (..)) - -mkInvitationCode :: IO InvitationCode -mkInvitationCode = InvitationCode . encodeBase64Url <$> randBytes 24 - -mkInvitationId :: IO InvitationId -mkInvitationId = Id <$> nextRandom - -data InvitationInfo = InvitationInfo - { iiCode :: InvitationCode, - iiTeam :: TeamId, - iiInvId :: InvitationId - } - deriving (Eq, Show) - -data InvitationByEmail - = InvitationByEmail InvitationInfo - | InvitationByEmailNotFound - | InvitationByEmailMoreThanOne - -insertInvitation :: - ( Log.MonadLogger m, - MonadReader Env m, - MonadClient m - ) => - ShowOrHideInvitationUrl -> - InvitationId -> - TeamId -> - Role -> - UTCTime -> - Maybe UserId -> - Email -> - Maybe Name -> - Maybe Phone -> - -- | The timeout for the invitation code. - Timeout -> - m (Invitation, InvitationCode) -insertInvitation showUrl iid t role (toUTCTimeMillis -> now) minviter email inviteeName phone timeout = do - code <- liftIO mkInvitationCode - url <- mkInviteUrl showUrl t code - let inv = Invitation t role iid now minviter email inviteeName phone url - retry x5 . batch $ do - setType BatchLogged - setConsistency LocalQuorum - addPrepQuery cqlInvitation (t, role, iid, code, email, now, minviter, inviteeName, phone, round timeout) - addPrepQuery cqlInvitationInfo (code, t, iid, round timeout) - addPrepQuery cqlInvitationByEmail (email, t, iid, code, round timeout) - pure (inv, code) - where - cqlInvitationInfo :: PrepQuery W (InvitationCode, TeamId, InvitationId, Int32) () - cqlInvitationInfo = "INSERT INTO team_invitation_info (code, team, id) VALUES (?, ?, ?) USING TTL ?" - cqlInvitation :: PrepQuery W (TeamId, Role, InvitationId, InvitationCode, Email, UTCTimeMillis, Maybe UserId, Maybe Name, Maybe Phone, Int32) () - cqlInvitation = "INSERT INTO team_invitation (team, role, id, code, email, created_at, created_by, name, phone) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) USING TTL ?" - -- Note: the edge case of multiple invites to the same team by different admins from the same team results in last-invite-wins in the team_invitation_email table. - cqlInvitationByEmail :: PrepQuery W (Email, TeamId, InvitationId, InvitationCode, Int32) () - cqlInvitationByEmail = "INSERT INTO team_invitation_email (email, team, invitation, code) VALUES (?, ?, ?, ?) USING TTL ?" - -lookupInvitation :: - ( MonadClient m, - MonadReader Env m, - Log.MonadLogger m - ) => - ShowOrHideInvitationUrl -> - TeamId -> - InvitationId -> - m (Maybe Invitation) -lookupInvitation showUrl t r = do - inv <- retry x1 (query1 cqlInvitation (params LocalQuorum (t, r))) - traverse (toInvitation showUrl) inv - where - cqlInvitation :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, Maybe Phone, InvitationCode) - cqlInvitation = "SELECT team, role, id, created_at, created_by, email, name, phone, code FROM team_invitation WHERE team = ? AND id = ?" - -lookupInvitationByCode :: - ( Log.MonadLogger m, - MonadReader Env m, - MonadClient m - ) => - ShowOrHideInvitationUrl -> - InvitationCode -> - m (Maybe Invitation) -lookupInvitationByCode showUrl i = - lookupInvitationInfo i >>= \case - Just InvitationInfo {..} -> lookupInvitation showUrl iiTeam iiInvId - _ -> pure Nothing - -lookupInvitationCode :: (MonadClient m) => TeamId -> InvitationId -> m (Maybe InvitationCode) -lookupInvitationCode t r = - fmap runIdentity - <$> retry x1 (query1 cqlInvitationCode (params LocalQuorum (t, r))) - where - cqlInvitationCode :: PrepQuery R (TeamId, InvitationId) (Identity InvitationCode) - cqlInvitationCode = "SELECT code FROM team_invitation WHERE team = ? AND id = ?" - -lookupInvitationCodeEmail :: (MonadClient m) => TeamId -> InvitationId -> m (Maybe (InvitationCode, Email)) -lookupInvitationCodeEmail t r = retry x1 (query1 cqlInvitationCodeEmail (params LocalQuorum (t, r))) - where - cqlInvitationCodeEmail :: PrepQuery R (TeamId, InvitationId) (InvitationCode, Email) - cqlInvitationCodeEmail = "SELECT code, email FROM team_invitation WHERE team = ? AND id = ?" - -lookupInvitations :: - ( Log.MonadLogger m, - MonadReader Env m, - MonadClient m - ) => - ShowOrHideInvitationUrl -> - TeamId -> - Maybe InvitationId -> - Range 1 500 Int32 -> - m (ResultPage Invitation) -lookupInvitations showUrl team start (fromRange -> size) = do - page <- case start of - Just ref -> retry x1 $ paginate cqlSelectFrom (paramsP LocalQuorum (team, ref) (size + 1)) - Nothing -> retry x1 $ paginate cqlSelect (paramsP LocalQuorum (Identity team) (size + 1)) - toResult (hasMore page) <$> traverse (toInvitation showUrl) (trim page) - where - trim p = take (fromIntegral size) (result p) - toResult more invs = - cassandraResultPage $ - emptyPage - { result = invs, - hasMore = more - } - cqlSelect :: PrepQuery R (Identity TeamId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, Maybe Phone, InvitationCode) - cqlSelect = "SELECT team, role, id, created_at, created_by, email, name, phone, code FROM team_invitation WHERE team = ? ORDER BY id ASC" - cqlSelectFrom :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, Maybe Phone, InvitationCode) - cqlSelectFrom = "SELECT team, role, id, created_at, created_by, email, name, phone, code FROM team_invitation WHERE team = ? AND id > ? ORDER BY id ASC" - -deleteInvitation :: (MonadClient m) => TeamId -> InvitationId -> m () -deleteInvitation t i = do - codeEmail <- lookupInvitationCodeEmail t i - case codeEmail of - Just (invCode, invEmail) -> retry x5 . batch $ do - setType BatchLogged - setConsistency LocalQuorum - addPrepQuery cqlInvitation (t, i) - addPrepQuery cqlInvitationInfo (Identity invCode) - addPrepQuery cqlInvitationEmail (invEmail, t) - Nothing -> - retry x5 $ write cqlInvitation (params LocalQuorum (t, i)) - where - cqlInvitation :: PrepQuery W (TeamId, InvitationId) () - cqlInvitation = "DELETE FROM team_invitation where team = ? AND id = ?" - cqlInvitationInfo :: PrepQuery W (Identity InvitationCode) () - cqlInvitationInfo = "DELETE FROM team_invitation_info WHERE code = ?" - cqlInvitationEmail :: PrepQuery W (Email, TeamId) () - cqlInvitationEmail = "DELETE FROM team_invitation_email WHERE email = ? AND team = ?" - -deleteInvitations :: (MonadClient m) => TeamId -> m () -deleteInvitations t = - liftClient $ - runConduit $ - paginateC cqlSelect (paramsP LocalQuorum (Identity t) 100) x1 - .| C.mapM_ (pooledMapConcurrentlyN_ 16 (deleteInvitation t . runIdentity)) - where - cqlSelect :: PrepQuery R (Identity TeamId) (Identity InvitationId) - cqlSelect = "SELECT id FROM team_invitation WHERE team = ? ORDER BY id ASC" - -lookupInvitationInfo :: (MonadClient m) => InvitationCode -> m (Maybe InvitationInfo) -lookupInvitationInfo ic@(InvitationCode c) - | c == mempty = pure Nothing - | otherwise = - fmap (toInvitationInfo ic) - <$> retry x1 (query1 cqlInvitationInfo (params LocalQuorum (Identity ic))) - where - toInvitationInfo i (t, r) = InvitationInfo i t r - cqlInvitationInfo :: PrepQuery R (Identity InvitationCode) (TeamId, InvitationId) - cqlInvitationInfo = "SELECT team, id FROM team_invitation_info WHERE code = ?" - -lookupInvitationByEmail :: - ( Log.MonadLogger m, - MonadReader Env m, - MonadClient m - ) => - ShowOrHideInvitationUrl -> - Email -> - m (Maybe Invitation) -lookupInvitationByEmail showUrl e = - lookupInvitationInfoByEmail e >>= \case - InvitationByEmail InvitationInfo {..} -> lookupInvitation showUrl iiTeam iiInvId - _ -> pure Nothing - -lookupInvitationInfoByEmail :: (Log.MonadLogger m, MonadClient m) => Email -> m InvitationByEmail -lookupInvitationInfoByEmail email = do - res <- retry x1 (query cqlInvitationEmail (params LocalQuorum (Identity email))) - case res of - [] -> pure InvitationByEmailNotFound - [(tid, invId, code)] -> - -- one invite pending - pure $ InvitationByEmail (InvitationInfo code tid invId) - _ : _ : _ -> do - -- edge case: more than one pending invite from different teams - Log.info $ - Log.msg (Log.val "team_invidation_email: multiple pending invites from different teams for the same email") - Log.~~ Log.field "email" (show email) - pure InvitationByEmailMoreThanOne - where - cqlInvitationEmail :: PrepQuery R (Identity Email) (TeamId, InvitationId, InvitationCode) - cqlInvitationEmail = "SELECT team, invitation, code FROM team_invitation_email WHERE email = ?" - -countInvitations :: (MonadClient m) => TeamId -> m Int64 -countInvitations t = - maybe 0 runIdentity - <$> retry x1 (query1 cqlSelect (params LocalQuorum (Identity t))) - where - cqlSelect :: PrepQuery R (Identity TeamId) (Identity Int64) - cqlSelect = "SELECT count(*) FROM team_invitation WHERE team = ?" - --- | brig used to not store the role, so for migration we allow this to be empty and fill in the --- default here. -toInvitation :: - ( MonadReader Env m, - Log.MonadLogger m - ) => - ShowOrHideInvitationUrl -> - ( TeamId, - Maybe Role, - InvitationId, - UTCTimeMillis, - Maybe UserId, - Email, - Maybe Name, - Maybe Phone, - InvitationCode - ) -> - m Invitation -toInvitation showUrl (t, r, i, tm, minviter, e, inviteeName, p, code) = do - url <- mkInviteUrl showUrl t code - pure $ Invitation t (fromMaybe defaultRole r) i tm minviter e inviteeName p url - -mkInviteUrl :: - ( MonadReader Env m, - Log.MonadLogger m - ) => - ShowOrHideInvitationUrl -> - TeamId -> - InvitationCode -> - m (Maybe (URIRef Absolute)) -mkInviteUrl HideInvitationUrl _ _ = pure Nothing -mkInviteUrl ShowInvitationUrl team (InvitationCode c) = do - template <- invitationEmailUrl . invitationEmail . snd <$> teamTemplates Nothing - branding <- view App.templateBranding - let url = toStrict $ renderTextWithBranding template replace branding - parseHttpsUrl url - where - replace "team" = idToText team - replace "code" = toText c - replace x = x - - parseHttpsUrl :: (Log.MonadLogger m) => Text -> m (Maybe (URIRef Absolute)) - parseHttpsUrl url = - either (\e -> logError url e >> pure Nothing) (pure . Just) $ - parseURI laxURIParserOptions (encodeUtf8 url) - - logError :: (Log.MonadLogger m, Show e) => Text -> e -> m () - logError url e = - Log.err $ - Log.msg - (Log.val "Unable to create invitation url. Please check configuration.") - . Log.field "url" url - . Log.field "error" (show e) diff --git a/services/brig/src/Brig/Team/Email.hs b/services/brig/src/Brig/Team/Email.hs index 07b38e1a57b..441cee5d7bf 100644 --- a/services/brig/src/Brig/Team/Email.hs +++ b/services/brig/src/Brig/Team/Email.hs @@ -18,134 +18,42 @@ -- with this program. If not, see . module Brig.Team.Email - ( InvitationEmail (..), - CreatorWelcomeEmail (..), - MemberWelcomeEmail (..), - sendInvitationMail, - sendCreatorWelcomeMail, - sendMemberWelcomeMail, + ( sendMemberWelcomeMail, + sendPersonalUserMemberWelcomeMail, + sendPersonalUserCreatorWelcomeMail, ) where import Brig.App import Brig.Team.Template -import Control.Lens (view) import Data.Id (TeamId, idToText) -import Data.Text.Ascii qualified as Ascii import Data.Text.Lazy (toStrict) import Imports import Network.Mail.Mime import Polysemy import Wire.API.User import Wire.EmailSending -import Wire.EmailSubsystem.Template (TemplateBranding, renderHtmlWithBranding, renderTextWithBranding) +import Wire.EmailSubsystem.Template -------------------------------------------------------------------------------- --- Invitation Email - -sendInvitationMail :: (Member EmailSending r) => Email -> TeamId -> Email -> InvitationCode -> Maybe Locale -> (AppT r) () -sendInvitationMail to tid from code loc = do - tpl <- invitationEmail . snd <$> teamTemplates loc - branding <- view templateBranding - let mail = InvitationEmail to tid code from - liftSem $ sendMail $ renderInvitationEmail mail tpl branding - -sendCreatorWelcomeMail :: (Member EmailSending r) => Email -> TeamId -> Text -> Maybe Locale -> (AppT r) () -sendCreatorWelcomeMail to tid teamName loc = do - tpl <- creatorWelcomeEmail . snd <$> teamTemplates loc - branding <- view templateBranding - let mail = CreatorWelcomeEmail to tid teamName - liftSem $ sendMail $ renderCreatorWelcomeMail mail tpl branding - -sendMemberWelcomeMail :: (Member EmailSending r) => Email -> TeamId -> Text -> Maybe Locale -> (AppT r) () +sendMemberWelcomeMail :: (Member EmailSending r) => EmailAddress -> TeamId -> Text -> Maybe Locale -> (AppT r) () sendMemberWelcomeMail to tid teamName loc = do - tpl <- memberWelcomeEmail . snd <$> teamTemplates loc - branding <- view templateBranding - let mail = MemberWelcomeEmail to tid teamName - liftSem $ sendMail $ renderMemberWelcomeMail mail tpl branding + tpl <- memberWelcomeEmail . snd <$> teamTemplatesWithLocale loc + branding <- asks (.templateBranding) + liftSem $ sendMail $ renderMemberWelcomeMail to tid teamName tpl branding -------------------------------------------------------------------------------- --- Invitation Email +sendPersonalUserMemberWelcomeMail :: EmailAddress -> TeamId -> Text -> Maybe Locale -> (AppT r) () +sendPersonalUserMemberWelcomeMail _ _ _ _ = do + pure () -data InvitationEmail = InvitationEmail - { invTo :: !Email, - invTeamId :: !TeamId, - invInvCode :: !InvitationCode, - invInviter :: !Email - } - -renderInvitationEmail :: InvitationEmail -> InvitationEmailTemplate -> TemplateBranding -> Mail -renderInvitationEmail InvitationEmail {..} InvitationEmailTemplate {..} branding = - (emptyMail from) - { mailTo = [to], - mailHeaders = - [ ("Subject", toStrict subj), - ("X-Zeta-Purpose", "TeamInvitation"), - ("X-Zeta-Code", Ascii.toText code) - ], - mailParts = [[plainPart txt, htmlPart html]] - } - where - (InvitationCode code) = invInvCode - from = Address (Just invitationEmailSenderName) (fromEmail invitationEmailSender) - to = Address Nothing (fromEmail invTo) - txt = renderTextWithBranding invitationEmailBodyText replace branding - html = renderHtmlWithBranding invitationEmailBodyHtml replace branding - subj = renderTextWithBranding invitationEmailSubject replace branding - replace "url" = renderInvitationUrl invitationEmailUrl invTeamId invInvCode branding - replace "inviter" = fromEmail invInviter - replace x = x - -renderInvitationUrl :: Template -> TeamId -> InvitationCode -> TemplateBranding -> Text -renderInvitationUrl t tid (InvitationCode c) branding = - toStrict $ renderTextWithBranding t replace branding - where - replace "team" = idToText tid - replace "code" = Ascii.toText c - replace x = x - -------------------------------------------------------------------------------- --- Creator Welcome Email - -data CreatorWelcomeEmail = CreatorWelcomeEmail - { cwTo :: !Email, - cwTid :: !TeamId, - cwTeamName :: !Text - } - -renderCreatorWelcomeMail :: CreatorWelcomeEmail -> CreatorWelcomeEmailTemplate -> TemplateBranding -> Mail -renderCreatorWelcomeMail CreatorWelcomeEmail {..} CreatorWelcomeEmailTemplate {..} branding = - (emptyMail from) - { mailTo = [to], - mailHeaders = - [ ("Subject", toStrict subj), - ("X-Zeta-Purpose", "Welcome") - ], - mailParts = [[plainPart txt, htmlPart html]] - } - where - from = Address (Just creatorWelcomeEmailSenderName) (fromEmail creatorWelcomeEmailSender) - to = Address Nothing (fromEmail cwTo) - txt = renderTextWithBranding creatorWelcomeEmailBodyText replace branding - html = renderHtmlWithBranding creatorWelcomeEmailBodyHtml replace branding - subj = renderTextWithBranding creatorWelcomeEmailSubject replace branding - replace "url" = creatorWelcomeEmailUrl - replace "email" = fromEmail cwTo - replace "team_id" = idToText cwTid - replace "team_name" = cwTeamName - replace x = x +sendPersonalUserCreatorWelcomeMail :: EmailAddress -> TeamId -> Text -> Maybe Locale -> (AppT r) () +sendPersonalUserCreatorWelcomeMail _ _ _ _ = do + pure () ------------------------------------------------------------------------------- -- Member Welcome Email -data MemberWelcomeEmail = MemberWelcomeEmail - { mwTo :: !Email, - mwTid :: !TeamId, - mwTeamName :: !Text - } - -renderMemberWelcomeMail :: MemberWelcomeEmail -> MemberWelcomeEmailTemplate -> TemplateBranding -> Mail -renderMemberWelcomeMail MemberWelcomeEmail {..} MemberWelcomeEmailTemplate {..} branding = +renderMemberWelcomeMail :: EmailAddress -> TeamId -> Text -> MemberWelcomeEmailTemplate -> TemplateBranding -> Mail +renderMemberWelcomeMail emailTo tid teamName MemberWelcomeEmailTemplate {..} branding = (emptyMail from) { mailTo = [to], mailHeaders = @@ -156,12 +64,12 @@ renderMemberWelcomeMail MemberWelcomeEmail {..} MemberWelcomeEmailTemplate {..} } where from = Address (Just memberWelcomeEmailSenderName) (fromEmail memberWelcomeEmailSender) - to = Address Nothing (fromEmail mwTo) + to = Address Nothing (fromEmail emailTo) txt = renderTextWithBranding memberWelcomeEmailBodyText replace branding html = renderHtmlWithBranding memberWelcomeEmailBodyHtml replace branding subj = renderTextWithBranding memberWelcomeEmailSubject replace branding replace "url" = memberWelcomeEmailUrl - replace "email" = fromEmail mwTo - replace "team_id" = idToText mwTid - replace "team_name" = mwTeamName + replace "email" = fromEmail emailTo + replace "team_id" = idToText tid + replace "team_name" = teamName replace x = x diff --git a/services/brig/src/Brig/Team/Template.hs b/services/brig/src/Brig/Team/Template.hs index 32f6f803ad4..86c409e9f62 100644 --- a/services/brig/src/Brig/Team/Template.hs +++ b/services/brig/src/Brig/Team/Template.hs @@ -30,40 +30,7 @@ where import Brig.Options import Brig.Template import Imports -import Wire.API.User.Identity - -data InvitationEmailTemplate = InvitationEmailTemplate - { invitationEmailUrl :: !Template, - invitationEmailSubject :: !Template, - invitationEmailBodyText :: !Template, - invitationEmailBodyHtml :: !Template, - invitationEmailSender :: !Email, - invitationEmailSenderName :: !Text - } - -data CreatorWelcomeEmailTemplate = CreatorWelcomeEmailTemplate - { creatorWelcomeEmailUrl :: !Text, - creatorWelcomeEmailSubject :: !Template, - creatorWelcomeEmailBodyText :: !Template, - creatorWelcomeEmailBodyHtml :: !Template, - creatorWelcomeEmailSender :: !Email, - creatorWelcomeEmailSenderName :: !Text - } - -data MemberWelcomeEmailTemplate = MemberWelcomeEmailTemplate - { memberWelcomeEmailUrl :: !Text, - memberWelcomeEmailSubject :: !Template, - memberWelcomeEmailBodyText :: !Template, - memberWelcomeEmailBodyHtml :: !Template, - memberWelcomeEmailSender :: !Email, - memberWelcomeEmailSenderName :: !Text - } - -data TeamTemplates = TeamTemplates - { invitationEmail :: !InvitationEmailTemplate, - creatorWelcomeEmail :: !CreatorWelcomeEmailTemplate, - memberWelcomeEmail :: !MemberWelcomeEmailTemplate - } +import Wire.EmailSubsystem.Template loadTeamTemplates :: Opts -> IO (Localised TeamTemplates) loadTeamTemplates o = readLocalesDir defLocale (templateDir gOptions) "team" $ \fp -> @@ -75,6 +42,13 @@ loadTeamTemplates o = readLocalesDir defLocale (templateDir gOptions) "team" $ \ <*> pure (emailSender gOptions) <*> readText fp "email/sender.txt" ) + <*> ( InvitationEmailTemplate tExistingUrl + <$> readTemplate fp "email/migration-subject.txt" + <*> readTemplate fp "email/migration.txt" + <*> readTemplate fp "email/migration.html" + <*> pure (emailSender gOptions) + <*> readText fp "email/sender.txt" + ) <*> ( CreatorWelcomeEmailTemplate (tCreatorWelcomeUrl tOptions) <$> readTemplate fp "email/new-creator-welcome-subject.txt" <*> readTemplate fp "email/new-creator-welcome.txt" @@ -89,10 +63,27 @@ loadTeamTemplates o = readLocalesDir defLocale (templateDir gOptions) "team" $ \ <*> pure (emailSender gOptions) <*> readText fp "email/sender.txt" ) + <*> ( PersonalUserMemberWelcomeEmailTemplate + "" + (template "") + (template "") + (template "") + (emailSender gOptions) + <$> readText fp "email/sender.txt" + ) + <*> ( PersonalUserCreatorWelcomeEmailTemplate + "" + (template "") + (template "") + (template "") + (emailSender gOptions) + <$> readText fp "email/sender.txt" + ) where - gOptions = general (emailSMS o) - tOptions = team (emailSMS o) - tUrl = template $ tInvitationUrl tOptions - defLocale = setDefaultTemplateLocale (optSettings o) + gOptions = o.emailSMS.general + tOptions = o.emailSMS.team + tUrl = template tOptions.tInvitationUrl + tExistingUrl = template tOptions.tExistingUserInvitationUrl + defLocale = defaultTemplateLocale o.settings readTemplate = readTemplateWithDefault (templateDir gOptions) defLocale "team" readText = readTextWithDefault (templateDir gOptions) defLocale "team" diff --git a/services/brig/src/Brig/Team/Util.hs b/services/brig/src/Brig/Team/Util.hs deleted file mode 100644 index 6ab5eab896d..00000000000 --- a/services/brig/src/Brig/Team/Util.hs +++ /dev/null @@ -1,68 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Brig.Team.Util where -- TODO: remove this module and move contents to Brig.IO.Intra? - -import Brig.API.Error -import Brig.App -import Brig.Data.User qualified as Data -import Brig.Types.User (HavePendingInvitations (NoPendingInvitations)) -import Control.Error -import Control.Lens -import Data.Id -import Data.Set qualified as Set -import Imports -import Polysemy (Member) -import Wire.API.Team.Member -import Wire.API.Team.Permission -import Wire.API.User (User (userTeam)) -import Wire.Error -import Wire.GalleyAPIAccess (GalleyAPIAccess) -import Wire.GalleyAPIAccess qualified as GalleyAPIAccess - --- | If the user is in a team, it has to have these permissions. If not, it is a personal --- user with account validation and thus given the permission implicitly. (Used for --- `SearchContactcs`.) -ensurePermissionsOrPersonalUser :: (Member GalleyAPIAccess r, IsPerm perm) => UserId -> [perm] -> ExceptT HttpError (AppT r) () -ensurePermissionsOrPersonalUser u perms = do - mbUser <- lift $ wrapHttp $ Data.lookupUser NoPendingInvitations u - maybe (pure ()) (\tid -> ensurePermissions u tid perms) (userTeam =<< mbUser :: Maybe TeamId) - -ensurePermissions :: (Member GalleyAPIAccess r, IsPerm perm) => UserId -> TeamId -> [perm] -> ExceptT HttpError (AppT r) () -ensurePermissions u t perms = do - m <- lift $ liftSem $ GalleyAPIAccess.getTeamMember u t - unless (check m) $ - throwStd insufficientTeamPermissions - where - check :: Maybe TeamMember -> Bool - check (Just m) = all (hasPermission m) perms - check Nothing = False - --- | Privilege escalation detection (make sure no `RoleMember` user creates a `RoleOwner`). --- --- There is some code duplication with 'Galley.API.Teams.ensureNotElevated'. -ensurePermissionToAddUser :: (Member GalleyAPIAccess r) => UserId -> TeamId -> Permissions -> ExceptT HttpError (AppT r) () -ensurePermissionToAddUser u t inviteePerms = do - minviter <- lift $ liftSem $ GalleyAPIAccess.getTeamMember u t - unless (check minviter) $ - throwStd insufficientTeamPermissions - where - check :: Maybe TeamMember -> Bool - check (Just inviter) = - hasPermission inviter AddTeamMember - && all (mayGrantPermission inviter) (Set.toList (inviteePerms ^. self)) - check Nothing = False diff --git a/services/brig/src/Brig/User/API/Handle.hs b/services/brig/src/Brig/User/API/Handle.hs index bfa3407059a..0b97637255e 100644 --- a/services/brig/src/Brig/User/API/Handle.hs +++ b/services/brig/src/Brig/User/API/Handle.hs @@ -30,7 +30,6 @@ import Brig.App import Brig.Data.User qualified as Data import Brig.Federation.Client qualified as Federation import Brig.Options (searchSameTeamOnly) -import Control.Lens (view) import Data.Handle (Handle, fromHandle) import Data.Id (UserId) import Data.Qualified @@ -86,7 +85,7 @@ getLocalHandleInfo self handle = do -- | Checks search permissions and filters accordingly filterHandleResults :: Local UserId -> [Public.UserProfile] -> (Handler r) [Public.UserProfile] filterHandleResults searchingUser us = do - sameTeamSearchOnly <- fromMaybe False <$> view (settings . searchSameTeamOnly) + sameTeamSearchOnly <- fromMaybe False <$> asks (.settings.searchSameTeamOnly) if sameTeamSearchOnly then do fromTeam <- lift . wrapClient $ Data.lookupUserTeam (tUnqualified searchingUser) diff --git a/services/brig/src/Brig/User/API/Search.hs b/services/brig/src/Brig/User/API/Search.hs deleted file mode 100644 index afb00c1efd6..00000000000 --- a/services/brig/src/Brig/User/API/Search.hs +++ /dev/null @@ -1,190 +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 Brig.User.API.Search - ( search, - teamUserSearch, - refreshIndex, - reindexAll, - reindexAllIfSameOrNewer, - ) -where - -import Brig.API.Error (fedError) -import Brig.API.Handler -import Brig.App -import Brig.Data.User qualified as DB -import Brig.Effects.FederationConfigStore -import Brig.Effects.FederationConfigStore qualified as E -import Brig.Federation.Client qualified as Federation -import Brig.Options qualified as Opts -import Brig.Team.Util (ensurePermissions, ensurePermissionsOrPersonalUser) -import Brig.Types.Search as Search -import Brig.User.API.Handle qualified as HandleAPI -import Brig.User.Search.Index -import Brig.User.Search.SearchIndex qualified as Q -import Brig.User.Search.TeamUserSearch qualified as Q -import Control.Lens (view) -import Data.Domain (Domain) -import Data.Handle qualified as Handle -import Data.Id -import Data.Range -import Imports -import Network.Wai.Utilities ((!>>)) -import Polysemy -import System.Logger (field, msg) -import System.Logger.Class (val, (~~)) -import System.Logger.Class qualified as Log -import Wire.API.Federation.API.Brig qualified as FedBrig -import Wire.API.Federation.API.Brig qualified as S -import Wire.API.Routes.FederationDomainConfig -import Wire.API.Team.Member (HiddenPerm (SearchContacts)) -import Wire.API.Team.Permission qualified as Public -import Wire.API.Team.SearchVisibility (TeamSearchVisibility (..)) -import Wire.API.User.Search -import Wire.API.User.Search qualified as Public -import Wire.GalleyAPIAccess (GalleyAPIAccess) -import Wire.GalleyAPIAccess qualified as GalleyAPIAccess -import Wire.UserStore (UserStore) -import Wire.UserSubsystem - --- FUTUREWORK: Consider augmenting 'SearchResult' with full user profiles --- for all results. This is tracked in https://wearezeta.atlassian.net/browse/SQCORE-599 -search :: - ( Member GalleyAPIAccess r, - Member FederationConfigStore r, - Member UserStore r, - Member UserSubsystem r - ) => - UserId -> - Text -> - Maybe Domain -> - Maybe (Range 1 500 Int32) -> - (Handler r) (Public.SearchResult Public.Contact) -search searcherId searchTerm maybeDomain maybeMaxResults = do - -- FUTUREWORK(fisx): to reduce cassandra traffic, 'ensurePermissionsOrPersonalUser' could be - -- run from `searchLocally` and `searchRemotely`, resp., where the team id is already - -- available (at least in the local case) and can be passed as an argument rather than - -- looked up again. - ensurePermissionsOrPersonalUser searcherId [SearchContacts] - federationDomain <- viewFederationDomain - mSearcherTeamId <- lift $ wrapClient $ DB.lookupUserTeam searcherId - let queryDomain = fromMaybe federationDomain maybeDomain - if queryDomain == federationDomain - then searchLocally searcherId searchTerm maybeMaxResults - else searchRemotely queryDomain mSearcherTeamId searchTerm - -searchRemotely :: (Member FederationConfigStore r) => Domain -> Maybe TeamId -> Text -> (Handler r) (Public.SearchResult Public.Contact) -searchRemotely domain mTid searchTerm = do - lift . Log.info $ - msg (val "searchRemotely") - ~~ field "domain" (show domain) - ~~ field "searchTerm" searchTerm - mFedCnf <- lift $ liftSem $ E.getFederationConfig domain - let onlyInTeams = case restriction <$> mFedCnf of - Just FederationRestrictionAllowAll -> Nothing - Just (FederationRestrictionByTeam teams) -> Just teams - -- if we are not federating at all, we also do not allow to search any remote teams - Nothing -> Just [] - - searchResponse <- Federation.searchUsers domain (FedBrig.SearchRequest searchTerm mTid onlyInTeams) !>> fedError - let contacts = S.contacts searchResponse - let count = length contacts - pure - SearchResult - { searchResults = contacts, - searchFound = count, - searchReturned = count, - searchTook = 0, - searchPolicy = S.searchPolicy searchResponse, - searchPagingState = Nothing, - searchHasMore = Nothing - } - -searchLocally :: - forall r. - ( Member GalleyAPIAccess r, - Member UserSubsystem r, - Member UserStore r - ) => - UserId -> - Text -> - Maybe (Range 1 500 Int32) -> - (Handler r) (Public.SearchResult Public.Contact) -searchLocally searcherId searchTerm maybeMaxResults = do - let maxResults = maybe 15 (fromIntegral . fromRange) maybeMaxResults - searcherTeamId <- lift $ wrapClient $ DB.lookupUserTeam searcherId - teamSearchInfo <- mkTeamSearchInfo searcherTeamId - - maybeExactHandleMatch <- exactHandleSearch - - let exactHandleMatchCount = length maybeExactHandleMatch - esMaxResults = maxResults - exactHandleMatchCount - - esResult <- - if esMaxResults > 0 - then Q.searchIndex (Q.LocalSearch searcherId searcherTeamId teamSearchInfo) searchTerm esMaxResults - else pure $ SearchResult 0 0 0 [] FullSearch Nothing Nothing - - -- Prepend results matching exact handle and results from ES. - pure $ - esResult - { searchResults = maybeToList maybeExactHandleMatch <> searchResults esResult, - searchFound = exactHandleMatchCount + searchFound esResult, - searchReturned = exactHandleMatchCount + searchReturned esResult - } - where - handleTeamVisibility :: TeamId -> TeamSearchVisibility -> Search.TeamSearchInfo - handleTeamVisibility _ SearchVisibilityStandard = Search.AllUsers - handleTeamVisibility t SearchVisibilityNoNameOutsideTeam = Search.TeamOnly t - - mkTeamSearchInfo :: Maybe TeamId -> (Handler r) TeamSearchInfo - mkTeamSearchInfo searcherTeamId = lift $ do - sameTeamSearchOnly <- fromMaybe False <$> view (settings . Opts.searchSameTeamOnly) - case searcherTeamId of - Nothing -> pure Search.NoTeam - Just t -> - -- This flag in brig overrules any flag on galley - it is system wide - if sameTeamSearchOnly - then pure (Search.TeamOnly t) - else do - -- For team users, we need to check the visibility flag - handleTeamVisibility t <$> liftSem (GalleyAPIAccess.getTeamSearchVisibility t) - - exactHandleSearch :: (Handler r) (Maybe Contact) - exactHandleSearch = do - lsearcherId <- qualifyLocal searcherId - case Handle.parseHandle searchTerm of - Nothing -> pure Nothing - Just handle -> do - HandleAPI.contactFromProfile - <$$> HandleAPI.getLocalHandleInfo lsearcherId handle - -teamUserSearch :: - (Member GalleyAPIAccess r) => - UserId -> - TeamId -> - Maybe Text -> - Maybe RoleFilter -> - Maybe TeamUserSearchSortBy -> - Maybe TeamUserSearchSortOrder -> - Maybe (Range 1 500 Int32) -> - Maybe PagingState -> - (Handler r) (Public.SearchResult Public.TeamContact) -teamUserSearch uid tid mQuery mRoleFilter mSortBy mSortOrder size mPagingState = do - ensurePermissions uid tid [Public.AddTeamMember] -- limit this to team admins to reduce risk of involuntary DOS attacks. (also, this way we don't need to worry about revealing confidential user data to other team members.) - Q.teamUserSearch tid mQuery mRoleFilter mSortBy mSortOrder (fromMaybe (unsafeRange 15) size) mPagingState diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index ba4f765436a..b8e6556f4aa 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -38,48 +38,49 @@ import Brig.API.Types import Brig.API.User (changeSingleAccountStatus) import Brig.App import Brig.Budget -import Brig.Data.Activation qualified as Data import Brig.Data.Client -import Brig.Data.User qualified as Data -import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Options qualified as Opt import Brig.Types.Intra import Brig.User.Auth.Cookie import Brig.ZAuth qualified as ZAuth import Cassandra import Control.Error hiding (bool) -import Control.Lens (to, view) import Data.ByteString.Conversion (toByteString) import Data.Code qualified as Code +import Data.Default import Data.Handle (Handle) import Data.Id import Data.List.NonEmpty qualified as NE import Data.List1 (List1) import Data.List1 qualified as List1 import Data.Misc (PlainTextPassword6) -import Data.Qualified (Local) -import Data.Time.Clock (UTCTime) +import Data.Qualified import Data.ZAuth.Token qualified as ZAuth import Imports import Network.Wai.Utilities.Error ((!>>)) import Polysemy -import Polysemy.Input (Input) +import Polysemy.Input import Polysemy.TinyLog (TinyLog) import Polysemy.TinyLog qualified as Log import System.Logger (field, msg, val, (~~)) +import Util.Timeout import Wire.API.Team.Feature import Wire.API.Team.Feature qualified as Public import Wire.API.User import Wire.API.User.Auth import Wire.API.User.Auth.LegalHold import Wire.API.User.Auth.Sso +import Wire.ActivationCodeStore (ActivationCodeStore) +import Wire.ActivationCodeStore qualified as ActivationCode +import Wire.AuthenticationSubsystem +import Wire.AuthenticationSubsystem qualified as Authentication +import Wire.Events (Events) import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess -import Wire.NotificationSubsystem -import Wire.PasswordStore (PasswordStore) -import Wire.Sem.Paging.Cassandra (InternalPaging) import Wire.UserKeyStore import Wire.UserStore +import Wire.UserSubsystem (UserSubsystem) +import Wire.UserSubsystem qualified as User import Wire.VerificationCode qualified as VerificationCode import Wire.VerificationCodeGen qualified as VerificationCodeGen import Wire.VerificationCodeSubsystem (VerificationCodeSubsystem) @@ -88,57 +89,58 @@ import Wire.VerificationCodeSubsystem qualified as VerificationCodeSubsystem login :: forall r. ( Member GalleyAPIAccess r, - Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, - Member PasswordStore r, + Member ActivationCodeStore r, + Member Events r, + Member TinyLog r, Member UserKeyStore r, Member UserStore r, - Member VerificationCodeSubsystem r + Member UserSubsystem r, + Member VerificationCodeSubsystem r, + Member AuthenticationSubsystem r ) => Login -> CookieType -> ExceptT LoginError (AppT r) (Access ZAuth.User) -login (PasswordLogin (PasswordLoginData li pw label code)) typ = do +login (MkLogin li pw label code) typ = do uid <- resolveLoginId li lift . liftSem . Log.debug $ field "user" (toByteString uid) . field "action" (val "User.login") - wrapHttpClientE $ checkRetryLimit uid - Data.authenticate uid pw `catchE` \case - AuthInvalidUser -> wrapHttpClientE $ loginFailed uid - AuthInvalidCredentials -> wrapHttpClientE $ loginFailed uid - AuthSuspended -> throwE LoginSuspended - AuthEphemeral -> throwE LoginEphemeral - AuthPendingInvitation -> throwE LoginPendingActivation + wrapClientE $ checkRetryLimit uid + + (lift . liftSem $ Authentication.authenticateEither uid pw) >>= \case + Right a -> pure a + Left e -> case e of + AuthInvalidUser -> lift (decrRetryLimit uid) >> throwE LoginFailed + AuthInvalidCredentials -> lift (decrRetryLimit uid) >> throwE LoginFailed + AuthSuspended -> throwE LoginSuspended + AuthEphemeral -> throwE LoginEphemeral + AuthPendingInvitation -> throwE LoginPendingActivation verifyLoginCode code uid newAccess @ZAuth.User @ZAuth.Access uid Nothing typ label where verifyLoginCode :: Maybe Code.Value -> UserId -> ExceptT LoginError (AppT r) () - verifyLoginCode mbCode uid = - verifyCode mbCode Login uid + verifyLoginCode mbCode uid = do + luid <- lift $ qualifyLocal uid + verifyCode mbCode Login luid `catchE` \case - VerificationCodeNoPendingCode -> wrapHttpClientE $ loginFailedWith LoginCodeInvalid uid - VerificationCodeRequired -> wrapHttpClientE $ loginFailedWith LoginCodeRequired uid - VerificationCodeNoEmail -> wrapHttpClientE $ loginFailed uid -login (SmsLogin _) _ = do - -- sms login not supported - throwE LoginFailed + VerificationCodeNoPendingCode -> lift (decrRetryLimit uid) >> throwE LoginCodeInvalid + VerificationCodeRequired -> lift (decrRetryLimit uid) >> throwE LoginCodeRequired + VerificationCodeNoEmail -> lift (decrRetryLimit uid) >> throwE LoginFailed verifyCode :: forall r. - (Member GalleyAPIAccess r, Member VerificationCodeSubsystem r) => + (Member GalleyAPIAccess r, Member VerificationCodeSubsystem r, Member UserSubsystem r) => Maybe Code.Value -> VerificationAction -> - UserId -> + Local UserId -> ExceptT VerificationCodeError (AppT r) () -verifyCode mbCode action uid = do - (mbEmail, mbTeamId) <- getEmailAndTeamId uid +verifyCode mbCode action luid = do + (mbEmail, mbTeamId) <- getEmailAndTeamId luid featureEnabled <- lift $ do mbFeatureEnabled <- liftSem $ GalleyAPIAccess.getVerificationCodeEnabled `traverse` mbTeamId - pure $ fromMaybe (Public.wsStatus (Public.defFeatureStatus @Public.SndFactorPasswordChallengeConfig) == Public.FeatureStatusEnabled) mbFeatureEnabled - isSsoUser <- wrapHttpClientE $ Data.isSamlUser uid + pure $ fromMaybe ((def @(Feature Public.SndFactorPasswordChallengeConfig)).status == Public.FeatureStatusEnabled) mbFeatureEnabled + account <- lift . liftSem $ User.getAccountNoFilter luid + let isSsoUser = maybe False isSamlUser account when (featureEnabled && not isSsoUser) $ do case (mbCode, mbEmail) of (Just code, Just email) -> do @@ -150,41 +152,44 @@ verifyCode mbCode action uid = do (_, Nothing) -> throwE VerificationCodeNoEmail where getEmailAndTeamId :: - UserId -> - ExceptT e (AppT r) (Maybe Email, Maybe TeamId) + Local UserId -> + ExceptT e (AppT r) (Maybe EmailAddress, Maybe TeamId) getEmailAndTeamId u = do - mbAccount <- wrapHttpClientE $ Data.lookupAccount u - pure (userEmail <$> accountUser =<< mbAccount, userTeam <$> accountUser =<< mbAccount) - -loginFailedWith :: (MonadClient m, MonadReader Env m) => LoginError -> UserId -> ExceptT LoginError m () -loginFailedWith e uid = decrRetryLimit uid >> throwE e - -loginFailed :: (MonadClient m, MonadReader Env m) => UserId -> ExceptT LoginError m () -loginFailed = loginFailedWith LoginFailed + mbAccount <- lift . liftSem $ User.getAccountNoFilter u + pure + ( userEmail =<< mbAccount, + userTeam =<< mbAccount + ) -decrRetryLimit :: (MonadClient m, MonadReader Env m) => UserId -> ExceptT LoginError m () -decrRetryLimit = withRetryLimit (\k b -> withBudget k b $ pure ()) +decrRetryLimit :: UserId -> (AppT r) () +decrRetryLimit = wrapClient . withRetryLimit (\k b -> withBudget k b $ pure ()) -checkRetryLimit :: (MonadClient m, MonadReader Env m) => UserId -> ExceptT LoginError m () -checkRetryLimit = withRetryLimit checkBudget +checkRetryLimit :: + ( MonadReader Env m, + MonadClient m + ) => + UserId -> + ExceptT LoginError m () +checkRetryLimit uid = + flip withRetryLimit uid $ \budgetKey budget -> + checkBudget budgetKey budget >>= \case + BudgetExhausted ttl -> throwE . LoginBlocked . RetryAfter . floor $ ttl + BudgetedValue () remaining -> pure $ BudgetedValue () remaining withRetryLimit :: (MonadReader Env m) => - (BudgetKey -> Budget -> ExceptT LoginError m (Budgeted ())) -> + (BudgetKey -> Budget -> m (Budgeted ())) -> UserId -> - ExceptT LoginError m () + m () withRetryLimit action uid = do - mLimitFailedLogins <- view (settings . to Opt.setLimitFailedLogins) + mLimitFailedLogins <- asks (.settings.limitFailedLogins) forM_ mLimitFailedLogins $ \opts -> do let bkey = BudgetKey ("login#" <> idToText uid) budget = Budget - (Opt.timeoutDiff $ Opt.timeout opts) + (timeoutDiff $ Opt.timeout opts) (fromIntegral $ Opt.retryLimit opts) - bresult <- action bkey budget - case bresult of - BudgetExhausted ttl -> throwE . LoginBlocked . RetryAfter . floor $ ttl - BudgetedValue () _ -> pure () + action bkey budget logout :: (ZAuth.TokenPair u a) => @@ -199,11 +204,8 @@ renewAccess :: forall r u a. ( ZAuth.TokenPair u a, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r ) => List1 (ZAuth.Token u) -> Maybe (ZAuth.Token a) -> @@ -219,27 +221,32 @@ renewAccess uts at mcid = do pure $ Access at' ck' revokeAccess :: - (Member TinyLog r, Member PasswordStore r) => - UserId -> + ( Member TinyLog r, + Member UserSubsystem r, + Member AuthenticationSubsystem r + ) => + Local UserId -> PlainTextPassword6 -> [CookieId] -> [CookieLabel] -> ExceptT AuthError (AppT r) () -revokeAccess u pw cc ll = do +revokeAccess luid@(tUnqualified -> u) pw cc ll = do lift . liftSem $ Log.debug $ field "user" (toByteString u) . field "action" (val "User.revokeAccess") - unlessM (lift . wrapHttpClient $ Data.isSamlUser u) $ Data.authenticate u pw + isSaml <- lift . liftSem $ do + account <- User.getAccountNoFilter luid + pure $ maybe False isSamlUser account + unless isSaml do + (lift . liftSem $ Authentication.authenticateEither u pw) + >>= either throwE pure lift $ wrapHttpClient $ revokeCookies u cc ll -------------------------------------------------------------------------------- -- Internal catchSuspendInactiveUser :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + ( Member TinyLog r, + Member UserSubsystem r, + Member Events r ) => UserId -> e -> @@ -264,11 +271,8 @@ newAccess :: forall u a r. ( ZAuth.TokenPair u a, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r ) => UserId -> Maybe ClientId -> @@ -284,44 +288,55 @@ newAccess uid cid ct cl = do t <- lift $ newAccessToken @u @a ck Nothing pure $ Access t (Just ck) -resolveLoginId :: (Member UserKeyStore r, Member UserStore r) => LoginId -> ExceptT LoginError (AppT r) UserId +resolveLoginId :: + ( Member UserKeyStore r, + Member UserStore r, + Member UserSubsystem r, + Member ActivationCodeStore r, + Member (Input (Local ())) r + ) => + LoginId -> + ExceptT LoginError (AppT r) UserId resolveLoginId li = do - usr <- wrapClientE (validateLoginId li) >>= lift . either (liftSem . lookupKey) (liftSem . lookupHandle) + usr <- lift . liftSem . either lookupKey lookupHandle $ validateLoginId li case usr of Nothing -> do - pending <- wrapClientE $ isPendingActivation li + pending <- lift $ isPendingActivation li throwE $ if pending then LoginPendingActivation else LoginFailed Just uid -> pure uid -validateLoginId :: (MonadReader Env m) => LoginId -> ExceptT LoginError m (Either EmailKey Handle) -validateLoginId (LoginByEmail email) = - either - (const $ throwE LoginFailed) - (pure . Left . mkEmailKey) - (validateEmail email) -validateLoginId (LoginByPhone _) = do - -- phone logins are not supported - throwE LoginFailed -validateLoginId (LoginByHandle h) = - pure (Right h) +validateLoginId :: LoginId -> Either EmailKey Handle +validateLoginId (LoginByEmail email) = (Left . mkEmailKey) email +validateLoginId (LoginByHandle h) = Right h -isPendingActivation :: (MonadClient m, MonadReader Env m) => LoginId -> m Bool +isPendingActivation :: + forall r. + ( Member UserSubsystem r, + Member ActivationCodeStore r, + Member (Input (Local ())) r + ) => + LoginId -> + AppT r Bool isPendingActivation ident = case ident of (LoginByHandle _) -> pure False (LoginByEmail e) -> checkKey (mkEmailKey e) - (LoginByPhone _) -> pure False where + checkKey :: EmailKey -> AppT r Bool checkKey k = do - usr <- (>>= fst) <$> Data.lookupActivationCode k - case usr of + musr <- (>>= fst) <$> liftSem (ActivationCode.lookupActivationCode k) + case musr of Nothing -> pure False - Just u -> maybe False (checkAccount k) <$> Data.lookupAccount u + Just usr -> liftSem do + lusr <- qualifyLocal' usr + maybe False (checkAccount k) <$> User.getAccountNoFilter lusr + + checkAccount :: EmailKey -> User -> Bool checkAccount k a = - let i = userIdentity (accountUser a) - statusAdmitsPending = case accountStatus a of + let i = userIdentity a + statusAdmitsPending = case userStatus a of Active -> True Suspended -> False Deleted -> False @@ -374,44 +389,49 @@ validateToken ut at = do -- | Allow to login as any user without having the credentials. ssoLogin :: ( Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r, + Member AuthenticationSubsystem r ) => SsoLogin -> CookieType -> ExceptT LoginError (AppT r) (Access ZAuth.User) ssoLogin (SsoLogin uid label) typ = do - wrapHttpClientE (Data.reauthenticate uid Nothing) `catchE` \case - ReAuthMissingPassword -> pure () - ReAuthCodeVerificationRequired -> pure () - ReAuthCodeVerificationNoPendingCode -> pure () - ReAuthCodeVerificationNoEmail -> pure () - ReAuthError e -> case e of - AuthInvalidUser -> throwE LoginFailed - AuthInvalidCredentials -> pure () - AuthSuspended -> throwE LoginSuspended - AuthEphemeral -> throwE LoginEphemeral - AuthPendingInvitation -> throwE LoginPendingActivation + lift + (liftSem $ Authentication.reauthenticateEither uid Nothing) + >>= \case + Right a -> pure a + Left loginErr -> case loginErr of + -- Important: We throw on Missing Password here because this can only be thrown + -- for non-SSO users, so if we got this error, someone tried to authenticate + -- a regular user as if they were an SSO user, bypassing pwd requirements. + -- This would be a serious security issue if this weren't an internal endpoint. + ReAuthMissingPassword -> throwE LoginFailed + ReAuthCodeVerificationRequired -> pure () + ReAuthCodeVerificationNoPendingCode -> pure () + ReAuthCodeVerificationNoEmail -> pure () + ReAuthError e -> case e of + AuthInvalidUser -> throwE LoginFailed + AuthInvalidCredentials -> pure () + AuthSuspended -> throwE LoginSuspended + AuthEphemeral -> throwE LoginEphemeral + AuthPendingInvitation -> throwE LoginPendingActivation newAccess @ZAuth.User @ZAuth.Access uid Nothing typ label -- | Log in as a LegalHold service, getting LegalHoldUser/Access Tokens. legalHoldLogin :: ( Member GalleyAPIAccess r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member AuthenticationSubsystem r, + Member Events r ) => LegalHoldLogin -> CookieType -> ExceptT LegalHoldLoginError (AppT r) (Access ZAuth.LegalHoldUser) legalHoldLogin (LegalHoldLogin uid pw label) typ = do - wrapHttpClientE (Data.reauthenticate uid pw) !>> LegalHoldReAuthError + (lift . liftSem $ Authentication.reauthenticateEither uid pw) + >>= either (throwE . LegalHoldReAuthError) (const $ pure ()) -- legalhold login is only possible if -- the user is a team user -- and the team has legalhold enabled @@ -428,8 +448,8 @@ assertLegalHoldEnabled :: TeamId -> ExceptT LegalHoldLoginError (AppT r) () assertLegalHoldEnabled tid = do - stat <- lift $ liftSem $ GalleyAPIAccess.getTeamLegalHoldStatus tid - case wsStatus stat of + feat <- lift $ liftSem $ GalleyAPIAccess.getTeamLegalHoldStatus tid + case feat.status of FeatureStatusDisabled -> throwE LegalHoldLoginLegalHoldNotEnabled FeatureStatusEnabled -> pure () diff --git a/services/brig/src/Brig/User/Auth/Cookie.hs b/services/brig/src/Brig/User/Auth/Cookie.hs index e1204c43bfd..081d23c1d38 100644 --- a/services/brig/src/Brig/User/Auth/Cookie.hs +++ b/services/brig/src/Brig/User/Auth/Cookie.hs @@ -31,7 +31,6 @@ module Brig.User.Auth.Cookie newCookieLimited, -- * HTTP - setResponseCookie, toWebCookie, -- * Re-exports @@ -46,7 +45,7 @@ import Brig.User.Auth.Cookie.Limit import Brig.ZAuth qualified as ZAuth import Cassandra import Control.Error -import Control.Lens (to, view) +import Control.Lens (view) import Control.Monad.Except import Data.ByteString.Conversion import Data.Id @@ -55,11 +54,10 @@ import Data.Proxy import Data.RetryAfter import Data.Time.Clock import Imports -import Network.Wai (Response) -import Network.Wai.Utilities.Response (addHeader) import Prometheus qualified as Prom import System.Logger.Class (field, msg, val, (~~)) import System.Logger.Class qualified as Log +import Util.Timeout import Web.Cookie qualified as WebCookie import Wire.API.User.Auth import Wire.SessionStore qualified as Store @@ -79,7 +77,7 @@ newCookie :: Maybe CookieLabel -> m (Cookie (ZAuth.Token u)) newCookie uid cid typ label = do - now <- liftIO =<< view currentTime + now <- liftIO =<< asks (.currentTime) tok <- if typ == PersistentCookie then ZAuth.newUserToken uid cid @@ -118,10 +116,10 @@ nextCookie c mNewCid = runMaybeT $ do -- Keep old client ID by default, but use new one if none was set. let mcid = mOldCid <|> mNewCid - s <- view settings - now <- liftIO =<< view currentTime + s <- asks (.settings) + now <- liftIO =<< asks (.currentTime) let created = cookieCreated c - let renewAge = fromInteger (setUserCookieRenewAge s) + let renewAge = fromInteger s.userCookieRenewAge -- Renew the cookie if the client ID has changed, regardless of age. -- FUTUREWORK: Also renew the cookie if it was signed with a different zauth -- key index, regardless of age. @@ -160,7 +158,7 @@ renewCookie old mcid = do -- around only for another renewal period so as not to build -- an ever growing chain of superseded cookies. let old' = old {cookieSucc = Just (cookieId new)} - ttl <- setUserCookieRenewAge <$> view settings + ttl <- asks (.settings.userCookieRenewAge) adhocSessionStoreInterpreter $ Store.insertCookie uid (toUnitCookie old') (Just (Store.TTL (fromIntegral ttl))) pure new @@ -170,10 +168,10 @@ renewCookie old mcid = do -- implicitly because of cyclical dependencies). mustSuspendInactiveUser :: (MonadReader Env m, MonadClient m) => UserId -> m Bool mustSuspendInactiveUser uid = - view (settings . to setSuspendInactiveUsers) >>= \case + asks (.settings.suspendInactiveUsers) >>= \case Nothing -> pure False Just (SuspendInactiveUsers (Timeout suspendAge)) -> do - now <- liftIO =<< view currentTime + now <- liftIO =<< asks (.currentTime) let suspendHere :: UTCTime suspendHere = addUTCTime (-suspendAge) now youngEnough :: Cookie () -> Bool @@ -195,7 +193,7 @@ newAccessToken c mt = do t' <- case mt of Nothing -> ZAuth.newAccessToken (cookieValue c) Just t -> ZAuth.renewAccessToken (ZAuth.userTokenClient (cookieValue c)) t - zSettings <- view (zauthEnv . ZAuth.settings) + zSettings <- asks ((.zauthEnv) <&> view ZAuth.settings) let ttl = view (ZAuth.settingsTTL (Proxy @a)) zSettings pure $ bearerToken @@ -249,9 +247,9 @@ newCookieLimited :: m (Either RetryAfter (Cookie (ZAuth.Token t))) newCookieLimited u c typ label = do cs <- filter ((typ ==) . cookieType) <$> adhocSessionStoreInterpreter (Store.listCookies u) - now <- liftIO =<< view currentTime - lim <- CookieLimit . setUserCookieLimit <$> view settings - thr <- setUserCookieThrottle <$> view settings + now <- liftIO =<< asks (.currentTime) + lim <- CookieLimit <$> asks (.settings.userCookieLimit) + thr <- asks (.settings.userCookieThrottle) let evict = map cookieId (limitCookies lim now cs) if null evict then Right <$> newCookie u c typ label @@ -264,18 +262,9 @@ newCookieLimited u c typ label = do -------------------------------------------------------------------------------- -- HTTP -setResponseCookie :: - (MonadReader Env m, ZAuth.UserTokenLike u) => - Cookie (ZAuth.Token u) -> - Response -> - m Response -setResponseCookie c r = do - hdr <- toByteString' . WebCookie.renderSetCookie <$> toWebCookie c - pure (addHeader "Set-Cookie" hdr r) - toWebCookie :: (MonadReader Env m, ZAuth.UserTokenLike u) => Cookie (ZAuth.Token u) -> m WebCookie.SetCookie toWebCookie c = do - s <- view settings + s <- asks (.settings) pure $ WebCookie.def { WebCookie.setCookieName = "zuid", @@ -285,7 +274,7 @@ toWebCookie c = do if cookieType c == PersistentCookie then Just (cookieExpires c) else Nothing, - WebCookie.setCookieSecure = not (setCookieInsecure s), + WebCookie.setCookieSecure = not s.cookieInsecure, WebCookie.setCookieHttpOnly = True } diff --git a/services/brig/src/Brig/User/EJPD.hs b/services/brig/src/Brig/User/EJPD.hs index 880fc7d4618..9b54d5bb20d 100644 --- a/services/brig/src/Brig/User/EJPD.hs +++ b/services/brig/src/Brig/User/EJPD.hs @@ -32,6 +32,7 @@ import Control.Lens (view, (^.)) import Data.Aeson qualified as A import Data.ByteString.Conversion import Data.Handle (Handle) +import Data.HavePendingInvitations import Data.Qualified import Data.Set qualified as Set import Data.Text qualified as T @@ -124,7 +125,7 @@ ejpdRequest (fromMaybe False -> includeContacts) (EJPDRequestBody handles) = do mbAssets <- do urls <- forM (userAssets target) $ \(asset :: Asset) -> do - cgh <- asks (view cargoholdEndpoint) + cgh <- asks (.cargoholdEndpoint) let key = toByteString' $ assetKey asset resp <- liftSem $ rpcWithRetries "cargohold" cgh (method GET . paths ["/i/assets", key]) pure $ diff --git a/services/brig/src/Brig/User/Search/Index.hs b/services/brig/src/Brig/User/Search/Index.hs index 7f687369486..a067b296324 100644 --- a/services/brig/src/Brig/User/Search/Index.hs +++ b/services/brig/src/Brig/User/Search/Index.hs @@ -19,9 +19,7 @@ -- with this program. If not, see . module Brig.User.Search.Index - ( mappingName, - boolQuery, - _TextId, + ( boolQuery, -- * Monad IndexEnv (..), @@ -29,78 +27,41 @@ module Brig.User.Search.Index runIndexIO, MonadIndexIO (..), - -- * Updates - reindex, - updateSearchVisibilityInbound, - -- * Administrative createIndex, createIndexIfNotPresent, resetIndex, - reindexAll, - reindexAllIfSameOrNewer, refreshIndex, updateMapping, -- * Re-exports - module Types, ES.IndexSettings (..), ES.IndexName (..), ) where -import Bilge (expect2xx, header, lbytes, paths) import Bilge.IO (MonadHttp) import Bilge.IO qualified as RPC -import Bilge.RPC (RPCException (RPCException)) -import Bilge.Request qualified as RPC (empty, host, method, port) -import Bilge.Response (responseJsonThrow) -import Bilge.Retry (rpcHandlers) import Brig.Index.Types (CreateIndexSettings (..)) -import Brig.Types.Search (SearchVisibilityInbound, defaultSearchVisibilityInbound, searchVisibilityInboundFromFeatureStatus) -import Brig.User.Search.Index.Types as Types -import Cassandra.CQL qualified as C -import Cassandra.Exec qualified as C -import Cassandra.Util import Control.Lens hiding ((#), (.=)) -import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow, throwM, try) +import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow, throwM) import Control.Monad.Except -import Control.Retry (RetryPolicy, exponentialBackoff, limitRetries, recovering) import Data.Aeson as Aeson -import Data.Aeson.Encoding -import Data.Aeson.Lens -import Data.ByteString (toStrict) -import Data.ByteString.Builder (Builder, toLazyByteString) -import Data.ByteString.Conversion (toByteString') -import Data.ByteString.Conversion qualified as Bytes -import Data.ByteString.Lazy qualified as BL import Data.Credentials -import Data.Handle (Handle) import Data.Id import Data.Map qualified as Map -import Data.Text qualified as T import Data.Text qualified as Text import Data.Text.Encoding -import Data.Text.Encoding.Error -import Data.Text.Lazy qualified as LT -import Data.Text.Lens hiding (text) -import Data.UUID qualified as UUID import Database.Bloodhound qualified as ES import Imports hiding (log, searchable) import Network.HTTP.Client hiding (host, path, port) -import Network.HTTP.Types (StdMethod (POST), hContentType, statusCode) +import Network.HTTP.Types (statusCode) import Prometheus (MonadMonitor) -import Prometheus qualified as Prom -import SAML2.WebSSO.Types qualified as SAML import System.Logger qualified as Log import System.Logger.Class (Logger, MonadLogger (..), field, info, msg, val, (+++), (~~)) -import URI.ByteString (URI, serializeURIRef) -import Util.Options (Endpoint, host, port) -import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Multi -import Wire.API.Team.Feature (SearchVisibilityInboundConfig, featureNameBS) -import Wire.API.User -import Wire.API.User qualified as User -import Wire.API.User.Search (Sso (..)) +import Util.Options (Endpoint) +import Wire.IndexedUserStore (IndexedUserStoreError (..)) +import Wire.UserSearch.Types (searchVisibilityInboundFieldName) -------------------------------------------------------------------------------- -- IndexIO Monad @@ -158,141 +119,6 @@ instance MonadHttp IndexIO where manager <- asks idxRpcHttpManager liftIO $ withResponse req manager handler -withDefaultESUrl :: (MonadIndexIO m) => ES.BH m a -> m a -withDefaultESUrl action = do - bhEnv <- liftIndexIO $ asks idxElastic - ES.runBH bhEnv action - --- | When the additional URL is not provided, uses the default url. -withAdditionalESUrl :: (MonadIndexIO m) => ES.BH m a -> m a -withAdditionalESUrl action = do - mAdditionalBHEnv <- liftIndexIO $ asks idxAdditionalElastic - defaultBHEnv <- liftIndexIO $ asks idxElastic - ES.runBH (fromMaybe defaultBHEnv mAdditionalBHEnv) action - --------------------------------------------------------------------------------- --- Updates - -reindex :: (MonadLogger m, MonadIndexIO m, C.MonadClient m) => UserId -> m () -reindex u = do - ixu <- lookupIndexUser u - updateIndex (maybe (IndexDeleteUser u) (IndexUpdateUser IndexUpdateIfNewerVersion) ixu) - -updateIndex :: (MonadIndexIO m) => IndexUpdate -> m () -updateIndex (IndexUpdateUser updateType iu) = liftIndexIO $ do - Prom.incCounter indexUpdateCounter - info $ - field "user" (Bytes.toByteString (view iuUserId iu)) - . msg (val "Indexing user") - idx <- asks idxName - withDefaultESUrl $ indexDoc idx - withAdditionalESUrl $ traverse_ indexDoc =<< asks idxAdditionalName - where - indexDoc :: (MonadIndexIO m, MonadThrow m) => ES.IndexName -> ES.BH m () - indexDoc idx = do - r <- ES.indexDocument idx mappingName versioning (indexToDoc iu) docId - unless (ES.isSuccess r || ES.isVersionConflict r) $ do - liftIO $ Prom.incCounter indexUpdateErrorCounter - ES.parseEsResponse r >>= throwM . IndexUpdateError . either id id - liftIO $ Prom.incCounter indexUpdateSuccessCounter - versioning = - ES.defaultIndexDocumentSettings - { ES.idsVersionControl = indexUpdateToVersionControl updateType (ES.ExternalDocVersion (docVersion (_iuVersion iu))) - } - docId = ES.DocId (view (iuUserId . re _TextId) iu) -updateIndex (IndexUpdateUsers updateType ius) = liftIndexIO $ do - Prom.incCounter indexBulkUpdateCounter - info $ - field "num_users" (length ius) - . msg (val "Bulk indexing users") - -- Sadly, 'bloodhound' is not aware of the versioning capabilities of ES' - -- bulk API, thus we need to stitch everything together by hand. - bhe <- ES.getBHEnv - ES.IndexName idx <- asks idxName - let (ES.MappingName mpp) = mappingName - let (ES.Server base) = ES.bhServer bhe - req <- parseRequest (view unpacked $ base <> "/" <> idx <> "/" <> mpp <> "/_bulk") - authHeaders <- mkAuthHeaders - res <- - liftIO $ - httpLbs - req - { method = "POST", - requestHeaders = [(hContentType, "application/x-ndjson")] <> authHeaders, -- sic - requestBody = RequestBodyLBS (toLazyByteString (foldMap bulkEncode ius)) - } - (ES.bhManager bhe) - unless (ES.isSuccess res) $ do - Prom.incCounter indexBulkUpdateErrorCounter - ES.parseEsResponse res >>= throwM . IndexUpdateError . either id id - Prom.incCounter indexBulkUpdateSuccessCounter - for_ (statuses res) $ \(s, f) -> - Prom.withLabel indexBulkUpdateResponseCounter (Text.pack $ show s) $ (void . flip Prom.addCounter (fromIntegral f)) - where - mkAuthHeaders = do - creds <- asks idxCredentials - pure $ maybe [] ((: []) . mkBasicAuthHeader) creds - - encodeJSONToString :: (ToJSON a) => a -> Builder - encodeJSONToString = fromEncoding . toEncoding - bulkEncode iu = - bulkMeta (view (iuUserId . re _TextId) iu) (docVersion (_iuVersion iu)) - <> "\n" - <> encodeJSONToString (indexToDoc iu) - <> "\n" - bulkMeta :: Text -> ES.DocVersion -> Builder - bulkMeta docId v = - fromEncoding . pairs . pair "index" . pairs $ - "_id" .= docId - <> "_version" .= v - -- "external_gt or external_gte" - <> "_version_type" .= indexUpdateToVersionControlText updateType - statuses :: ES.Reply -> [(Int, Int)] -- [(Status, Int)] - statuses = - Map.toList - . Map.fromListWith (+) - . flip zip [1, 1 ..] - . toListOf (key "items" . values . key "index" . key "status" . _Integral) - . responseBody -updateIndex (IndexDeleteUser u) = liftIndexIO $ do - Prom.incCounter indexDeleteCounter - info $ - field "user" (Bytes.toByteString u) - . msg (val "(Soft) deleting user from index") - idx <- asks idxName - r <- ES.getDocument idx mappingName (ES.DocId (review _TextId u)) - case statusCode (responseStatus r) of - 200 -> case preview (key "_version" . _Integer) (responseBody r) of - Nothing -> throwM $ ES.EsProtocolException "'version' not found" (responseBody r) - Just v -> updateIndex . IndexUpdateUser IndexUpdateIfNewerVersion . mkIndexUser u =<< mkIndexVersion (v + 1) - 404 -> pure () - _ -> ES.parseEsResponse r >>= throwM . IndexUpdateError . either id id - -updateSearchVisibilityInbound :: (MonadIndexIO m) => Multi.TeamStatus SearchVisibilityInboundConfig -> m () -updateSearchVisibilityInbound status = liftIndexIO $ do - withDefaultESUrl . updateAllDocs =<< asks idxName - withAdditionalESUrl $ traverse_ updateAllDocs =<< asks idxAdditionalName - where - updateAllDocs :: (MonadIndexIO m, MonadThrow m) => ES.IndexName -> ES.BH m () - updateAllDocs idx = do - r <- ES.updateByQuery idx query (Just script) - unless (ES.isSuccess r || ES.isVersionConflict r) $ do - ES.parseEsResponse r >>= throwM . IndexUpdateError . either id id - - query :: ES.Query - query = ES.TermQuery (ES.Term "team" $ idToText (Multi.team status)) Nothing - - script :: ES.Script - script = ES.Script (Just (ES.ScriptLanguage "painless")) (Just (ES.ScriptInline scriptText)) Nothing Nothing - - -- Unfortunately ES disallows updating ctx._version with a "Update By Query" - scriptText = - "ctx._source." - <> searchVisibilityInboundFieldName - <> " = '" - <> decodeUtf8 (toByteString' (searchVisibilityInboundFromFeatureStatus (Multi.status status))) - <> "';" - -------------------------------------------------------------------------------- -- Administrative @@ -395,44 +221,9 @@ resetIndex ciSettings = liftIndexIO $ do then createIndex ciSettings else throwM (IndexError "Index deletion failed.") -reindexAllIfSameOrNewer :: (MonadLogger m, MonadIndexIO m, C.MonadClient m) => m () -reindexAllIfSameOrNewer = reindexAllWith IndexUpdateIfSameOrNewerVersion - -reindexAll :: (MonadLogger m, MonadIndexIO m, C.MonadClient m) => m () -reindexAll = reindexAllWith IndexUpdateIfNewerVersion - -reindexAllWith :: (MonadLogger m, MonadIndexIO m, C.MonadClient m) => IndexDocUpdateType -> m () -reindexAllWith updateType = do - idx <- liftIndexIO $ asks idxName - C.liftClient (scanForIndex 1000) >>= loop idx - where - loop idx page = do - info $ - field "size" (length (C.result page)) - . msg (val "Reindex: processing C* page") - unless (null (C.result page)) $ do - let teamsInPage = mapMaybe teamInReindexRow (C.result page) - lookupFn <- liftIndexIO $ getSearchVisibilityInboundMulti teamsInPage - let reindexRow row = - let sv = maybe defaultSearchVisibilityInbound lookupFn (teamInReindexRow row) - in reindexRowToIndexUser row sv - indexUsers <- mapM reindexRow (C.result page) - updateIndex (IndexUpdateUsers updateType indexUsers) - when (C.hasMore page) $ - C.liftClient (C.nextPage page) >>= loop idx - -------------------------------------------------------------------------------- -- Internal --- This is useful and necessary due to the lack of expressiveness in the bulk API -indexUpdateToVersionControlText :: IndexDocUpdateType -> Text -indexUpdateToVersionControlText IndexUpdateIfNewerVersion = "external_gt" -indexUpdateToVersionControlText IndexUpdateIfSameOrNewerVersion = "external_gte" - -indexUpdateToVersionControl :: IndexDocUpdateType -> (ES.ExternalDocVersion -> ES.VersionControl) -indexUpdateToVersionControl IndexUpdateIfNewerVersion = ES.ExternalGT -indexUpdateToVersionControl IndexUpdateIfSameOrNewerVersion = ES.ExternalGTE - traceES :: (MonadIndexIO m) => ByteString -> IndexIO ES.Reply -> m ES.Reply traceES descr act = liftIndexIO $ do info (msg descr) @@ -587,7 +378,7 @@ indexMapping = mpAnalyzer = Nothing, mpFields = mempty }, - (fromString . T.unpack $ searchVisibilityInboundFieldName) + searchVisibilityInboundFieldName .= MappingProperty { mpType = MPKeyword, mpStore = False, @@ -681,278 +472,6 @@ instance ToJSON MappingField where boolQuery :: ES.BoolQuery boolQuery = ES.mkBoolQuery [] [] [] [] -_TextId :: Prism' Text (Id a) -_TextId = prism' (UUID.toText . toUUID) (fmap Id . UUID.fromText) - -mappingName :: ES.MappingName -mappingName = ES.MappingName "user" - -lookupIndexUser :: - (MonadIndexIO m, C.MonadClient m) => - UserId -> - m (Maybe IndexUser) -lookupIndexUser = lookupForIndex - -lookupForIndex :: (C.MonadClient m, MonadIndexIO m) => UserId -> m (Maybe IndexUser) -lookupForIndex u = do - mrow <- C.retry C.x1 (C.query1 cql (C.params C.LocalQuorum (Identity u))) - for mrow $ \row -> do - let mteam = teamInReindexRow row - searchVis <- liftIndexIO $ getSearchVisibilityInbound mteam - reindexRowToIndexUser row searchVis - where - cql :: C.PrepQuery C.R (Identity UserId) ReindexRow - cql = - "SELECT \ - \id, \ - \team, \ - \name, \ - \writetime(name), \ - \status, \ - \writetime(status), \ - \handle, \ - \writetime(handle), \ - \email, \ - \writetime(email), \ - \accent_id, \ - \writetime(accent_id), \ - \activated, \ - \writetime(activated), \ - \service, \ - \writetime(service), \ - \managed_by, \ - \writetime(managed_by), \ - \sso_id, \ - \writetime(sso_id), \ - \email_unvalidated, \ - \writetime(email_unvalidated) \ - \FROM user \ - \WHERE id = ?" - -getSearchVisibilityInbound :: - Maybe TeamId -> - IndexIO SearchVisibilityInbound -getSearchVisibilityInbound Nothing = pure defaultSearchVisibilityInbound -getSearchVisibilityInbound (Just tid) = do - searchVisibilityInboundFromStatus <$> getTeamSearchVisibilityInbound tid - -getSearchVisibilityInboundMulti :: [TeamId] -> IndexIO (TeamId -> SearchVisibilityInbound) -getSearchVisibilityInboundMulti tids = do - Multi.TeamFeatureNoConfigMultiResponse teamsStatuses <- getTeamSearchVisibilityInboundMulti tids - let lookupMap = Map.fromList (teamsStatuses <&> \x -> (Multi.team x, x)) - pure $ \tid -> - searchVisibilityInboundFromStatus (tid `Map.lookup` lookupMap) - -searchVisibilityInboundFromStatus :: Maybe (Multi.TeamStatus SearchVisibilityInboundConfig) -> SearchVisibilityInbound -searchVisibilityInboundFromStatus = \case - Nothing -> defaultSearchVisibilityInbound - Just tvi -> searchVisibilityInboundFromFeatureStatus . Multi.status $ tvi - -scanForIndex :: Int32 -> C.Client (C.Page ReindexRow) -scanForIndex num = do - C.paginate cql (C.paramsP C.One () (num + 1)) - where - cql :: C.PrepQuery C.R () ReindexRow - cql = - "SELECT \ - \id, \ - \team, \ - \name, \ - \writetime(name), \ - \status, \ - \writetime(status), \ - \handle, \ - \writetime(handle), \ - \email, \ - \writetime(email), \ - \accent_id, \ - \writetime(accent_id), \ - \activated, \ - \writetime(activated), \ - \service, \ - \writetime(service), \ - \managed_by, \ - \writetime(managed_by), \ - \sso_id, \ - \writetime(sso_id), \ - \email_unvalidated, \ - \writetime(email_unvalidated) \ - \FROM user" - -type Activated = Bool - -type ReindexRow = - ( UserId, - Maybe TeamId, - Name, - Writetime Name, - Maybe AccountStatus, - Maybe (Writetime AccountStatus), - Maybe Handle, - Maybe (Writetime Handle), - Maybe Email, - Maybe (Writetime Email), - ColourId, - Writetime ColourId, - Activated, - Writetime Activated, - Maybe ServiceId, - Maybe (Writetime ServiceId), - Maybe ManagedBy, - Maybe (Writetime ManagedBy), - Maybe UserSSOId, - Maybe (Writetime UserSSOId), - Maybe Email, - Maybe (Writetime Email) - ) - --- the _2 lens does not work for a tuple this big -teamInReindexRow :: ReindexRow -> Maybe TeamId -teamInReindexRow (_f1, f2, _f3, _f4, _f5, _f6, _f7, _f8, _f9, _f10, _f11, _f12, _f13, _f14, _f15, _f16, _f17, _f18, _f19, _f20, _f21, _f22) = f2 - -reindexRowToIndexUser :: forall m. (MonadThrow m) => ReindexRow -> SearchVisibilityInbound -> m IndexUser -reindexRowToIndexUser - ( u, - mteam, - name, - tName, - status, - tStatus, - handle, - tHandle, - email, - tEmail, - colour, - tColour, - activated, - tActivated, - service, - tService, - managedBy, - tManagedBy, - ssoId, - tSsoId, - emailUnvalidated, - tEmailUnvalidated - ) - searchVisInbound = - do - iu <- - mkIndexUser u - <$> version - [ Just (v tName), - v <$> tStatus, - v <$> tHandle, - v <$> tEmail, - Just (v tColour), - Just (v tActivated), - v <$> tService, - v <$> tManagedBy, - v <$> tSsoId, - v <$> tEmailUnvalidated - ] - pure $ - if shouldIndex - then - iu - & set iuTeam mteam - . set iuName (Just name) - . set iuHandle handle - . set iuEmail email - . set iuColourId (Just colour) - . set iuAccountStatus status - . set iuSAMLIdP (idpUrl =<< ssoId) - . set iuManagedBy managedBy - . set iuCreatedAt (Just (writetimeToUTC tActivated)) - . set iuSearchVisibilityInbound (Just searchVisInbound) - . set iuScimExternalId (join $ User.scimExternalId <$> managedBy <*> ssoId) - . set iuSso (sso =<< ssoId) - . set iuEmailUnvalidated emailUnvalidated - else - iu - -- We insert a tombstone-style user here, as it's easier than deleting the old one. - -- It's mostly empty, but having the status here might be useful in the future. - & set iuAccountStatus status - where - v :: Writetime a -> Int64 - v = writetimeToInt64 - - version :: [Maybe Int64] -> m IndexVersion - version = mkIndexVersion . getMax . mconcat . fmap Max . catMaybes - - shouldIndex = - ( case status of - Nothing -> True - Just Active -> True - Just Suspended -> True - Just Deleted -> False - Just Ephemeral -> False - Just PendingInvitation -> False - ) - && activated -- FUTUREWORK: how is this adding to the first case? - && isNothing service - idpUrl :: UserSSOId -> Maybe Text - idpUrl (UserSSOId (SAML.UserRef (SAML.Issuer uri) _subject)) = - Just $ fromUri uri - idpUrl (UserScimExternalId _) = Nothing - - fromUri :: URI -> Text - fromUri = - decodeUtf8With lenientDecode - . toStrict - . toLazyByteString - . serializeURIRef - - sso :: UserSSOId -> Maybe Sso - sso userSsoId = do - (issuer, nameid) <- User.ssoIssuerAndNameId userSsoId - pure $ Sso {ssoIssuer = issuer, ssoNameId = nameid} - -getTeamSearchVisibilityInbound :: - TeamId -> - IndexIO (Maybe (Multi.TeamStatus SearchVisibilityInboundConfig)) -getTeamSearchVisibilityInbound tid = do - Multi.TeamFeatureNoConfigMultiResponse teamsStatuses <- getTeamSearchVisibilityInboundMulti [tid] - case filter ((== tid) . Multi.team) teamsStatuses of - [teamStatus] -> pure (Just teamStatus) - _ -> pure Nothing - -getTeamSearchVisibilityInboundMulti :: - [TeamId] -> - IndexIO (Multi.TeamFeatureNoConfigMultiResponse SearchVisibilityInboundConfig) -getTeamSearchVisibilityInboundMulti tids = do - galley <- asks idxGalley - serviceRequest' "galley" galley POST req >>= responseJsonThrow (ParseException "galley") - where - req = - paths ["i", "features-multi-teams", featureNameBS @SearchVisibilityInboundConfig] - . header "Content-Type" "application/json" - . expect2xx - . lbytes (encode $ Multi.TeamFeatureNoConfigMultiRequest tids) - - serviceRequest' :: - forall m. - (MonadIO m, MonadMask m, MonadHttp m) => - LT.Text -> - Endpoint -> - StdMethod -> - (Request -> Request) -> - m (Response (Maybe BL.ByteString)) - serviceRequest' nm endpoint m r = do - let service = mkEndpoint endpoint - recovering x3 rpcHandlers $ - const $ do - let rq = (RPC.method m . r) service - res <- try $ RPC.httpLbs rq id - case res of - Left x -> throwM $ RPCException nm rq x - Right x -> pure x - where - mkEndpoint service = RPC.host (encodeUtf8 (service ^. host)) . RPC.port (service ^. port) $ RPC.empty - - x3 :: RetryPolicy - x3 = limitRetries 3 <> exponentialBackoff 100000 - data ParseException = ParseException { _parseExceptionRemote :: !Text, _parseExceptionMsg :: String @@ -966,87 +485,3 @@ instance Show ParseException where ++ m instance Exception ParseException - ---------------------------------------------------------------------------------- --- Metrics - -{-# NOINLINE indexUpdateCounter #-} -indexUpdateCounter :: Prom.Counter -indexUpdateCounter = - Prom.unsafeRegister $ - Prom.counter - Prom.Info - { Prom.metricName = "user_index_update_count", - Prom.metricHelp = "Number of updates on user index" - } - -{-# NOINLINE indexUpdateErrorCounter #-} -indexUpdateErrorCounter :: Prom.Counter -indexUpdateErrorCounter = - Prom.unsafeRegister $ - Prom.counter - Prom.Info - { Prom.metricName = "user_index_update_err", - Prom.metricHelp = "Number of errors during user index update" - } - -{-# NOINLINE indexUpdateSuccessCounter #-} -indexUpdateSuccessCounter :: Prom.Counter -indexUpdateSuccessCounter = - Prom.unsafeRegister $ - Prom.counter - Prom.Info - { Prom.metricName = "user_index_update_ok", - Prom.metricHelp = "Number of successful user index updates" - } - -{-# NOINLINE indexBulkUpdateCounter #-} -indexBulkUpdateCounter :: Prom.Counter -indexBulkUpdateCounter = - Prom.unsafeRegister $ - Prom.counter - Prom.Info - { Prom.metricName = "user_index_update_bulk_count", - Prom.metricHelp = "Number of bulk updates on user index" - } - -{-# NOINLINE indexBulkUpdateErrorCounter #-} -indexBulkUpdateErrorCounter :: Prom.Counter -indexBulkUpdateErrorCounter = - Prom.unsafeRegister $ - Prom.counter - Prom.Info - { Prom.metricName = "user_index_update_bulk_err", - Prom.metricHelp = "Number of errors during bulk updates on user index" - } - -{-# NOINLINE indexBulkUpdateSuccessCounter #-} -indexBulkUpdateSuccessCounter :: Prom.Counter -indexBulkUpdateSuccessCounter = - Prom.unsafeRegister $ - Prom.counter - Prom.Info - { Prom.metricName = "user_index_update_bulk_ok", - Prom.metricHelp = "Number of successful bulk updates on user index" - } - -{-# NOINLINE indexBulkUpdateResponseCounter #-} -indexBulkUpdateResponseCounter :: Prom.Vector Prom.Label1 Prom.Counter -indexBulkUpdateResponseCounter = - Prom.unsafeRegister $ - Prom.vector ("status") $ - Prom.counter - Prom.Info - { Prom.metricName = "user_index_update_bulk_response", - Prom.metricHelp = "Number of successful bulk updates on user index" - } - -{-# NOINLINE indexDeleteCounter #-} -indexDeleteCounter :: Prom.Counter -indexDeleteCounter = - Prom.unsafeRegister $ - Prom.counter - Prom.Info - { Prom.metricName = "user_index_delete_count", - Prom.metricHelp = "Number of deletes on user index" - } diff --git a/services/brig/src/Brig/User/Search/Index/Types.hs b/services/brig/src/Brig/User/Search/Index/Types.hs deleted file mode 100644 index 766c5b5df90..00000000000 --- a/services/brig/src/Brig/User/Search/Index/Types.hs +++ /dev/null @@ -1,230 +0,0 @@ -{-# LANGUAGE StrictData #-} -{-# 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 Brig.User.Search.Index.Types where - -import Brig.Types.Search -import Control.Lens (makeLenses) -import Control.Monad.Catch -import Data.Aeson -import Data.Handle (Handle) -import Data.Id -import Data.Json.Util (UTCTimeMillis (..), toUTCTimeMillis) -import Data.Text qualified as T -import Data.Text.ICU.Translit (trans, transliterate) -import Data.Time (UTCTime) -import Database.Bloodhound hiding (key) -import Database.Bloodhound.Internal.Client (DocVersion (DocVersion)) -import Imports -import Wire.API.Team.Role (Role) -import Wire.API.User -import Wire.API.User.Search (Sso (..)) - -data IndexDocUpdateType - = IndexUpdateIfNewerVersion - | IndexUpdateIfSameOrNewerVersion - -data IndexUpdate - = IndexUpdateUser IndexDocUpdateType IndexUser - | IndexUpdateUsers IndexDocUpdateType [IndexUser] - | IndexDeleteUser UserId - --- | Represents the ES *index*, ie. the attributes of a user that is searchable in ES. See also: --- 'UserDoc'. -data IndexUser = IndexUser - { _iuUserId :: UserId, - _iuVersion :: IndexVersion, - _iuTeam :: Maybe TeamId, - _iuName :: Maybe Name, - _iuHandle :: Maybe Handle, - _iuEmail :: Maybe Email, - _iuColourId :: Maybe ColourId, - _iuAccountStatus :: Maybe AccountStatus, - _iuSAMLIdP :: Maybe Text, - _iuManagedBy :: Maybe ManagedBy, - _iuCreatedAt :: Maybe UTCTime, - _iuRole :: Maybe Role, - _iuSearchVisibilityInbound :: Maybe SearchVisibilityInbound, - _iuScimExternalId :: Maybe Text, - _iuSso :: Maybe Sso, - _iuEmailUnvalidated :: Maybe Email - } - -data IndexQuery r = IndexQuery Query Filter [DefaultSort] - -data IndexError - = IndexUpdateError EsError - | IndexLookupError EsError - | IndexError Text - deriving (Show) - -instance Exception IndexError - -newtype IndexVersion = IndexVersion {docVersion :: DocVersion} - --- | Represents an ES *document*, ie. the subset of user attributes stored in ES. --- See also 'IndexUser'. --- --- If a user is not searchable, e.g. because the account got --- suspended, all fields except for the user id are set to 'Nothing' and --- consequently removed from the index. -data UserDoc = UserDoc - { udId :: UserId, - udTeam :: Maybe TeamId, - udName :: Maybe Name, - udNormalized :: Maybe Text, - udHandle :: Maybe Handle, - udEmail :: Maybe Email, - udColourId :: Maybe ColourId, - udAccountStatus :: Maybe AccountStatus, - udSAMLIdP :: Maybe Text, - udManagedBy :: Maybe ManagedBy, - udCreatedAt :: Maybe UTCTimeMillis, - udRole :: Maybe Role, - udSearchVisibilityInbound :: Maybe SearchVisibilityInbound, - udScimExternalId :: Maybe Text, - udSso :: Maybe Sso, - udEmailUnvalidated :: Maybe Email - } - deriving (Eq, Show) - --- Note: Keep this compatible with the FromJSON instances --- of 'Contact' and 'TeamContact' from 'Wire.API.User.Search -instance ToJSON UserDoc where - toJSON ud = - object - [ "id" .= udId ud, - "team" .= udTeam ud, - "name" .= udName ud, - "normalized" .= udNormalized ud, - "handle" .= udHandle ud, - "email" .= udEmail ud, - "accent_id" .= udColourId ud, - "account_status" .= udAccountStatus ud, - "saml_idp" .= udSAMLIdP ud, - "managed_by" .= udManagedBy ud, - "created_at" .= udCreatedAt ud, - "role" .= udRole ud, - (fromString . T.unpack $ searchVisibilityInboundFieldName) .= udSearchVisibilityInbound ud, - "scim_external_id" .= udScimExternalId ud, - "sso" .= udSso ud, - "email_unvalidated" .= udEmailUnvalidated ud - ] - -instance FromJSON UserDoc where - parseJSON = withObject "UserDoc" $ \o -> - UserDoc - <$> o .: "id" - <*> o .:? "team" - <*> o .:? "name" - <*> o .:? "normalized" - <*> o .:? "handle" - <*> o .:? "email" - <*> o .:? "accent_id" - <*> o .:? "account_status" - <*> o .:? "saml_idp" - <*> o .:? "managed_by" - <*> o .:? "created_at" - <*> o .:? "role" - <*> o .:? (fromString . T.unpack $ searchVisibilityInboundFieldName) - <*> o .:? "scim_external_id" - <*> o .:? "sso" - <*> o .:? "email_unvalidated" - -searchVisibilityInboundFieldName :: Text -searchVisibilityInboundFieldName = "search_visibility_inbound" - -makeLenses ''IndexUser - -mkIndexVersion :: (MonadThrow m, Integral a) => a -> m IndexVersion -mkIndexVersion i = - if i > fromIntegral (maxBound :: Int) - then throwM $ IndexError "Index overflow" - else pure . IndexVersion . fromMaybe maxBound . mkDocVersion . fromIntegral $ i - -mkIndexUser :: UserId -> IndexVersion -> IndexUser -mkIndexUser u v = - IndexUser - { _iuUserId = u, - _iuVersion = v, - _iuTeam = Nothing, - _iuName = Nothing, - _iuHandle = Nothing, - _iuEmail = Nothing, - _iuColourId = Nothing, - _iuAccountStatus = Nothing, - _iuSAMLIdP = Nothing, - _iuManagedBy = Nothing, - _iuCreatedAt = Nothing, - _iuRole = Nothing, - _iuSearchVisibilityInbound = Nothing, - _iuScimExternalId = Nothing, - _iuSso = Nothing, - _iuEmailUnvalidated = Nothing - } - -indexToDoc :: IndexUser -> UserDoc -indexToDoc iu = - UserDoc - { udId = _iuUserId iu, - udTeam = _iuTeam iu, - udName = _iuName iu, - udAccountStatus = _iuAccountStatus iu, - udNormalized = normalized . fromName <$> _iuName iu, - udHandle = _iuHandle iu, - udEmail = _iuEmail iu, - udColourId = _iuColourId iu, - udSAMLIdP = _iuSAMLIdP iu, - udManagedBy = _iuManagedBy iu, - udCreatedAt = toUTCTimeMillis <$> _iuCreatedAt iu, - udRole = _iuRole iu, - udSearchVisibilityInbound = _iuSearchVisibilityInbound iu, - udScimExternalId = _iuScimExternalId iu, - udSso = _iuSso iu, - udEmailUnvalidated = _iuEmailUnvalidated iu - } - --- | FUTUREWORK: Transliteration should be left to ElasticSearch (ICU plugin), but this will --- require a data migration. -normalized :: Text -> Text -normalized = transliterate (trans "Any-Latin; Latin-ASCII; Lower") - -docToIndex :: UserDoc -> IndexUser -docToIndex ud = - -- (Don't use 'mkIndexUser' here! With 'IndexUser', you get compiler warnings if you - -- forget to add new fields here.) - IndexUser - { _iuUserId = udId ud, - _iuVersion = IndexVersion (DocVersion 1), - _iuTeam = udTeam ud, - _iuName = udName ud, - _iuHandle = udHandle ud, - _iuEmail = udEmail ud, - _iuColourId = udColourId ud, - _iuAccountStatus = udAccountStatus ud, - _iuSAMLIdP = udSAMLIdP ud, - _iuManagedBy = udManagedBy ud, - _iuCreatedAt = fromUTCTimeMillis <$> udCreatedAt ud, - _iuRole = udRole ud, - _iuSearchVisibilityInbound = udSearchVisibilityInbound ud, - _iuScimExternalId = udScimExternalId ud, - _iuSso = udSso ud, - _iuEmailUnvalidated = udEmailUnvalidated ud - } diff --git a/services/brig/src/Brig/User/Search/SearchIndex.hs b/services/brig/src/Brig/User/Search/SearchIndex.hs index 82b76637976..f45006c8387 100644 --- a/services/brig/src/Brig/User/Search/SearchIndex.hs +++ b/services/brig/src/Brig/User/Search/SearchIndex.hs @@ -25,10 +25,10 @@ module Brig.User.Search.SearchIndex where import Brig.App (Env, viewFederationDomain) -import Brig.Types.Search import Brig.User.Search.Index import Control.Lens hiding (setting, (#), (.=)) import Control.Monad.Catch (MonadThrow, throwM) +import Data.Aeson.Key qualified as Key import Data.Domain (Domain) import Data.Handle (Handle (fromHandle)) import Data.Id @@ -37,13 +37,17 @@ import Database.Bloodhound qualified as ES import Imports hiding (log, searchable) import Wire.API.User (ColourId (..), Name (fromName)) import Wire.API.User.Search +import Wire.IndexedUserStore (IndexedUserStoreError (..)) +import Wire.IndexedUserStore.ElasticSearch (mappingName) +import Wire.UserSearch.Types +import Wire.UserStore.IndexUser (normalized) --- | User that is performing the search --- Team of user that is performing the search --- Outgoing search restrictions data SearchSetting = FederatedSearch (Maybe [TeamId]) - | LocalSearch + | -- | User that is performing the search + -- Team of user that is performing the search + -- Outgoing search restrictions + LocalSearch UserId (Maybe TeamId) TeamSearchInfo @@ -186,7 +190,7 @@ termQ f v = matchSelf :: SearchSetting -> Maybe ES.Query matchSelf (FederatedSearch _) = Nothing -matchSelf (LocalSearch searcher _tid _searchInfo) = Just (termQ "_id" (review _TextId searcher)) +matchSelf (LocalSearch searcher _tid _searchInfo) = Just (termQ "_id" (idToText searcher)) -- | See 'TeamSearchInfo' restrictSearchSpace :: SearchSetting -> ES.Query @@ -244,7 +248,7 @@ matchTeamMembersSearchableByAllTeams = boolQuery { ES.boolQueryMustMatch = [ ES.QueryExistsQuery $ ES.FieldName "team", - ES.TermQuery (ES.Term searchVisibilityInboundFieldName "searchable-by-all-teams") Nothing + ES.TermQuery (ES.Term (Key.toText searchVisibilityInboundFieldName) "searchable-by-all-teams") Nothing ] } diff --git a/services/brig/src/Brig/User/Search/TeamSize.hs b/services/brig/src/Brig/User/Search/TeamSize.hs deleted file mode 100644 index dce653ab03b..00000000000 --- a/services/brig/src/Brig/User/Search/TeamSize.hs +++ /dev/null @@ -1,45 +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 . - -module Brig.User.Search.TeamSize - ( teamSize, - ) -where - -import Brig.Types.Team (TeamSize (..)) -import Brig.User.Search.Index -import Control.Monad.Catch (throwM) -import Data.Id -import Database.Bloodhound qualified as ES -import Imports hiding (log, searchable) - -teamSize :: (MonadIndexIO m) => TeamId -> m TeamSize -teamSize t = liftIndexIO $ do - indexName <- asks idxName - countResEither <- ES.countByIndex indexName (ES.CountQuery query) - countRes <- either (throwM . IndexLookupError) pure countResEither - pure . TeamSize $ ES.crCount countRes - where - query = - ES.TermQuery - ES.Term - { ES.termField = "team", - ES.termValue = idToText t - } - Nothing diff --git a/services/brig/src/Brig/User/Search/TeamUserSearch.hs b/services/brig/src/Brig/User/Search/TeamUserSearch.hs deleted file mode 100644 index 90bcb969e96..00000000000 --- a/services/brig/src/Brig/User/Search/TeamUserSearch.hs +++ /dev/null @@ -1,175 +0,0 @@ -{-# LANGUAGE StrictData #-} -{-# OPTIONS_GHC -Wno-orphans #-} --- Disabling to stop warnings on HasCallStack -{-# OPTIONS_GHC -Wno-redundant-constraints #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Brig.User.Search.TeamUserSearch - ( teamUserSearch, - teamUserSearchQuery, - TeamUserSearchSortBy (..), - TeamUserSearchSortOrder (..), - RoleFilter (..), - ) -where - -import Brig.User.Search.Index -import Control.Error (lastMay) -import Control.Monad.Catch (MonadThrow (throwM)) -import Data.Aeson (decode', encode) -import Data.ByteString (fromStrict, toStrict) -import Data.Id (TeamId, idToText) -import Data.Range (Range (..)) -import Data.Text.Ascii (decodeBase64Url, encodeBase64Url) -import Database.Bloodhound qualified as ES -import Imports hiding (log, searchable) -import Wire.API.User.Search - -teamUserSearch :: - (HasCallStack, MonadIndexIO m) => - TeamId -> - Maybe Text -> - Maybe RoleFilter -> - Maybe TeamUserSearchSortBy -> - Maybe TeamUserSearchSortOrder -> - Range 1 500 Int32 -> - Maybe PagingState -> - m (SearchResult TeamContact) -teamUserSearch tid mbSearchText mRoleFilter mSortBy mSortOrder (fromRange -> size) mPagingState = liftIndexIO $ do - let (IndexQuery q f sortSpecs) = teamUserSearchQuery tid mbSearchText mRoleFilter mSortBy mSortOrder - idx <- asks idxName - let search = - (ES.mkSearch (Just q) (Just f)) - { -- we are requesting one more result than the page size to determine if there is a next page - ES.size = ES.Size (fromIntegral size + 1), - ES.sortBody = Just (fmap ES.DefaultSortSpec sortSpecs), - ES.searchAfterKey = toSearchAfterKey =<< mPagingState - } - r <- - ES.searchByType idx mappingName search - >>= ES.parseEsResponse - either (throwM . IndexLookupError) (pure . mkResult) r - where - toSearchAfterKey :: PagingState -> Maybe ES.SearchAfterKey - toSearchAfterKey ps = decode' . fromStrict =<< (decodeBase64Url . unPagingState $ ps) - - fromSearchAfterKey :: ES.SearchAfterKey -> PagingState - fromSearchAfterKey = PagingState . encodeBase64Url . toStrict . encode - - mkResult es = - let hitsPlusOne = ES.hits . ES.searchHits $ es - hits = take (fromIntegral size) hitsPlusOne - mps = fromSearchAfterKey <$> lastMay (mapMaybe ES.hitSort hits) - results = mapMaybe ES.hitSource hits - in SearchResult - { searchFound = ES.hitsTotal . ES.searchHits $ es, - searchReturned = length results, - searchTook = ES.took es, - searchResults = results, - searchPolicy = FullSearch, - searchPagingState = mps, - searchHasMore = Just $ length hitsPlusOne > length hits - } - --- FUTURWORK: Implement role filter (needs galley data) -teamUserSearchQuery :: - TeamId -> - Maybe Text -> - Maybe RoleFilter -> - Maybe TeamUserSearchSortBy -> - Maybe TeamUserSearchSortOrder -> - IndexQuery TeamContact -teamUserSearchQuery tid mbSearchText _mRoleFilter mSortBy mSortOrder = - IndexQuery - ( maybe - (ES.MatchAllQuery Nothing) - matchPhraseOrPrefix - mbQStr - ) - teamFilter - -- in combination with pagination a non-unique search specification can lead to missing results - -- therefore we use the unique `_doc` value as a tie breaker - -- - see https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-sort.html for details on `_doc` - -- - see https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-search-after.html for details on pagination and tie breaker - -- in the latter article it "is advised to duplicate (client side or [...]) the content of the _id field - -- in another field that has doc value enabled and to use this new field as the tiebreaker for the sort" - -- so alternatively we could use the user ID as a tie breaker, but this would require a change in the index mapping - (sorting ++ sortingTieBreaker) - where - sorting :: [ES.DefaultSort] - sorting = - maybe - [defaultSort SortByCreatedAt SortOrderDesc | isNothing mbQStr] - (\tuSortBy -> [defaultSort tuSortBy (fromMaybe SortOrderAsc mSortOrder)]) - mSortBy - sortingTieBreaker :: [ES.DefaultSort] - sortingTieBreaker = [ES.DefaultSort (ES.FieldName "_doc") ES.Ascending Nothing Nothing Nothing Nothing] - - mbQStr :: Maybe Text - mbQStr = - case mbSearchText of - Nothing -> Nothing - Just q -> - case normalized q of - "" -> Nothing - term' -> Just term' - - matchPhraseOrPrefix term' = - ES.QueryMultiMatchQuery $ - ( ES.mkMultiMatchQuery - [ ES.FieldName "email^4", - ES.FieldName "handle^4", - ES.FieldName "normalized^3", - ES.FieldName "email.prefix^3", - ES.FieldName "handle.prefix^2", - ES.FieldName "normalized.prefix" - ] - (ES.QueryString term') - ) - { ES.multiMatchQueryType = Just ES.MultiMatchMostFields, - ES.multiMatchQueryOperator = ES.And - } - - teamFilter = - ES.Filter $ - ES.QueryBoolQuery - boolQuery - { ES.boolQueryMustMatch = [ES.TermQuery (ES.Term "team" $ idToText tid) Nothing] - } - - defaultSort :: TeamUserSearchSortBy -> TeamUserSearchSortOrder -> ES.DefaultSort - defaultSort tuSortBy sortOrder = - ES.DefaultSort - ( case tuSortBy of - SortByName -> ES.FieldName "name" - SortByHandle -> ES.FieldName "handle.keyword" - SortByEmail -> ES.FieldName "email.keyword" - SortBySAMLIdp -> ES.FieldName "saml_idp" - SortByManagedBy -> ES.FieldName "managed_by" - SortByRole -> ES.FieldName "role" - SortByCreatedAt -> ES.FieldName "created_at" - ) - ( case sortOrder of - SortOrderAsc -> ES.Ascending - SortOrderDesc -> ES.Descending - ) - Nothing - Nothing - Nothing - Nothing diff --git a/services/brig/src/Brig/User/Template.hs b/services/brig/src/Brig/User/Template.hs index 0667a4b2cd2..110d979bc80 100644 --- a/services/brig/src/Brig/User/Template.hs +++ b/services/brig/src/Brig/User/Template.hs @@ -138,17 +138,17 @@ loadUserTemplates o = readLocalesDir defLocale templateDir "user" $ \fp -> <*> readText fp "email/sender.txt" ) where - gOptions = Opt.general $ Opt.emailSMS o - uOptions = Opt.user $ Opt.emailSMS o - tOptions = Opt.team $ Opt.emailSMS o - emailSender = Opt.emailSender gOptions - smsSender = Opt.smsSender gOptions - smsActivationUrl = template $ Opt.smsActivationUrl uOptions - activationUrl = template $ Opt.activationUrl uOptions - teamActivationUrl = template $ Opt.tActivationUrl tOptions - passwordResetUrl = template $ Opt.passwordResetUrl uOptions - deletionUserUrl = template $ Opt.deletionUrl uOptions - defLocale = Opt.setDefaultTemplateLocale (Opt.optSettings o) - templateDir = Opt.templateDir gOptions + gOptions = o.emailSMS.general + uOptions = o.emailSMS.user + tOptions = o.emailSMS.team + emailSender = gOptions.emailSender + smsSender = gOptions.smsSender + smsActivationUrl = template uOptions.smsActivationUrl + activationUrl = template uOptions.activationUrl + teamActivationUrl = template tOptions.tActivationUrl + passwordResetUrl = template uOptions.passwordResetUrl + deletionUserUrl = template uOptions.deletionUrl + defLocale = Opt.defaultTemplateLocale o.settings + templateDir = gOptions.templateDir readTemplate = readTemplateWithDefault templateDir defLocale "user" readText = readTextWithDefault templateDir defLocale "user" diff --git a/services/brig/src/Brig/Version.hs b/services/brig/src/Brig/Version.hs index 9d16efedd7d..86f3a33b937 100644 --- a/services/brig/src/Brig/Version.hs +++ b/services/brig/src/Brig/Version.hs @@ -19,7 +19,6 @@ module Brig.Version where import Brig.API.Handler import Brig.App -import Control.Lens import Data.Set qualified as Set import Imports import Servant (ServerT) @@ -28,9 +27,9 @@ import Wire.API.Routes.Version versionAPI :: ServerT VersionAPI (Handler r) versionAPI = Named $ do - fed <- view federator + fed <- asks (.federator) dom <- viewFederationDomain - disabled <- view disabledVersions + disabled <- asks (.disabledVersions) let allVersions = Set.difference (Set.fromList supportedVersions) disabled devVersions = Set.difference (Set.fromList developmentVersions) disabled supported = Set.difference allVersions devVersions diff --git a/services/brig/src/Brig/ZAuth.hs b/services/brig/src/Brig/ZAuth.hs index 9eaf2cba30a..512e1251b98 100644 --- a/services/brig/src/Brig/ZAuth.hs +++ b/services/brig/src/Brig/ZAuth.hs @@ -77,7 +77,6 @@ module Brig.ZAuth userTokenRand, tokenExpires, tokenExpiresUTC, - tokenKeyIndex, zauthType, -- * Re-exports @@ -444,9 +443,6 @@ userTokenRand' t = t ^. body . rand legalHoldUserTokenRand :: Token LegalHoldUser -> Word32 legalHoldUserTokenRand t = t ^. body . legalHoldUser . rand -tokenKeyIndex :: Token a -> Int -tokenKeyIndex t = t ^. header . key - tokenExpires :: Token a -> POSIXTime tokenExpires t = fromIntegral (t ^. header . time) diff --git a/services/brig/test/integration/API/Calling.hs b/services/brig/test/integration/API/Calling.hs index 442dcfca55b..ce9b2bebbf6 100644 --- a/services/brig/test/integration/API/Calling.hs +++ b/services/brig/test/integration/API/Calling.hs @@ -102,7 +102,7 @@ testSFT b opts = do "when SFT discovery is not enabled, sft_servers shouldn't be returned" Nothing (cfg ^. rtcConfSftServers) - withSettingsOverrides (opts & Opts.sftL ?~ Opts.SFTOptions "integration-tests.zinfra.io" Nothing (Just 0.001) Nothing Nothing) $ do + withSettingsOverrides (opts & Opts.sftLens ?~ Opts.SFTOptions "integration-tests.zinfra.io" Nothing (Just 0.001) Nothing Nothing) $ do cfg1 <- retryWhileN 10 (isNothing . view rtcConfSftServers) (getTurnConfigurationV2 uid b) -- These values are controlled by https://github.com/zinfra/cailleach/tree/77ca2d23cf2959aa183dd945d0a0b13537a8950d/environments/dns-integration-tests let Right server1 = mkHttpsUrl =<< first show (parseURI laxURIParserOptions "https://sft01.integration-tests.zinfra.io:443") @@ -116,7 +116,7 @@ testSFT b opts = do testSFTUnavailable :: Brig -> Opts.Opts -> String -> Http () testSFTUnavailable b opts domain = do uid <- userId <$> randomUser b - withSettingsOverrides (opts {Opts.optSettings = (Opts.optSettings opts) {Opts.setSftStaticUrl = fromByteString (cs domain), Opts.setSftListAllServers = Just Opts.ListAllSFTServers}}) $ do + withSettingsOverrides (opts {Opts.settings = (Opts.settings opts) {Opts.sftStaticUrl = fromByteString (cs domain), Opts.sftListAllServers = Just Opts.ListAllSFTServers}}) $ do cfg <- getTurnConfigurationV2 uid b liftIO $ do assertEqual @@ -178,7 +178,7 @@ testCallsConfigSRV b opts = do uid <- userId <$> randomUser b let dnsOpts = Opts.TurnSourceDNS (Opts.TurnDnsOpts "integration-tests.zinfra.io" (Just 0.5)) config <- - withSettingsOverrides (opts & Opts.turnL . Opts.serversSourceL .~ dnsOpts) $ + withSettingsOverrides (opts & Opts.turnLens . Opts.serversSourceLens .~ dnsOpts) $ responseJsonError =<< ( retryWhileN 10 (\r -> statusCode r /= 200) (getTurnConfiguration "" uid b) randomUser b let dnsOpts = Opts.TurnSourceDNS (Opts.TurnDnsOpts "integration-tests.zinfra.io" (Just 0.5)) config <- - withSettingsOverrides (opts & Opts.turnL . Opts.serversSourceL .~ dnsOpts) $ + withSettingsOverrides (opts & Opts.turnLens . Opts.serversSourceLens .~ dnsOpts) $ responseJsonError =<< ( retryWhileN 10 (\r -> statusCode r /= 200) (getTurnConfiguration "v2" uid b) Opt.Opts -> Opt.Opts allowFullSearch domain opts = - opts & Opt.optionSettings . Opt.federationDomainConfigs ?~ [Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domain FullSearch FederationRestrictionAllowAll] + opts & Opt.settingsLens . Opt.federationDomainConfigsLens ?~ [Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domain FullSearch FederationRestrictionAllowAll] testSearchSuccess :: Opt.Opts -> Brig -> Http () testSearchSuccess opts brig = do @@ -176,7 +176,7 @@ testSearchRestrictions opts brig = do let opts' = opts - & Opt.optionSettings . Opt.federationDomainConfigs + & Opt.settingsLens . Opt.federationDomainConfigsLens ?~ [ Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domainNoSearch NoSearch FederationRestrictionAllowAll, Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domainExactHandle ExactHandleSearch FederationRestrictionAllowAll, Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domainFullSearch FullSearch FederationRestrictionAllowAll @@ -220,7 +220,7 @@ testGetUserByHandleRestrictions opts brig = do let opts' = opts - & Opt.optionSettings . Opt.federationDomainConfigs + & Opt.settingsLens . Opt.federationDomainConfigsLens ?~ [ Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domainNoSearch NoSearch FederationRestrictionAllowAll, Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domainExactHandle ExactHandleSearch FederationRestrictionAllowAll, Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domainFullSearch FullSearch FederationRestrictionAllowAll diff --git a/services/brig/test/integration/API/Internal.hs b/services/brig/test/integration/API/Internal.hs index f3b65f6b37c..55a713d321d 100644 --- a/services/brig/test/integration/API/Internal.hs +++ b/services/brig/test/integration/API/Internal.hs @@ -23,43 +23,35 @@ module API.Internal ) where -import API.Internal.Util import API.MLS.Util import Bilge import Bilge.Assert -import Brig.Data.User (lookupFeatureConferenceCalling, userExists) +import Brig.Data.User import Brig.Options qualified as Opt import Cassandra qualified as C import Cassandra qualified as Cass import Cassandra.Util -import Control.Exception (ErrorCall (ErrorCall), throwIO) -import Control.Lens ((^.), (^?!)) -import Data.Aeson qualified as Aeson -import Data.Aeson.Lens qualified as Aeson -import Data.Aeson.Types qualified as Aeson +import Control.Monad.Catch import Data.ByteString.Conversion (toByteString') import Data.Default import Data.Id import Data.Qualified -import GHC.TypeLits (KnownSymbol) import Imports import System.IO.Temp import Test.Tasty import Test.Tasty.HUnit import Util import Util.Options (Endpoint) -import Wire.API.Team.Feature -import Wire.API.Team.Feature qualified as ApiFt import Wire.API.User import Wire.API.User.Client +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 "account features: conferenceCalling" $ - testFeatureConferenceCallingByAccount opts mgr db brig brigep galley, - test mgr "suspend and unsuspend user" $ testSuspendUser db brig, + [ 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 "mls/clients" $ testGetMlsClients brig, @@ -94,76 +86,6 @@ setAccountStatus brig u s = . json (AccountStatusUpdate s) ) -testFeatureConferenceCallingByAccount :: forall m. (TestConstraints m) => Opt.Opts -> Manager -> Cass.ClientState -> Brig -> Endpoint -> Galley -> m () -testFeatureConferenceCallingByAccount (Opt.optSettings -> settings) mgr db brig brigep galley = do - let check :: (HasCallStack) => ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig -> m () - check status = do - uid <- userId <$> createUser "joe" brig - _ <- - aFewTimes 12 (putAccountConferenceCallingConfigClient brigep mgr uid status) isRight - >>= either (liftIO . throwIO . ErrorCall . ("putAccountConferenceCallingConfigClient: " <>) . show) pure - - mbStatus' <- getAccountConferenceCallingConfigClient brigep mgr uid - liftIO $ assertEqual "GET /i/users/:uid/features/conferenceCalling" (Right status) mbStatus' - - featureConfigs <- getAllFeatureConfigs galley uid - liftIO $ assertEqual "GET /feature-configs" status (ApiFt.forgetLock $ readFeatureConfigs featureConfigs) - - featureConfigsConfCalling <- getFeatureConfig @ApiFt.ConferenceCallingConfig galley uid - liftIO $ assertEqual "GET /feature-configs/conferenceCalling" status (responseJsonUnsafe featureConfigsConfCalling) - - check' :: m () - check' = do - uid <- userId <$> createUser "joe" brig - let defaultIfNull :: ApiFt.WithStatus ApiFt.ConferenceCallingConfig - defaultIfNull = settings ^. Opt.getAfcConferenceCallingDefNull - - defaultIfNewRaw :: Maybe (ApiFt.WithStatus ApiFt.ConferenceCallingConfig) - defaultIfNewRaw = - -- tested manually: whether we remove `defaultForNew` from `brig.yaml` or set it - -- to `enabled` or `disabled`, this test always passes. - settings ^. Opt.getAfcConferenceCallingDefNewMaybe - - do - cassandraResp :: Maybe (ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig) <- - aFewTimes - 12 - (Cass.runClient db (lookupFeatureConferenceCalling uid)) - isJust - liftIO $ assertEqual mempty (ApiFt.forgetLock <$> defaultIfNewRaw) cassandraResp - - _ <- - aFewTimes 12 (deleteAccountConferenceCallingConfigClient brigep mgr uid) isRight - >>= either (liftIO . throwIO . ErrorCall . ("deleteAccountConferenceCallingConfigClient: " <>) . show) pure - - do - cassandraResp :: Maybe (ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig) <- - aFewTimes - 12 - (Cass.runClient db (lookupFeatureConferenceCalling uid)) - isJust - liftIO $ assertEqual mempty Nothing cassandraResp - - mbStatus' <- getAccountConferenceCallingConfigClient brigep mgr uid - liftIO $ assertEqual "GET /i/users/:uid/features/conferenceCalling" (Right (ApiFt.forgetLock defaultIfNull)) mbStatus' - - featureConfigs <- getAllFeatureConfigs galley uid - liftIO $ assertEqual "GET /feature-configs" defaultIfNull (readFeatureConfigs featureConfigs) - - featureConfigsConfCalling <- getFeatureConfig @ApiFt.ConferenceCallingConfig galley uid - liftIO $ assertEqual "GET /feature-configs/conferenceCalling" defaultIfNull (responseJsonUnsafe featureConfigsConfCalling) - - readFeatureConfigs :: (HasCallStack) => ResponseLBS -> ApiFt.WithStatus ApiFt.ConferenceCallingConfig - readFeatureConfigs = - either (error . show) id - . Aeson.parseEither Aeson.parseJSON - . (^?! Aeson.key "conferenceCalling") - . responseJsonUnsafe @Aeson.Value - - check $ ApiFt.WithStatusNoLock ApiFt.FeatureStatusEnabled ApiFt.ConferenceCallingConfig ApiFt.FeatureTTLUnlimited - check $ ApiFt.WithStatusNoLock ApiFt.FeatureStatusDisabled ApiFt.ConferenceCallingConfig ApiFt.FeatureTTLUnlimited - check' - testGetMlsClients :: Brig -> Http () testGetMlsClients brig = do qusr <- userQualifiedId <$> randomUser brig @@ -198,14 +120,6 @@ createClient brig u i = (defNewClient PermanentClientType [somePrekeys !! i] (someLastPrekeys !! i)) (Request -> Request) -> UserId -> m ResponseLBS -getFeatureConfig galley uid = do - get $ apiVersion "v1" . galley . paths ["feature-configs", featureNameBS @cfg] . zUser uid - -getAllFeatureConfigs :: (MonadHttp m, HasCallStack) => (Request -> Request) -> UserId -> m ResponseLBS -getAllFeatureConfigs galley uid = do - get $ galley . paths ["feature-configs"] . zUser uid - 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 diff --git a/services/brig/test/integration/API/Internal/Util.hs b/services/brig/test/integration/API/Internal/Util.hs deleted file mode 100644 index b37bff338a2..00000000000 --- a/services/brig/test/integration/API/Internal/Util.hs +++ /dev/null @@ -1,67 +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.Util - ( TestConstraints, - getAccountConferenceCallingConfigClient, - putAccountConferenceCallingConfigClient, - deleteAccountConferenceCallingConfigClient, - ) -where - -import Bilge hiding (host, port) -import Control.Lens ((^.)) -import Control.Monad.Catch (MonadCatch) -import Data.Id -import Data.Proxy (Proxy (Proxy)) -import Data.String.Conversions -import Imports -import Servant.API ((:>)) -import Servant.API.ContentTypes (NoContent) -import Servant.Client qualified as Client -import Util.Options (Endpoint, host, port) -import Wire.API.Routes.Internal.Brig as IAPI -import Wire.API.Team.Feature qualified as Public - -type TestConstraints m = (MonadFail m, MonadCatch m, MonadIO m, MonadHttp m) - -getAccountConferenceCallingConfigClientM :: UserId -> Client.ClientM (Public.WithStatusNoLock Public.ConferenceCallingConfig) -getAccountConferenceCallingConfigClientM = Client.client (Proxy @("i" :> IAPI.GetAccountConferenceCallingConfig)) - -putAccountConferenceCallingConfigClientM :: UserId -> Public.WithStatusNoLock Public.ConferenceCallingConfig -> Client.ClientM NoContent -putAccountConferenceCallingConfigClientM = Client.client (Proxy @("i" :> IAPI.PutAccountConferenceCallingConfig)) - -deleteAccountConferenceCallingConfigClientM :: UserId -> Client.ClientM NoContent -deleteAccountConferenceCallingConfigClientM = Client.client (Proxy @("i" :> IAPI.DeleteAccountConferenceCallingConfig)) - -getAccountConferenceCallingConfigClient :: (HasCallStack, MonadIO m) => Endpoint -> Manager -> UserId -> m (Either Client.ClientError (Public.WithStatusNoLock Public.ConferenceCallingConfig)) -getAccountConferenceCallingConfigClient brigep mgr uid = runHereClientM brigep mgr (getAccountConferenceCallingConfigClientM uid) - -putAccountConferenceCallingConfigClient :: (HasCallStack, MonadIO m) => Endpoint -> Manager -> UserId -> Public.WithStatusNoLock Public.ConferenceCallingConfig -> m (Either Client.ClientError NoContent) -putAccountConferenceCallingConfigClient brigep mgr uid cfg = runHereClientM brigep mgr (putAccountConferenceCallingConfigClientM uid cfg) - -deleteAccountConferenceCallingConfigClient :: (HasCallStack, MonadIO m) => Endpoint -> Manager -> UserId -> m (Either Client.ClientError NoContent) -deleteAccountConferenceCallingConfigClient brigep mgr uid = runHereClientM brigep mgr (deleteAccountConferenceCallingConfigClientM uid) - -runHereClientM :: (HasCallStack, MonadIO m) => Endpoint -> Manager -> Client.ClientM a -> m (Either Client.ClientError a) -runHereClientM brigep mgr action = do - let env = Client.mkClientEnv mgr baseurl - baseurl = Client.BaseUrl Client.Http (cs $ brigep ^. host) (fromIntegral $ brigep ^. port) "" - liftIO $ Client.runClientM action env diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 3b3eba50b38..dc54cd254dd 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -16,7 +16,7 @@ -- with this program. If not, see . {-# OPTIONS_GHC -fno-warn-orphans #-} -module API.OAuth where +module API.OAuth (tests) where import API.Team.Util qualified as Team import Bilge @@ -122,8 +122,7 @@ tests m db b n o = do ], testGroup "oauth applications" - [ test m "list applications with account access" $ testListApplicationsWithAccountAccess b, - test m "revoke application account access" $ testRevokeApplicationAccountAccess b + [ test m "list applications with account access" $ testListApplicationsWithAccountAccess b ] ] @@ -193,10 +192,10 @@ testCreateAccessTokenSuccess opts brig = do createOAuthAccessToken' brig accessTokenRequest !!! do const 404 === statusCode const (Just "not-found") === fmap Error.label . responseJsonMaybe - k <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") + k <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.settings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") verifiedOrError <- liftIO $ verify k (unOAuthToken $ resp.accessToken) verifiedOrErrorWithotherKey <- liftIO $ verify badKey (unOAuthToken $ resp.accessToken) - let expectedDomain = domainText $ Opt.setFederationDomain $ Opt.optSettings opts + let expectedDomain = domainText opts.settings.federationDomain liftIO $ do isRight verifiedOrError @?= True isLeft verifiedOrErrorWithotherKey @?= True @@ -248,7 +247,7 @@ testCreateAccessTokenWrongUrl brig = do testCreateAccessTokenExpiredCode :: Opt.Opts -> Brig -> Http () testCreateAccessTokenExpiredCode opts brig = - withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthAuthorizationCodeExpirationTimeSecsInternal ?~ 1) $ do + withSettingsOverrides (opts & Opt.settingsLens . Opt.oAuthAuthorizationCodeExpirationTimeSecsInternalLens ?~ 1) $ do uid <- randomId let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [WriteConversations, WriteConversationsCode] @@ -298,14 +297,14 @@ testCreateAccessTokenWrongCodeVerifier brig = do testGetOAuthClientInfoAccessDeniedWhenDisabled :: Opt.Opts -> Brig -> Http () testGetOAuthClientInfoAccessDeniedWhenDisabled opts brig = - withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthEnabledInternal ?~ False) $ do + withSettingsOverrides (opts & Opt.settingsLens . Opt.oAuthEnabledInternalLens ?~ False) $ do cid <- randomId uid <- randomId getOAuthClientInfo' brig uid cid !!! assertAccessDenied testCreateCodeOAuthClientAccessDeniedWhenDisabled :: Opt.Opts -> Brig -> Http () testCreateCodeOAuthClientAccessDeniedWhenDisabled opts brig = - withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthEnabledInternal ?~ False) $ do + withSettingsOverrides (opts & Opt.settingsLens . Opt.oAuthEnabledInternalLens ?~ False) $ do cid <- randomId uid <- randomId state <- UUID.toText <$> liftIO nextRandom @@ -319,7 +318,7 @@ testCreateCodeOAuthClientAccessDeniedWhenDisabled opts brig = testCreateAccessTokenAccessDeniedWhenDisabled :: Opt.Opts -> Brig -> Http () testCreateAccessTokenAccessDeniedWhenDisabled opts brig = - withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthEnabledInternal ?~ False) $ do + withSettingsOverrides (opts & Opt.settingsLens . Opt.oAuthEnabledInternalLens ?~ False) $ do cid <- randomId let code = OAuthAuthorizationCode $ encodeBase16 "eb32eb9e2aa36c081c89067dddf81bce83c1c57e0b74cfb14c9f026f145f2b1f" let url = mkUrl "https://example.com" @@ -334,13 +333,13 @@ testRefreshAccessTokenAccessDeniedWhenDisabled opts brig = do (cid, code) <- generateOAuthClientAndAuthorizationCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest - withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthEnabledInternal ?~ False) $ do + withSettingsOverrides (opts & Opt.settingsLens . Opt.oAuthEnabledInternalLens ?~ False) $ do let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid resp.refreshToken refreshOAuthAccessToken' brig refreshAccessTokenRequest !!! assertAccessDenied testRegisterOAuthClientAccessDeniedWhenDisabled :: Opt.Opts -> Brig -> Http () testRegisterOAuthClientAccessDeniedWhenDisabled opts brig = - withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthEnabledInternal ?~ False) $ do + withSettingsOverrides (opts & Opt.settingsLens . Opt.oAuthEnabledInternalLens ?~ False) $ do let newOAuthClient = newOAuthClientRequestBody "E Corp" "https://example.com" registerNewOAuthClient' brig newOAuthClient !!! assertAccessDenied @@ -413,7 +412,7 @@ testAccessResourceInvalidSignature opts brig nginz = do (cid, code) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest - key <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") + key <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.settings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") claimSet <- fromRight (error "token invalid") <$> liftIO (verify key (unOAuthToken $ resp.accessToken)) tokenSignedWithotherKey <- signAccessToken badKey claimSet get (nginz . paths ["self"] . authHeader (OAuthToken tokenSignedWithotherKey)) !!! do @@ -422,9 +421,9 @@ testAccessResourceInvalidSignature opts brig nginz = do testRefreshTokenMaxActiveTokens :: Opts -> C.ClientState -> Brig -> Http () testRefreshTokenMaxActiveTokens opts db brig = - withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthMaxActiveRefreshTokensInternal ?~ 2) $ do + withSettingsOverrides (opts & Opt.settingsLens . Opt.oAuthMaxActiveRefreshTokensInternalLens ?~ 2) $ do uid <- randomId - jwk <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") + jwk <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.settings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [WriteConversations, WriteConversationsCode] let delayOneSec = @@ -439,7 +438,7 @@ testRefreshTokenMaxActiveTokens opts db brig = resp <- createOAuthAccessToken brig accessTokenRequest rid <- extractRefreshTokenId jwk resp.refreshToken tokens <- C.runClient db (lookupOAuthRefreshTokens uid) - liftIO $ assertBool testMsg $ [rid] `hasSameElems` (refreshTokenId <$> tokens) + liftIO $ assertBool testMsg $ [rid] `hasSameElems` ((.refreshTokenId) <$> tokens) pure (rid, cid, secret) delayOneSec rid2 <- do @@ -449,7 +448,7 @@ testRefreshTokenMaxActiveTokens opts db brig = resp <- createOAuthAccessToken brig accessTokenRequest rid <- extractRefreshTokenId jwk resp.refreshToken tokens <- C.runClient db (lookupOAuthRefreshTokens uid) - liftIO $ assertBool testMsg $ [rid1, rid] `hasSameElems` (refreshTokenId <$> tokens) + liftIO $ assertBool testMsg $ [rid1, rid] `hasSameElems` ((.refreshTokenId) <$> tokens) pure rid delayOneSec rid3 <- do @@ -460,7 +459,7 @@ testRefreshTokenMaxActiveTokens opts db brig = rid <- extractRefreshTokenId jwk resp.refreshToken recoverN 3 $ do tokens <- C.runClient db (lookupOAuthRefreshTokens uid) - liftIO $ assertBool testMsg $ [rid2, rid] `hasSameElems` (refreshTokenId <$> tokens) + liftIO $ assertBool testMsg $ [rid2, rid] `hasSameElems` ((.refreshTokenId) <$> tokens) pure rid delayOneSec do @@ -470,7 +469,7 @@ testRefreshTokenMaxActiveTokens opts db brig = resp <- createOAuthAccessToken brig accessTokenRequest rid <- extractRefreshTokenId jwk resp.refreshToken tokens <- C.runClient db (lookupOAuthRefreshTokens uid) - liftIO $ assertBool testMsg $ [rid3, rid] `hasSameElems` (refreshTokenId <$> tokens) + liftIO $ assertBool testMsg $ [rid3, rid] `hasSameElems` ((.refreshTokenId) <$> tokens) where extractRefreshTokenId :: (MonadIO m) => JWK -> OAuthRefreshToken -> m OAuthRefreshTokenId extractRefreshTokenId jwk rt = do @@ -502,7 +501,7 @@ testRefreshTokenWrongSignature opts brig = do (cid, code) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest - key <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") + key <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.settings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") badRefreshToken <- liftIO $ do claims <- verifyRefreshToken key (unOAuthToken $ resp.refreshToken) OAuthToken <$> signRefreshToken badKey claims @@ -517,7 +516,7 @@ testRefreshTokenNoTokenId opts brig = do let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] (cid, _) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl - key <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") + key <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.settings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") badRefreshToken <- liftIO $ OAuthToken <$> signRefreshToken key emptyClaimsSet let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid badRefreshToken refreshOAuthAccessToken' brig refreshAccessTokenRequest !!! do @@ -532,7 +531,7 @@ testRefreshTokenNonExistingId opts brig = do (cid, code) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest - key <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") + key <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.settings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") badRefreshToken <- liftIO $ OAuthToken <$> do @@ -576,7 +575,7 @@ testRefreshTokenWrongGrantType brig = do testRefreshTokenExpiredToken :: Opts -> Brig -> Http () testRefreshTokenExpiredToken opts brig = -- overriding settings and set refresh token to expire in 2 seconds - withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthRefreshTokenExpirationTimeSecsInternal ?~ 2) $ do + withSettingsOverrides (opts & Opt.settingsLens . Opt.oAuthRefreshTokenExpirationTimeSecsInternalLens ?~ 2) $ do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] @@ -609,14 +608,14 @@ testListApplicationsWithAccountAccess brig = do bob <- createUser "bob" brig do apps <- listOAuthApplications brig (User.userId alice) - liftIO $ assertEqual "apps" 0 (length apps) + liftIO $ apps @?= [] void $ createOAuthApplicationWithAccountAccess brig (User.userId alice) void $ createOAuthApplicationWithAccountAccess brig (User.userId alice) do aliceApps <- listOAuthApplications brig (User.userId alice) liftIO $ assertEqual "apps" 2 (length aliceApps) bobsApps <- listOAuthApplications brig (User.userId bob) - liftIO $ assertEqual "apps" 0 (length bobsApps) + liftIO $ bobsApps @?= [] void $ createOAuthApplicationWithAccountAccess brig (User.userId alice) void $ createOAuthApplicationWithAccountAccess brig (User.userId bob) do @@ -625,31 +624,6 @@ testListApplicationsWithAccountAccess brig = do bobsApps <- listOAuthApplications brig (User.userId bob) liftIO $ assertEqual "apps" 1 (length bobsApps) -testRevokeApplicationAccountAccess :: Brig -> Http () -testRevokeApplicationAccountAccess brig = do - user <- createUser "alice" brig - do - apps <- listOAuthApplications brig (User.userId user) - liftIO $ assertEqual "apps" 0 (length apps) - for_ [1 .. 3 :: Int] $ const $ createOAuthApplicationWithAccountAccess brig (User.userId user) - cids <- fmap applicationId <$> listOAuthApplications brig (User.userId user) - liftIO $ assertEqual "apps" 3 (length cids) - case cids of - [cid1, cid2, cid3] -> do - revokeOAuthApplicationAccess brig (User.userId user) cid1 - do - apps <- listOAuthApplications brig (User.userId user) - liftIO $ assertEqual "apps" 2 (length apps) - revokeOAuthApplicationAccess brig (User.userId user) cid2 - do - apps <- listOAuthApplications brig (User.userId user) - liftIO $ assertEqual "apps" 1 (length apps) - revokeOAuthApplicationAccess brig (User.userId user) cid3 - do - apps <- listOAuthApplications brig (User.userId user) - liftIO $ assertEqual "apps" 0 (length apps) - _ -> liftIO $ assertFailure "unexpected number of apps" - testWriteConversationsSuccessNginz :: Brig -> Nginz -> Http () testWriteConversationsSuccessNginz brig nginz = do (uid, tid) <- Team.createUserWithTeam brig @@ -786,14 +760,6 @@ listOAuthApplications :: (MonadIO m, MonadHttp m, MonadCatch m, HasCallStack) => listOAuthApplications brig uid = responseJsonError =<< listOAuthApplications' brig uid Brig -> UserId -> OAuthClientId -> m ResponseLBS -revokeOAuthApplicationAccess' brig uid cid = - delete (brig . paths ["oauth", "applications", toByteString' cid] . zUser uid) - -revokeOAuthApplicationAccess :: (MonadIO m, MonadHttp m, MonadCatch m, HasCallStack) => Brig -> UserId -> OAuthClientId -> m () -revokeOAuthApplicationAccess brig uid cid = - void $ revokeOAuthApplicationAccess' brig uid cid Brig -> UserId -> OAuthScopes -> RedirectUrl -> m (OAuthClientId, OAuthAuthorizationCode) generateOAuthClientAndAuthorizationCode = generateOAuthClientAndAuthorizationCode' challenge diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index 0d2aad8a5db..67ab4913344 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -133,8 +133,6 @@ tests dom conf p db b c g n = do "service whitelist" [ test p "search permissions" $ testWhitelistSearchPermissions conf db b g, - test p "update permissions" $ - testWhitelistUpdatePermissions conf db b g, test p "basic functionality" $ testWhitelistBasic conf db b g, test p "search" $ testSearchWhitelist conf db b g, @@ -263,7 +261,7 @@ testPasswordResetProvider db brig = do loginProvider brig email newPw !!! const 200 === statusCode where - resetPw :: PlainTextPassword6 -> Email -> Http ResponseLBS + resetPw :: PlainTextPassword6 -> EmailAddress -> Http ResponseLBS resetPw newPw email = do -- Get the code directly from the DB let gen = mkVerificationCodeGen email @@ -879,32 +877,6 @@ testWhitelistSearchPermissions _config _db brig galley = do listTeamServiceProfilesByPrefix brig member tid Nothing True 20 !!! const 200 === statusCode -testWhitelistUpdatePermissions :: Config -> DB.ClientState -> Brig -> Galley -> Http () -testWhitelistUpdatePermissions config db brig galley = do - -- Create a team - (owner, tid) <- Team.createUserWithTeam brig - -- Create a team admin - let Just adminPermissions = newPermissions serviceWhitelistPermissions mempty - admin <- userId <$> Team.createTeamMember brig galley owner tid adminPermissions - -- Create a service - pid <- providerId <$> randomProvider db brig - new <- defNewService config - sid <- serviceId <$> addGetService brig pid new - enableService brig pid sid - -- Check that a random user can't add it to the whitelist - _uid <- userId <$> randomUser brig - updateServiceWhitelist brig _uid tid (UpdateServiceWhitelist pid sid True) !!! do - const 403 === statusCode - const (Just "insufficient-permissions") === fmap Error.label . responseJsonMaybe - -- Check that a member who's not a team admin also can't add it to the whitelist - _uid <- userId <$> Team.createTeamMember brig galley owner tid noPermissions - updateServiceWhitelist brig _uid tid (UpdateServiceWhitelist pid sid True) !!! do - const 403 === statusCode - const (Just "insufficient-permissions") === fmap Error.label . responseJsonMaybe - -- Check that a team admin can add and remove from the whitelist - whitelistService brig admin tid pid sid - dewhitelistService brig admin tid pid sid - testSearchWhitelist :: Config -> DB.ClientState -> Brig -> Galley -> Http () testSearchWhitelist config db brig galley = do -- Create a team, a team owner, and a team member with no permissions @@ -1109,7 +1081,7 @@ registerProvider brig new = getProviderActivationCodeInternal :: Brig -> - Email -> + EmailAddress -> Http ResponseLBS getProviderActivationCodeInternal brig email = get $ @@ -1131,7 +1103,7 @@ activateProvider brig key val = loginProvider :: Brig -> - Email -> + EmailAddress -> PlainTextPassword6 -> Http ResponseLBS loginProvider brig email pw = @@ -1425,7 +1397,7 @@ addBot :: addBot brig uid pid sid cid = post $ brig - . paths ["conversations", toByteString' cid, "bots"] + . paths ["bot", "conversations", toByteString' cid] . header "Z-Type" "access" . header "Z-User" (toByteString' uid) . header "Z-Connection" "conn" @@ -1441,7 +1413,7 @@ removeBot :: removeBot brig uid cid bid = delete $ brig - . paths ["conversations", toByteString' cid, "bots", toByteString' bid] + . paths ["bot", "conversations", toByteString' cid, toByteString' bid] . header "Z-Type" "access" . header "Z-User" (toByteString' uid) . header "Z-Connection" "conn" @@ -1580,7 +1552,7 @@ enabled2ndFaForTeamInternal galley tid = do ( galley . paths ["i", "teams", toByteString' tid, "features", featureNameBS @Public.SndFactorPasswordChallengeConfig] . contentJson - . Bilge.json (Public.WithStatusNoLock Public.FeatureStatusEnabled Public.SndFactorPasswordChallengeConfig Public.FeatureTTLUnlimited) + . Bilge.json (Public.Feature Public.FeatureStatusEnabled Public.SndFactorPasswordChallengeConfig) ) !!! const 200 === statusCode @@ -1836,7 +1808,7 @@ defNewService config = liftIO $ do newServiceTags = defServiceTags } -defNewProvider :: Email -> NewProvider +defNewProvider :: EmailAddress -> NewProvider defNewProvider email = NewProvider { newProviderEmail = email, diff --git a/services/brig/test/integration/API/Search.hs b/services/brig/test/integration/API/Search.hs index 54111e832f5..14832d5e377 100644 --- a/services/brig/test/integration/API/Search.hs +++ b/services/brig/test/integration/API/Search.hs @@ -520,7 +520,7 @@ testSearchSameTeamOnly brig opts = do nonTeamMember <- setRandomHandle brig nonTeamMember' (_, _, [teamMember]) <- createPopulatedBindingTeam brig 1 refreshIndex brig - let newOpts = opts & Opt.optionSettings . Opt.searchSameTeamOnly ?~ True + let newOpts = opts & Opt.settingsLens . Opt.searchSameTeamOnlyLens ?~ True withSettingsOverrides newOpts $ do assertCan'tFind brig (userId teamMember) (userQualifiedId nonTeamMember) (fromName (userDisplayName nonTeamMember)) let nonTeamMemberHandle = fromMaybe (error "nonTeamMember must have a handle") (userHandle nonTeamMember) @@ -613,8 +613,8 @@ testMigrationToNewIndex opts brig = do withOldESProxy opts $ \oldESUrl oldESIndex -> do let optsOldIndex = opts - & Opt.elasticsearchL . Opt.indexL .~ (ES.IndexName oldESIndex) - & Opt.elasticsearchL . Opt.urlL .~ (ES.Server oldESUrl) + & Opt.elasticsearchLens . Opt.indexLens .~ (ES.IndexName oldESIndex) + & Opt.elasticsearchLens . Opt.urlLens .~ (ES.Server oldESUrl) -- Phase 1: Using old index only (phase1NonTeamUser, teamOwner, phase1TeamUser1, phase1TeamUser2, tid) <- withSettingsOverrides optsOldIndex $ do nonTeamUser <- randomUser brig @@ -624,10 +624,10 @@ testMigrationToNewIndex opts brig = do -- Phase 2: Using old index for search, writing to both indices, migrations have not run let phase2OptsWhile = optsOldIndex - & Opt.elasticsearchL . Opt.additionalWriteIndexL ?~ (opts ^. Opt.elasticsearchL . Opt.indexL) - & Opt.elasticsearchL . Opt.additionalWriteIndexUrlL ?~ (opts ^. Opt.elasticsearchL . Opt.urlL) - & Opt.elasticsearchL . Opt.additionalCaCertL .~ (opts ^. Opt.elasticsearchL . Opt.caCertL) - & Opt.elasticsearchL . Opt.additionalInsecureSkipVerifyTlsL .~ (opts ^. Opt.elasticsearchL . Opt.insecureSkipVerifyTlsL) + & Opt.elasticsearchLens . Opt.additionalWriteIndexLens ?~ (opts ^. Opt.elasticsearchLens . Opt.indexLens) + & Opt.elasticsearchLens . Opt.additionalWriteIndexUrlLens ?~ (opts ^. Opt.elasticsearchLens . Opt.urlLens) + & Opt.elasticsearchLens . Opt.additionalCaCertLens .~ (opts ^. Opt.elasticsearchLens . Opt.caCertLens) + & Opt.elasticsearchLens . Opt.additionalInsecureSkipVerifyTlsLens .~ (opts ^. Opt.elasticsearchLens . Opt.insecureSkipVerifyTlsLens) (phase2NonTeamUser, phase2TeamUser) <- withSettingsOverrides phase2OptsWhile $ do phase2NonTeamUser <- randomUser brig phase2TeamUser <- inviteAndRegisterUser teamOwner tid brig @@ -652,7 +652,7 @@ testMigrationToNewIndex opts brig = do assertCanFindByName brig phase1TeamUser1 phase2TeamUser -- Run Migrations - let newIndexName = opts ^. Opt.elasticsearchL . Opt.indexL + let newIndexName = opts ^. Opt.elasticsearchLens . Opt.indexLens taskNodeId <- assertRight =<< runBH opts (ES.reindexAsync $ ES.mkReindexRequest (ES.IndexName oldESIndex) newIndexName) runBH opts $ waitForTaskToComplete @ES.ReindexResponse taskNodeId @@ -746,14 +746,14 @@ withOldIndex :: (MonadIO m, HasCallStack) => Opt.Opts -> WaiTest.Session a -> m withOldIndex opts f = do indexName <- randomHandle createIndexWithMapping opts indexName oldMapping - let newOpts = opts & Opt.elasticsearchL . Opt.indexL .~ (ES.IndexName indexName) + let newOpts = opts & Opt.elasticsearchLens . Opt.indexLens .~ (ES.IndexName indexName) withSettingsOverrides newOpts f <* deleteIndex opts indexName optsForOldIndex :: (MonadIO m, HasCallStack) => Opt.Opts -> m (Opt.Opts, Text) optsForOldIndex opts = do indexName <- randomHandle createIndexWithMapping opts indexName oldMapping - pure (opts & Opt.elasticsearchL . Opt.indexL .~ (ES.IndexName indexName), indexName) + pure (opts & Opt.elasticsearchLens . Opt.indexLens .~ (ES.IndexName indexName), indexName) createIndexWithMapping :: (MonadIO m, HasCallStack) => Opt.Opts -> Text -> Value -> m () createIndexWithMapping opts name val = do @@ -773,7 +773,7 @@ deleteIndex opts name = do runBH :: (MonadIO m, HasCallStack) => Opt.Opts -> ES.BH m a -> m a runBH opts action = do - let (ES.Server esURL) = opts ^. Opt.elasticsearchL . Opt.urlL + let (ES.Server esURL) = opts ^. Opt.elasticsearchLens . Opt.urlLens mgr <- liftIO $ initHttpManagerWithTLSConfig opts.elasticsearch.insecureSkipVerifyTls opts.elasticsearch.caCert let bEnv = mkBHEnv esURL mgr ES.runBH bEnv action diff --git a/services/brig/test/integration/API/Search/Util.hs b/services/brig/test/integration/API/Search/Util.hs index 9f8c83b34e0..3dc5598a871 100644 --- a/services/brig/test/integration/API/Search/Util.hs +++ b/services/brig/test/integration/API/Search/Util.hs @@ -102,13 +102,6 @@ assertCan'tFind brig self expected q = do assertBool ("User shouldn't be present in results for query: " <> show q) $ expected `notElem` map contactQualifiedId r -assertCan'tFindWithDomain :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> UserId -> Qualified UserId -> Text -> Domain -> m () -assertCan'tFindWithDomain brig self expected q domain = do - r <- searchResults <$> executeSearchWithDomain brig self q domain - liftIO $ do - assertBool ("User shouldn't be present in results for query: " <> show q) $ - expected `notElem` map contactQualifiedId r - executeTeamUserSearch :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> diff --git a/services/brig/test/integration/API/Settings.hs b/services/brig/test/integration/API/Settings.hs index 1d350b08868..3647a4463bc 100644 --- a/services/brig/test/integration/API/Settings.hs +++ b/services/brig/test/integration/API/Settings.hs @@ -107,7 +107,7 @@ testUsersEmailVisibleIffExpected opts brig galley viewingUserIs visibilitySettin let uids = C8.intercalate "," $ toByteString' <$> [userId userA, userId userB, userId nonTeamUser] - expected :: Set (Maybe UserId, Maybe Email) + expected :: Set (Maybe UserId, Maybe EmailAddress) expected = Set.fromList [ ( Just $ userId userA, @@ -126,7 +126,7 @@ testUsersEmailVisibleIffExpected opts brig galley viewingUserIs visibilitySettin else Nothing ) ] - let newOpts = opts & Opt.optionSettings . Opt.emailVisibility .~ visibilitySetting + let newOpts = opts & Opt.settingsLens . Opt.emailVisibilityLens .~ visibilitySetting withSettingsOverrides newOpts $ do get (apiVersion "v1" . brig . zUser viewerId . path "users" . queryItem "ids" uids) !!! do const 200 === statusCode @@ -137,7 +137,7 @@ testUsersEmailVisibleIffExpected opts brig galley viewingUserIs visibilitySettin testGetUserEmailShowsEmailsIffExpected :: Opts -> Brig -> Galley -> ViewingUserIs -> EmailVisibilityConfig -> Http () testGetUserEmailShowsEmailsIffExpected opts brig galley viewingUserIs visibilitySetting = do (viewerId, userA, userB, nonTeamUser) <- setup brig galley viewingUserIs - let expectations :: [(UserId, Maybe Email)] + let expectations :: [(UserId, Maybe EmailAddress)] expectations = [ ( userId userA, if expectEmailVisible visibilitySetting viewingUserIs SameTeam @@ -155,14 +155,14 @@ testGetUserEmailShowsEmailsIffExpected opts brig galley viewingUserIs visibility else Nothing ) ] - let newOpts = opts & Opt.optionSettings . Opt.emailVisibility .~ visibilitySetting + let newOpts = opts & Opt.settingsLens . Opt.emailVisibilityLens .~ visibilitySetting withSettingsOverrides newOpts $ do forM_ expectations $ \(uid, expectedEmail) -> get (apiVersion "v1" . brig . zUser viewerId . paths ["users", toByteString' uid]) !!! do const 200 === statusCode const expectedEmail === emailResult where - emailResult :: Response (Maybe LByteString) -> Maybe Email + emailResult :: Response (Maybe LByteString) -> Maybe EmailAddress emailResult r = responseJsonMaybe r >>= jsonField "email" setup :: Brig -> Galley -> ViewingUserIs -> Http (UserId, User, User, User) diff --git a/services/brig/test/integration/API/SystemSettings.hs b/services/brig/test/integration/API/SystemSettings.hs index 40b20c0606f..265d2d43dde 100644 --- a/services/brig/test/integration/API/SystemSettings.hs +++ b/services/brig/test/integration/API/SystemSettings.hs @@ -49,7 +49,7 @@ testGetSettings opts = liftIO $ do where expectResultForSetting :: Maybe Bool -> Bool -> IO () expectResultForSetting restrictUserCreationSetting expectedRes = do - let newOpts = opts & (optionSettings . restrictUserCreation) .~ restrictUserCreationSetting + let newOpts = opts & (settingsLens . restrictUserCreationLens) .~ restrictUserCreationSetting -- Run call in `WaiTest.Session` with an adjusted brig `Application`. I.e. -- the response is created by running the brig `Application` (with -- modified options) directly on the `Request`. No real HTTP request is @@ -73,7 +73,7 @@ testGetSettingsInternal opts = liftIO $ do where expectResultForEnableMls :: UserId -> Maybe Bool -> Bool -> IO () expectResultForEnableMls uid setEnableMlsValue expectedRes = do - let newOpts = opts & (optionSettings . enableMLS) .~ setEnableMlsValue + let newOpts = opts & (settingsLens . enableMLSLens) .~ setEnableMlsValue -- Run call in `WaiTest.Session` with an adjusted brig `Application`. I.e. -- the response is created by running the brig `Application` (with -- modified options) directly on the `Request`. No real HTTP request is diff --git a/services/brig/test/integration/API/Team.hs b/services/brig/test/integration/API/Team.hs index 6eafdb1ed9c..d4599cc0fc0 100644 --- a/services/brig/test/integration/API/Team.hs +++ b/services/brig/test/integration/API/Team.hs @@ -43,8 +43,7 @@ import Data.LegalHold (UserLegalHoldStatus (UserLegalHoldDisabled)) import Data.String.Conversions (cs) import Data.Text qualified as Text import Data.Text.Ascii qualified as Ascii -import Data.Text.Encoding (encodeUtf8) -import Data.Time (addUTCTime, getCurrentTime) +import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.UUID qualified as UUID (fromString) import Data.UUID.V4 qualified as UUID import Imports @@ -60,6 +59,7 @@ import URI.ByteString import UnliftIO.Async (mapConcurrently_, pooledForConcurrentlyN_, replicateConcurrently) import Util import Util.AWS as Util +import Util.Timeout import Web.Cookie (parseSetCookie, setCookieName) import Wire.API.Asset import Wire.API.Connection @@ -81,8 +81,8 @@ newtype TeamSizeLimit = TeamSizeLimit Word32 tests :: Opt.Opts -> Manager -> Nginz -> Brig -> Cannon -> Galley -> UserJournalWatcher -> IO TestTree tests conf m n b c g aws = do - let tl = TeamSizeLimit . Opt.setMaxTeamSize . Opt.optSettings $ conf - let it = Opt.setTeamInvitationTimeout . Opt.optSettings $ conf + let tl = TeamSizeLimit conf.settings.maxTeamSize + let it = conf.settings.teamInvitationTimeout pure $ testGroup "team" @@ -168,8 +168,8 @@ testUpdateEvents brig cannon = do inviteeEmail <- randomEmail -- invite and register Bob let invite = stdInvitationRequest inviteeEmail - inv <- responseJsonError =<< postInvitation brig tid alice invite - Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) + inv :: Invitation <- responseJsonError =<< postInvitation brig tid alice invite + Just inviteeCode <- getInvitationCode brig tid inv.invitationId rsp2 <- post ( brig @@ -187,7 +187,7 @@ testUpdateEvents brig cannon = do newAssets = Just [ImageAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "5cd81cc4-c643-4e9c-849c-c596a88c27fd"))) AssetExpiring) (Just AssetComplete)] newName = Just $ Name "Alice in Wonderland" newPic = Nothing -- Legacy - userUpdate = UserUpdate newName newPic newAssets newColId + userUpdate = UserUpdate newName Nothing newPic newAssets newColId update = RequestBodyLBS . encode $ userUpdate -- Update profile & receive notification WS.bracketRN cannon [alice, bob] $ \[aliceWS, bobWS] -> do @@ -204,35 +204,34 @@ testInvitationEmail brig = do const 201 === statusCode inv <- responseJsonError res let actualHeader = getHeader "Location" res - let expectedHeader = "/teams/" <> toByteString' tid <> "/invitations/" <> toByteString' (inInvitation inv) + let expectedHeader = "/teams/" <> toByteString' tid <> "/invitations/" <> toByteString' inv.invitationId liftIO $ do - Just inviter @=? inCreatedBy inv - tid @=? inTeam inv + Just inviter @=? inv.createdBy + tid @=? inv.team assertInvitationResponseInvariants invite inv - (isNothing . inInviteeUrl) inv @? "No invitation url expected" + (isNothing . (.inviteeUrl)) inv @? "No invitation url expected" actualHeader @?= Just expectedHeader assertInvitationResponseInvariants :: InvitationRequest -> Invitation -> Assertion assertInvitationResponseInvariants invReq inv = do - irInviteeName invReq @=? inInviteeName inv - irInviteePhone invReq @=? inInviteePhone inv - irInviteeEmail invReq @=? inInviteeEmail inv + invReq.inviteeName @=? inv.inviteeName + invReq.inviteeEmail @=? inv.inviteeEmail testGetInvitation :: Brig -> Http () testGetInvitation brig = do (inviter, tid) <- createUserWithTeam brig invite <- stdInvitationRequest <$> randomEmail inv1 <- responseJsonError =<< postInvitation brig tid inviter invite Http () testDeleteInvitation brig = do (inviter, tid) <- createUserWithTeam brig invite <- stdInvitationRequest <$> randomEmail - iid <- inInvitation <$> (responseJsonError =<< postInvitation brig tid inviter invite (toStrict . toByteString)) getQueryParam "team" resp @=? (pure . encodeUtf8 . idToText) tid getQueryParam :: ByteString -> ResponseLBS -> Maybe ByteString getQueryParam name r = do - inv <- (eitherToMaybe . responseJsonEither) r - url <- inInviteeUrl inv + inv :: Invitation <- (eitherToMaybe . responseJsonEither) r + url <- inv.inviteeUrl (lookup name . queryPairs . uriQuery) url -- | Mock the feature API because exposeInvitationURLsToTeamAdmin depends on @@ -275,11 +274,10 @@ invitationUrlGalleyMock featureStatus tid inviter (ReceivedRequest mth pth body_ && pth == ["i", "teams", Text.pack (show tid), "features", "exposeInvitationURLsToTeamAdmin"] = pure . Wai.responseLBS HTTP.status200 mempty $ encode - ( withStatus + ( LockableFeature featureStatus LockStatusUnlocked ExposeInvitationURLsToTeamAdminConfig - FeatureTTLUnlimited ) | mth == "GET" && pth == ["i", "teams", Text.pack (show tid), "members", Text.pack (show inviter)] = @@ -287,7 +285,7 @@ invitationUrlGalleyMock featureStatus tid inviter (ReceivedRequest mth pth body_ encode (mkTeamMember inviter fullPermissions Nothing UserLegalHoldDisabled) | mth == "GET" && pth == ["i", "feature-configs"] = - pure $ Wai.responseLBS HTTP.status200 mempty (encode (def @AllFeatureConfigs)) + pure $ Wai.responseLBS HTTP.status200 mempty (encode (def @AllTeamFeatures)) | otherwise = let errBody = encode . object $ @@ -311,13 +309,13 @@ testNoInvitationUrl opts brig = do Http () testInvitationEmailLookup brig = do @@ -340,6 +338,8 @@ testInvitationEmailLookupRegister brig = do email <- randomEmail (owner, tid) <- createUserWithTeam brig let invite = stdInvitationRequest email + -- This incidentally also tests that sending multiple + -- invites from the same team results in last-invite-wins scenario void $ postInvitation brig tid owner invite inv :: Invitation <- responseJsonError =<< postInvitation brig tid owner invite -- expect an invitation to be found querying with email after invite @@ -359,7 +359,7 @@ testInvitationEmailLookupNginz brig nginz = do -- expect an invitation to be found querying with email after invite headInvitationByEmail nginz email 200 -headInvitationByEmail :: (Request -> Request) -> Email -> Int -> Http () +headInvitationByEmail :: (Request -> Request) -> EmailAddress -> Int -> Http () headInvitationByEmail service email expectedCode = Bilge.head (service . path "/teams/invitations/by-email" . contentJson . queryItem "email" (toByteString' email)) !!! const expectedCode === statusCode @@ -372,16 +372,16 @@ testInvitationTooManyPending opts brig (TeamSizeLimit limit) = do -- If this test takes longer to run than `team-invitation-timeout`, then some of the -- invitations have likely expired already and this test will actually _fail_ -- therefore we increase the timeout from default 10 to 300 seconds - let longerTimeout = opts {Opt.optSettings = (Opt.optSettings opts) {Opt.setTeamInvitationTimeout = 300}} + let longerTimeout = opts {Opt.settings = opts.settings {Opt.teamInvitationTimeout = 300}} withSettingsOverrides longerTimeout $ do forM_ emails $ postInvitation brig tid inviter . stdInvitationRequest postInvitation brig tid inviter (stdInvitationRequest email) !!! do const 403 === statusCode const (Just "too-many-team-invitations") === fmap Error.label . responseJsonMaybe -registerInvite :: Brig -> TeamId -> Invitation -> Email -> Http UserId +registerInvite :: Brig -> TeamId -> Invitation -> EmailAddress -> Http UserId registerInvite brig tid inv invemail = do - Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) + Just inviteeCode <- getInvitationCode brig tid inv.invitationId rsp <- post ( brig @@ -430,19 +430,19 @@ testInvitationRoles brig galley = do testInvitationEmailAccepted :: Brig -> Galley -> Http () testInvitationEmailAccepted brig galley = do - inviteeEmail <- randomEmail - let invite = stdInvitationRequest inviteeEmail - void $ createAndVerifyInvitation (accept (irInviteeEmail invite)) invite brig galley + email <- randomEmail + let invite = stdInvitationRequest email + void $ createAndVerifyInvitation (accept invite.inviteeEmail) invite brig galley -- | Related: 'testDomainsBlockedForRegistration'. When we remove the customer-specific -- extension of domain blocking, this test will fail to compile (so you will know it's time to -- remove it). testInvitationEmailAcceptedInBlockedDomain :: Opt.Opts -> Brig -> Galley -> Http () testInvitationEmailAcceptedInBlockedDomain opts brig galley = do - inviteeEmail :: Email <- randomEmail - let invite = stdInvitationRequest inviteeEmail - replacementBrigApp = withDomainsBlockedForRegistration opts [emailDomain inviteeEmail] - void $ createAndVerifyInvitation' (Just replacementBrigApp) (accept (irInviteeEmail invite)) invite brig galley + email :: EmailAddress <- randomEmail + let invite = stdInvitationRequest email + replacementBrigApp = withDomainsBlockedForRegistration opts [decodeUtf8 $ domainPart email] + void $ createAndVerifyInvitation' (Just replacementBrigApp) (accept invite.inviteeEmail) invite brig galley -- | FUTUREWORK: this is an alternative helper to 'createPopulatedBindingTeam'. it has been -- added concurrently, and the two should probably be consolidated. @@ -485,9 +485,9 @@ createAndVerifyInvitation' replacementBrigApp acceptFn invite brig galley = do ) => m' (Maybe (UserId, UTCTimeMillis), Invitation, UserId, ResponseLBS) invitationHandshake = do - inv <- responseJsonError =<< postInvitation brig tid inviter invite - let invmeta = Just (inviter, inCreatedAt inv) - Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) + inv :: Invitation <- responseJsonError =<< postInvitation brig tid inviter invite + let invmeta = Just (inviter, inv.createdAt) + Just inviteeCode <- getInvitationCode brig tid inv.invitationId Just invitation <- getInvitationInfo brig inviteeCode rsp2 <- post @@ -615,9 +615,8 @@ testInvitationCodeExists brig = do (uid, tid) <- createUserWithTeam brig let invite email = stdInvitationRequest email email <- randomEmail - rsp <- postInvitation brig tid uid (invite email) responseJsonMaybe rsp - Just invCode <- getInvitationCode brig tid invId + inv :: Invitation <- responseJsonError =<< postInvitation brig tid uid (invite email) + EmailAddress -> Maybe InvitationCode -> - Maybe BindingNewTeam -> + Maybe NewTeam -> Maybe InvitationCode -> HttpT IO (Response (Maybe LByteString)) req e c t i = @@ -698,8 +697,8 @@ testInvitationTooManyMembers brig galley (TeamSizeLimit limit) = do SearchUtil.refreshIndex brig let invite email = stdInvitationRequest email email <- randomEmail - inv <- responseJsonError =<< postInvitation brig tid creator (invite email) - Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) + inv :: Invitation <- responseJsonError =<< postInvitation brig tid creator (invite email) + Just inviteeCode <- getInvitationCode brig tid inv.invitationId post ( brig . path "/register" @@ -716,7 +715,7 @@ testInvitationPaging opts brig = do (uid, tid) <- createUserWithTeam brig let total = 5 invite email = stdInvitationRequest email - longerTimeout = opts {Opt.optSettings = (Opt.optSettings opts) {Opt.setTeamInvitationTimeout = 300}} + longerTimeout = opts {Opt.settings = opts.settings {Opt.teamInvitationTimeout = 300}} emails <- withSettingsOverrides longerTimeout $ replicateM total $ do @@ -733,7 +732,7 @@ testInvitationPaging opts brig = do === statusCode (invs, more) <- (ilInvitations &&& ilHasMore) <$> responseJsonError r if more - then (invs :) <$> getPages (count + step) (fmap inInvitation . listToMaybe . reverse $ invs) step + then (invs :) <$> getPages (count + step) (fmap (.invitationId) . listToMaybe . reverse $ invs) step else pure [invs] let checkSize :: (HasCallStack) => Int -> [Int] -> Http () checkSize pageSize expectedSizes = @@ -742,13 +741,13 @@ testInvitationPaging opts brig = do mapM_ validateInv $ concat invss validateInv :: Invitation -> Assertion validateInv inv = do - assertEqual "tid" tid (inTeam inv) - assertBool "email" (inInviteeEmail inv `elem` emails) + assertEqual "tid" tid (inv.team) + assertBool "email" (inv.inviteeEmail `elem` emails) -- (the output list is not ordered chronologically and emails are unique, so we just -- check whether the email is one of the valid ones.) - assertBool "timestamp" (inCreatedAt inv > before && inCreatedAt inv < after1ms) - assertEqual "uid" (Just uid) (inCreatedBy inv) - -- not checked: @inInvitation inv :: InvitationId@ + assertBool "timestamp" (inv.createdAt > before && inv.createdAt < after1ms) + assertEqual "uid" (Just uid) (inv.createdBy) + -- not checked: @invitation inv :: InvitationId@ checkSize 2 [2, 2, 1] checkSize total [total] @@ -760,7 +759,7 @@ testInvitationInfo brig = do (uid, tid) <- createUserWithTeam brig let invite = stdInvitationRequest email inv <- responseJsonError =<< postInvitation brig tid uid invite - Just invCode <- getInvitationCode brig tid (inInvitation inv) + Just invCode <- getInvitationCode brig tid inv.invitationId Just invitation <- getInvitationInfo brig invCode liftIO $ assertEqual "Invitations differ" inv invitation @@ -771,15 +770,15 @@ testInvitationInfoBadCode brig = do get (brig . path ("/teams/invitations/info?code=" <> icode)) !!! const 400 === statusCode -testInvitationInfoExpired :: Brig -> Opt.Timeout -> Http () +testInvitationInfoExpired :: Brig -> Timeout -> Http () testInvitationInfoExpired brig timeout = do email <- randomEmail (uid, tid) <- createUserWithTeam brig let invite = stdInvitationRequest email - inv <- responseJsonError =<< postInvitation brig tid uid invite + inv :: Invitation <- responseJsonError =<< postInvitation brig tid uid invite -- Note: This value must be larger than the option passed as `team-invitation-timeout` - awaitExpiry (round timeout + 5) tid (inInvitation inv) - getCode tid (inInvitation inv) !!! const 400 === statusCode + awaitExpiry (round timeout + 5) tid inv.invitationId + getCode tid inv.invitationId !!! const 400 === statusCode headInvitationByEmail brig email 404 where getCode t i = @@ -803,8 +802,8 @@ testSuspendTeam brig = do (inviter, tid) <- createUserWithTeam brig -- invite and register invitee let invite = stdInvitationRequest inviteeEmail - inv <- responseJsonError =<< postInvitation brig tid inviter invite - Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) + inv :: Invitation <- responseJsonError =<< postInvitation brig tid inviter invite + Just inviteeCode <- getInvitationCode brig tid inv.invitationId rsp2 <- post ( brig @@ -817,8 +816,8 @@ testSuspendTeam brig = do -- invite invitee2 (don't register) let invite2 = stdInvitationRequest inviteeEmail2 - inv2 <- responseJsonError =<< postInvitation brig tid inviter invite2 - Just _ <- getInvitationCode brig tid (inInvitation inv2) + inv2 :: Invitation <- responseJsonError =<< postInvitation brig tid inviter invite2 + Just _ <- getInvitationCode brig tid inv2.invitationId -- suspend team suspendTeam brig tid !!! const 200 === statusCode -- login fails @@ -828,7 +827,7 @@ testSuspendTeam brig = do -- check status chkStatus brig inviter Suspended chkStatus brig invitee Suspended - assertNoInvitationCode brig tid (inInvitation inv2) + assertNoInvitationCode brig tid inv2.invitationId -- unsuspend unsuspendTeam brig tid !!! const 200 === statusCode chkStatus brig inviter Active diff --git a/services/brig/test/integration/API/Team/Util.hs b/services/brig/test/integration/API/Team/Util.hs index 61ab960962f..12258c5dbd5 100644 --- a/services/brig/test/integration/API/Team/Util.hs +++ b/services/brig/test/integration/API/Team/Util.hs @@ -90,11 +90,11 @@ createPopulatedBindingTeamWithNames brig names = do invitees <- forM names $ \name -> do inviteeEmail <- randomEmail let invite = stdInvitationRequest inviteeEmail - inv <- + inv :: Invitation <- responseJsonError =<< postInvitation brig tid (userId inviter) invite Galley -> Http TeamId -createTeam u galley = do - tid <- randomId - r <- - put - ( galley - . paths ["i", "teams", toByteString' tid] - . contentJson - . zAuthAccess u "conn" - . expect2xx - . lbytes (encode newTeam) - ) - maybe (error "invalid team id") pure $ - fromByteString $ - getHeader' "Location" r - -- | Create user and binding team. -- -- NB: the created user is the team owner. @@ -183,11 +167,11 @@ inviteAndRegisterUser :: inviteAndRegisterUser u tid brig = do inviteeEmail <- randomEmail let invite = stdInvitationRequest inviteeEmail - inv <- + inv :: Invitation <- responseJsonError =<< postInvitation brig tid u invite TeamId -> FeatureStatus -> Galley -> Http () putLegalHoldEnabled tid enabled g = do @@ -282,7 +266,7 @@ putLegalHoldEnabled tid enabled g = do g . paths ["i", "teams", toByteString' tid, "features", "legalhold"] . contentJson - . lbytes (encode (Public.WithStatusNoLock enabled Public.LegalholdConfig Public.FeatureTTLUnlimited)) + . lbytes (encode (Public.Feature enabled Public.LegalholdConfig)) . expect2xx putLHWhitelistTeam :: (HasCallStack) => Galley -> TeamId -> Http ResponseLBS @@ -292,10 +276,10 @@ putLHWhitelistTeam galley tid = do . paths ["i", "legalhold", "whitelisted-teams", toByteString' tid] ) -accept :: Email -> InvitationCode -> RequestBody +accept :: EmailAddress -> InvitationCode -> RequestBody accept = acceptWithName (Name "Bob") -acceptWithName :: Name -> Email -> InvitationCode -> RequestBody +acceptWithName :: Name -> EmailAddress -> InvitationCode -> RequestBody acceptWithName name email code = RequestBodyLBS . encode $ object @@ -305,7 +289,7 @@ acceptWithName name email code = "team_code" .= code ] -extAccept :: Email -> Name -> Phone -> ActivationCode -> InvitationCode -> RequestBody +extAccept :: EmailAddress -> Name -> Phone -> ActivationCode -> InvitationCode -> RequestBody extAccept email name phone phoneCode code = RequestBodyLBS . encode $ object @@ -318,7 +302,7 @@ extAccept email name phone phoneCode code = "team_code" .= code ] -register :: Email -> BindingNewTeam -> Brig -> Http (Response (Maybe LByteString)) +register :: EmailAddress -> NewTeam -> Brig -> Http (Response (Maybe LByteString)) register e t brig = post ( brig @@ -335,7 +319,7 @@ register e t brig = ) ) -register' :: Email -> BindingNewTeam -> ActivationCode -> Brig -> Http (Response (Maybe LByteString)) +register' :: EmailAddress -> NewTeam -> ActivationCode -> Brig -> Http (Response (Maybe LByteString)) register' e t c brig = post ( brig @@ -439,12 +423,12 @@ isActivatedUser uid brig = do Just (_ : _) -> True _ -> False -stdInvitationRequest :: Email -> InvitationRequest +stdInvitationRequest :: EmailAddress -> InvitationRequest stdInvitationRequest = stdInvitationRequest' Nothing Nothing -stdInvitationRequest' :: Maybe Locale -> Maybe Role -> Email -> InvitationRequest +stdInvitationRequest' :: Maybe Locale -> Maybe Role -> EmailAddress -> InvitationRequest stdInvitationRequest' loc role email = - InvitationRequest loc role Nothing email Nothing + InvitationRequest loc role Nothing email setTeamTeamSearchVisibilityAvailable :: (HasCallStack, MonadHttp m, MonadIO m, MonadCatch m) => Galley -> TeamId -> FeatureStatus -> m () setTeamTeamSearchVisibilityAvailable galley tid status = @@ -452,7 +436,7 @@ setTeamTeamSearchVisibilityAvailable galley tid status = ( galley . paths ["i/teams", toByteString' tid, "features/searchVisibility"] . contentJson - . body (RequestBodyLBS . encode $ Public.WithStatusNoLock status Public.SearchVisibilityAvailableConfig Public.FeatureTTLUnlimited) + . body (RequestBodyLBS . encode $ Public.Feature status Public.SearchVisibilityAvailableConfig) ) !!! do const 200 === statusCode @@ -474,12 +458,12 @@ setTeamSearchVisibilityInboundAvailable galley tid status = ( galley . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @Public.SearchVisibilityInboundConfig] . contentJson - . body (RequestBodyLBS . encode $ Public.WithStatusNoLock status Public.SearchVisibilityInboundConfig Public.FeatureTTLUnlimited) + . body (RequestBodyLBS . encode $ Public.Feature status Public.SearchVisibilityInboundConfig) ) !!! do const 200 === statusCode -setUserEmail :: Brig -> UserId -> UserId -> Email -> Http ResponseLBS +setUserEmail :: Brig -> UserId -> UserId -> EmailAddress -> Http ResponseLBS setUserEmail brig from uid email = do put ( brig diff --git a/services/brig/test/integration/API/TeamUserSearch.hs b/services/brig/test/integration/API/TeamUserSearch.hs index b70f59a4b17..0301b8eaa4e 100644 --- a/services/brig/test/integration/API/TeamUserSearch.hs +++ b/services/brig/test/integration/API/TeamUserSearch.hs @@ -35,7 +35,7 @@ import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (assertBool, assertEqual, (@?=)) import Util (Brig, Galley, randomEmail, test, withSettingsOverrides) import Wire.API.User (User (..), userEmail, userId) -import Wire.API.User.Identity +import Wire.API.User.Identity hiding (toByteString) import Wire.API.User.Search type TestConstraints m = (MonadFail m, MonadCatch m, MonadIO m, MonadHttp m) @@ -43,7 +43,7 @@ type TestConstraints m = (MonadFail m, MonadCatch m, MonadIO m, MonadHttp m) tests :: Opt.Opts -> Manager -> Galley -> Brig -> IO TestTree tests opts mgr _galley brig = do pure $ - testGroup "/teams/:tid/search" $ + testGroup "teams user search" $ [ testWithNewIndex "can find user by email" (testSearchByEmailSameTeam brig), testWithNewIndex "empty query returns the whole team sorted" (testEmptyQuerySorted brig), testWithNewIndex "sorting by some properties works" (testSort brig), diff --git a/services/brig/test/integration/API/User.hs b/services/brig/test/integration/API/User.hs index 59e79905156..95e26d13ef1 100644 --- a/services/brig/test/integration/API/User.hs +++ b/services/brig/test/integration/API/User.hs @@ -26,8 +26,6 @@ import API.User.Auth qualified import API.User.Client qualified import API.User.Connection qualified import API.User.Handles qualified -import API.User.PasswordReset qualified -import API.User.Property qualified import API.User.RichInfo qualified import API.User.Util import Bilge hiding (accept, timeout) @@ -57,8 +55,8 @@ tests :: UserJournalWatcher -> IO TestTree tests conf fbc p b c ch g n aws db userJournalWatcher = do - let cl = ConnectionLimit $ Opt.setUserMaxConnections (Opt.optSettings conf) - let at = Opt.setActivationTimeout (Opt.optSettings conf) + let cl = ConnectionLimit conf.settings.userMaxConnections + let at = conf.settings.activationTimeout z <- mkZAuthEnv (Just conf) pure $ testGroup @@ -68,8 +66,6 @@ tests conf fbc p b c ch g n aws db userJournalWatcher = do API.User.Auth.tests conf p z db b g n, API.User.Connection.tests cl at p b c g fbc db, API.User.Handles.tests cl at conf p b c g, - API.User.PasswordReset.tests db cl at conf p b c g, - API.User.Property.tests cl at conf p b c g, API.User.RichInfo.tests cl at conf p b c g ] diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index 8e46b4437dd..e1757ba6a69 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -50,6 +50,7 @@ import Data.Json.Util (fromUTCTimeMillis) import Data.LegalHold import Data.List.NonEmpty qualified as NonEmpty import Data.List1 (singleton) +import Data.Mailbox import Data.Misc (plainTextPassword6Unsafe) import Data.Proxy import Data.Qualified @@ -59,8 +60,6 @@ import Data.String.Conversions import Data.Text qualified as T import Data.Text qualified as Text import Data.Text.Encoding qualified as T -import Data.Time (UTCTime, getCurrentTime) -import Data.Time.Clock (diffUTCTime) import Data.UUID qualified as UUID import Data.UUID.V4 qualified as UUID import Federator.MockServer (FederatedRequest (..), MockException (..)) @@ -73,20 +72,21 @@ import Network.Wai.Utilities.Error qualified as Error import Network.Wai.Utilities.Error qualified as Wai import Test.QuickCheck (arbitrary, generate) import Test.Tasty hiding (Timeout) -import Test.Tasty.Cannon hiding (Cannon) +import Test.Tasty.Cannon hiding (Cannon, Timeout) import Test.Tasty.Cannon qualified as WS import Test.Tasty.HUnit import UnliftIO (mapConcurrently_) import Util import Util.AWS as Util +import Util.Timeout import Web.Cookie (parseSetCookie) import Wire.API.Asset hiding (Asset) import Wire.API.Asset qualified as Asset import Wire.API.Connection import Wire.API.Conversation import Wire.API.Routes.MultiTablePaging -import Wire.API.Team.Feature (ExposeInvitationURLsToTeamAdminConfig (..), FeatureStatus (..), FeatureTTL' (..), LockStatus (LockStatusLocked), withStatus) -import Wire.API.Team.Invitation (Invitation (inInvitation)) +import Wire.API.Team.Feature +import Wire.API.Team.Invitation import Wire.API.Team.Permission hiding (self) import Wire.API.User import Wire.API.User.Activation @@ -94,22 +94,20 @@ import Wire.API.User.Auth import Wire.API.User.Auth qualified as Auth import Wire.API.User.Client -tests :: ConnectionLimit -> Opt.Timeout -> Opt.Opts -> Manager -> Brig -> Cannon -> CargoHold -> Galley -> AWS.Env -> UserJournalWatcher -> TestTree +tests :: ConnectionLimit -> Timeout -> Opt.Opts -> Manager -> Brig -> Cannon -> CargoHold -> Galley -> AWS.Env -> UserJournalWatcher -> TestTree tests _ at opts p b c ch g aws userJournalWatcher = testGroup "account" [ test p "post /register - 201 (with preverified)" $ testCreateUserWithPreverified opts b userJournalWatcher, test p "testCreateUserWithInvalidVerificationCode - post /register - 400 (with preverified)" $ testCreateUserWithInvalidVerificationCode b, test p "post /register - 201" $ testCreateUser b g, - test p "post /register - 400 + no email" $ testCreateUserNoEmailNoPassword b, test p "post /register - 201 anonymous" $ testCreateUserAnon b g, test p "testCreateUserEmptyName - post /register - 400 empty name" $ testCreateUserEmptyName b, test p "testCreateUserLongName - post /register - 400 name too long" $ testCreateUserLongName b, test p "post /register - 201 anonymous expiry" $ testCreateUserAnonExpiry b, test p "post /register - 201 pending" $ testCreateUserPending opts b, - test p "post /register - 201 existing activation" $ testCreateAccountPendingActivationKey opts b, test p "testCreateUserConflict - post /register - 409 conflict" $ testCreateUserConflict opts b, - test p "testCreateUserInvalidEmailOrPhone - post /register - 400 invalid input" $ testCreateUserInvalidEmailOrPhone opts b, + test p "testCreateUserInvalidEmail - post /register - 400 invalid input" $ testCreateUserInvalidEmail opts b, test p "post /register - 403 blacklist" $ testCreateUserBlacklist opts b aws, test p "post /register - 400 external-SSO" $ testCreateUserExternalSSO b, test p "post /register - 403 restricted user creation" $ testRestrictedUserCreation opts b, @@ -129,7 +127,6 @@ tests _ at opts p b c ch g aws userJournalWatcher = test p "post /list-users - 200" $ testMultipleUsers opts b, test p "put /self - 200" $ testUserUpdate b c userJournalWatcher, test p "put /access/self/email - 2xx" $ testEmailUpdate b userJournalWatcher, - test p "put /self/phone - 400" $ testPhoneUpdate b, test p "head /self/password - 200/404" $ testPasswordSet b, test p "put /self/password - 400" $ testPasswordSetInvalidPasswordLength b, test p "put /self/password - 200" $ testPasswordChange b, @@ -164,25 +161,14 @@ tests _ at opts p b c ch g aws userJournalWatcher = ] -- The testCreateUserWithInvalidVerificationCode test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- Registering with an invalid verification code and valid account details should fail. testCreateUserWithInvalidVerificationCode :: Brig -> Http () testCreateUserWithInvalidVerificationCode brig = do - -- Attempt to register (pre verified) user with phone - p <- randomPhone - code <- randomActivationCode -- incorrect but syntactically valid activation code - let Object regPhone = - object - [ "name" .= Name "Alice", - "phone" .= fromPhone p, - "phone_code" .= code - ] - postUserRegister' regPhone brig !!! do - const 400 === statusCode - const (Just "invalid-phone") === fmap Wai.label . responseJsonMaybe - -- Attempt to register (pre verified) user with email e <- randomEmail + code <- randomActivationCode -- incorrect but syntactically valid activation code let Object regEmail = object [ "name" .= Name "Alice", @@ -191,6 +177,8 @@ testCreateUserWithInvalidVerificationCode brig = do ] postUserRegister' regEmail brig !!! const 404 === statusCode +-- @END + testUpdateUserEmailByTeamOwner :: Opt.Opts -> Brig -> Http () testUpdateUserEmailByTeamOwner opts brig = do (_, teamOwner, emailOwner : otherTeamMember : _) <- createPopulatedBindingTeamWithNamesAndHandles brig 2 @@ -212,13 +200,13 @@ testUpdateUserEmailByTeamOwner opts brig = do checkUnauthorizedRequests emailOwner otherTeamMember teamOwnerDifferentTeam newEmail checkActivationCode newEmail False where - checkLetActivationExpire :: Email -> Http () + checkLetActivationExpire :: EmailAddress -> Http () checkLetActivationExpire email = do - let timeout = round (Opt.setActivationTimeout (Opt.optSettings opts)) + let timeout = round opts.settings.activationTimeout threadDelay ((timeout + 1) * 1000_000) checkActivationCode email False - checkActivationCode :: Email -> Bool -> Http () + checkActivationCode :: EmailAddress -> Bool -> Http () checkActivationCode email shouldExist = do maybeActivationCode <- Util.getActivationCode brig (Left email) void $ @@ -227,25 +215,17 @@ testUpdateUserEmailByTeamOwner opts brig = do then assertBool "activation code should exists" (isJust maybeActivationCode) else assertBool "activation code should not exists" (isNothing maybeActivationCode) - checkSetUserEmail :: User -> User -> Email -> Int -> Http () + checkSetUserEmail :: User -> User -> EmailAddress -> Int -> Http () checkSetUserEmail teamOwner emailOwner email expectedStatusCode = setUserEmail brig (userId teamOwner) (userId emailOwner) email !!! (const expectedStatusCode === statusCode) - checkUnauthorizedRequests :: User -> User -> User -> Email -> Http () + checkUnauthorizedRequests :: User -> User -> User -> EmailAddress -> Http () checkUnauthorizedRequests emailOwner otherTeamMember teamOwnerDifferentTeam email = do setUserEmail brig (userId teamOwnerDifferentTeam) (userId emailOwner) email !!! (const 404 === statusCode) setUserEmail brig (userId otherTeamMember) (userId emailOwner) email !!! (const 403 === statusCode) testCreateUserWithPreverified :: Opt.Opts -> Brig -> UserJournalWatcher -> Http () testCreateUserWithPreverified opts brig userJournalWatcher = do - -- Register (pre verified) user with phone - p <- randomPhone - let phoneReq = RequestBodyLBS . encode $ object ["phone" .= fromPhone p] - post (brig . path "/activate/send" . contentJson . body phoneReq) - !!! do - const 400 === statusCode - const (Just "invalid-phone") === fmap Wai.label . responseJsonMaybe - -- Register (pre verified) user with email e <- randomEmail let emailReq = RequestBodyLBS . encode $ object ["email" .= fromEmail e] @@ -260,7 +240,7 @@ testCreateUserWithPreverified opts brig userJournalWatcher = do "email" .= fromEmail e, "email_code" .= c ] - if Opt.setRestrictUserCreation (Opt.optSettings opts) == Just True + if opts.settings.restrictUserCreation == Just True then do postUserRegister' reg brig !!! const 403 === statusCode else do @@ -309,6 +289,7 @@ assertOnlySelfConversations galley uid = do liftIO $ cnvType conv @?= SelfConv -- The testCreateUserEmptyName test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- An empty name is not allowed on registration testCreateUserEmptyName :: Brig -> Http () @@ -320,7 +301,10 @@ testCreateUserEmptyName brig = do post (brig . path "/register" . contentJson . body p) !!! const 400 === statusCode +-- @END + -- The testCreateUserLongName test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- a name with > 128 characters is not allowed. testCreateUserLongName :: Brig -> Http () @@ -333,6 +317,8 @@ testCreateUserLongName brig = do post (brig . path "/register" . contentJson . body p) !!! const 400 === statusCode +-- @END + testCreateUserAnon :: Brig -> Galley -> Http () testCreateUserAnon brig galley = do let p = @@ -355,7 +341,7 @@ testCreateUserAnon brig galley = do Search.assertCan'tFind brig suid quid "Mr. Pink" testCreateUserPending :: Opt.Opts -> Brig -> Http () -testCreateUserPending (Opt.setRestrictUserCreation . Opt.optSettings -> Just True) _ = pure () +testCreateUserPending (Opt.restrictUserCreation . Opt.settings -> Just True) _ = pure () testCreateUserPending _ brig = do e <- randomEmail let p = @@ -389,26 +375,12 @@ testCreateUserPending _ brig = do Search.refreshIndex brig Search.assertCan'tFind brig suid quid "Mr. Pink" -testCreateUserNoEmailNoPassword :: Brig -> Http () -testCreateUserNoEmailNoPassword brig = do - p <- randomPhone - let newUser = - RequestBodyLBS . encode $ - object - [ "name" .= ("Alice" :: Text), - "phone" .= fromPhone p - ] - post - (brig . path "/i/users" . contentJson . body newUser) - !!! do - const 400 === statusCode - (const (Just "invalid-phone") === fmap Error.label . responseJsonMaybe) - -- The testCreateUserConflict test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- email address must not be taken on @/register@. testCreateUserConflict :: Opt.Opts -> Brig -> Http () -testCreateUserConflict (Opt.setRestrictUserCreation . Opt.optSettings -> Just True) _ = pure () +testCreateUserConflict (Opt.restrictUserCreation . Opt.settings -> Just True) _ = pure () testCreateUserConflict _ brig = do -- trusted email domains u <- createUser "conflict" brig @@ -424,7 +396,9 @@ testCreateUserConflict _ brig = do const (Just "key-exists") === fmap Error.label . responseJsonMaybe -- untrusted email domains u2 <- createUserUntrustedEmail "conflict" brig - let Just (Email loc dom) = userEmail u2 + let Just email = userEmail u2 + dom = T.decodeUtf8 $ domainPart email + loc = T.decodeUtf8 $ localPart email let p2 = RequestBodyLBS . encode $ object @@ -436,38 +410,29 @@ testCreateUserConflict _ brig = do const 409 === statusCode const (Just "key-exists") === fmap Error.label . responseJsonMaybe --- The testCreateUserInvalidEmailOrPhone test conforms to the following testing standards: --- --- Test to make sure a new user cannot be created with an invalid email address or invalid phone number. -testCreateUserInvalidEmailOrPhone :: Opt.Opts -> Brig -> Http () -testCreateUserInvalidEmailOrPhone (Opt.setRestrictUserCreation . Opt.optSettings -> Just True) _ = pure () -testCreateUserInvalidEmailOrPhone _ brig = do - email <- randomEmail - let reqEmail = - RequestBodyLBS . encode $ - object - [ "name" .= ("foo" :: Text), - "email" .= fromEmail email, - "password" .= defPassword, - "phone" .= ("123456" :: Text) -- invalid phone nr - ] - post (brig . path "/register" . contentJson . body reqEmail) - !!! const 400 === statusCode +-- @END - phone <- randomPhone +-- The testCreateUserInvalidEmail test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 +-- +-- Test to make sure a new user cannot be created with an invalid email address +testCreateUserInvalidEmail :: Opt.Opts -> Brig -> Http () +testCreateUserInvalidEmail (Opt.restrictUserCreation . Opt.settings -> Just True) _ = pure () +testCreateUserInvalidEmail _ brig = do let reqPhone = RequestBodyLBS . encode $ object [ "name" .= ("foo" :: Text), - "email" .= ("invalid@email" :: Text), -- invalid since there's only a single label - "password" .= defPassword, - "phone" .= fromPhone phone + "email" .= ("invalid@" :: Text), + "password" .= defPassword ] post (brig . path "/register" . contentJson . body reqPhone) !!! const 400 === statusCode +-- @END + testCreateUserBlacklist :: Opt.Opts -> Brig -> AWS.Env -> Http () -testCreateUserBlacklist (Opt.setRestrictUserCreation . Opt.optSettings -> Just True) _ _ = pure () +testCreateUserBlacklist (Opt.restrictUserCreation . Opt.settings -> Just True) _ _ = pure () testCreateUserBlacklist _ brig aws = mapM_ ensureBlacklist ["bounce", "complaint"] where @@ -491,18 +456,18 @@ testCreateUserBlacklist _ brig aws = "password" .= defPassword ] -- If there is no queue available, we need to force it either by publishing an event or using the API - forceBlacklist :: Text -> Email -> Http () + forceBlacklist :: Text -> EmailAddress -> Http () forceBlacklist typ em = case aws ^. AWS.sesQueue of Just queue -> publishMessage typ em queue Nothing -> Bilge.post (brig . path "i/users/blacklist" . queryItem "email" (toByteString' em)) !!! const 200 === statusCode - publishMessage :: Text -> Email -> Text -> Http () + publishMessage :: Text -> EmailAddress -> Text -> Http () publishMessage typ em queue = do let bdy = encode $ case typ of - "bounce" -> MailBounce BouncePermanent [em] - "complaint" -> MailComplaint [em] + "bounce" -> MailBounce BouncePermanent [Mailbox Nothing em] + "complaint" -> MailComplaint [Mailbox Nothing em] x -> error ("Unsupported message type: " ++ show x) void . AWS.execute aws $ AWS.enqueueStandard queue bdy - awaitBlacklist :: Int -> Email -> Http () + awaitBlacklist :: Int -> EmailAddress -> Http () awaitBlacklist n e = do r <- Bilge.head (brig . path "i/users/blacklist" . queryItem "email" (toByteString' e)) when (statusCode r == 404 && n > 0) $ do @@ -525,8 +490,8 @@ testCreateUserExternalSSO brig = do post (brig . path "/register" . contentJson . body (p True True)) !!! const 400 === statusCode -testActivateWithExpiry :: Opt.Opts -> Brig -> Opt.Timeout -> Http () -testActivateWithExpiry (Opt.setRestrictUserCreation . Opt.optSettings -> Just True) _ _ = pure () +testActivateWithExpiry :: Opt.Opts -> Brig -> Timeout -> Http () +testActivateWithExpiry (Opt.restrictUserCreation . Opt.settings -> Just True) _ _ = pure () testActivateWithExpiry _ brig timeout = do u <- responseJsonError =<< registerUser "dilbert" brig let email = fromMaybe (error "missing email") (userEmail u) @@ -690,7 +655,7 @@ testMultipleUsersUnqualified brig = do -- on this endpoint, only from the self profile (/self). expected = Set.fromList - [ (Just $ userDisplayName u1, Nothing :: Maybe Email), + [ (Just $ userDisplayName u1, Nothing :: Maybe EmailAddress), (Just $ userDisplayName u2, Nothing), (Just $ userDisplayName u3, Nothing) ] @@ -722,7 +687,7 @@ testMultipleUsersV3 brig = do q = ListUsersByIds (map userQualifiedId users) expected = Set.fromList - [ (Just $ userDisplayName u1, Nothing :: Maybe Email), + [ (Just $ userDisplayName u1, Nothing :: Maybe EmailAddress), (Just $ userDisplayName u2, Nothing), (Just $ userDisplayName u3, Nothing) ] @@ -759,6 +724,7 @@ testMultipleUsers opts brig = do UserProfile { profileQualifiedId = u5, profileName = Name "u5", + profileTextStatus = Nothing, profilePict = Pict [], profileAssets = [], profileAccentId = ColourId 0, @@ -775,7 +741,7 @@ testMultipleUsers opts brig = do q = ListUsersByIds $ u5 : u4 : map userQualifiedId users expected = Set.fromList - [ (Just $ userDisplayName u1, Nothing :: Maybe Email), + [ (Just $ userDisplayName u1, Nothing :: Maybe EmailAddress), (Just $ userDisplayName u2, Nothing), (Just $ userDisplayName u3, Nothing), (Just $ profileName u5Profile, profileEmail u5Profile) @@ -882,8 +848,9 @@ testUserUpdate brig cannon userJournalWatcher = do (Just AssetComplete) ] mNewName = Just $ aliceNewName + mNewTextStatus = rightToMaybe $ mkTextStatus "fun status" newPic = Nothing -- Legacy - userUpdate = UserUpdate mNewName newPic newAssets newColId + userUpdate = UserUpdate mNewName mNewTextStatus newPic newAssets newColId update = RequestBodyLBS . encode $ userUpdate -- Update profile & receive notification WS.bracketRN cannon [alice, bob] $ \[aliceWS, bobWS] -> do @@ -895,9 +862,10 @@ testUserUpdate brig cannon userJournalWatcher = do -- get the updated profile get (brig . path "/self" . zUser alice) !!! do const 200 === statusCode - const (mNewName, newColId, newAssets) + const (mNewName, mNewTextStatus, newColId, newAssets) === ( \u -> ( fmap userDisplayName u, + userTextStatus =<< u, fmap userAccentId u, fmap userAssets u ) @@ -929,7 +897,7 @@ testEmailUpdate brig userJournalWatcher = do -- ensure no other user has "test+@example.com" -- if there is such a user, let's delete it first. otherwise -- this test fails since there can be only one user with "test+...@example.com" - ensureNoOtherUserWithEmail (Email "test" "example.com") + ensureNoOtherUserWithEmail (unsafeEmailAddress "test" "example.com") -- we want to use a non-trusted domain in order to verify profile changes flip initiateUpdateAndActivate uid =<< mkEmailRandomLocalSuffix "test@example.com" flip initiateUpdateAndActivate uid =<< mkEmailRandomLocalSuffix "test@example.com" @@ -940,7 +908,7 @@ testEmailUpdate brig userJournalWatcher = do -- In that case, you might need to manually delete the user from the test DB. @elland deleteUserInternal uid brig !!! const 202 === statusCode where - ensureNoOtherUserWithEmail :: Email -> Http () + ensureNoOtherUserWithEmail :: EmailAddress -> Http () ensureNoOtherUserWithEmail eml = do tk :: Maybe AccessToken <- responseJsonMaybe <$> login brig (defEmailLogin eml) SessionCookie @@ -948,7 +916,7 @@ testEmailUpdate brig userJournalWatcher = do deleteUser (Auth.user t) (Just defPassword) brig !!! const 200 === statusCode Util.assertDeleteJournaled userJournalWatcher (Auth.user t) "user deletion" - initiateUpdateAndActivate :: Email -> UserId -> Http () + initiateUpdateAndActivate :: EmailAddress -> UserId -> Http () initiateUpdateAndActivate eml uid = do initiateEmailUpdateNoSend brig eml uid !!! const 202 === statusCode activateEmail brig eml @@ -956,28 +924,7 @@ testEmailUpdate brig userJournalWatcher = do Util.assertEmailUpdateJournaled userJournalWatcher uid eml "user update" -- Ensure login work both with the full email and the "short" version login brig (defEmailLogin eml) SessionCookie !!! const 200 === statusCode - login brig (defEmailLogin (Email "test" "example.com")) SessionCookie !!! const 200 === statusCode - -testPhoneUpdate :: Brig -> Http () -testPhoneUpdate brig = do - uid <- userId <$> randomUser brig - phn <- randomPhone - updatePhone brig uid phn - -- check new phone - get (brig . path "/self" . zUser uid) !!! do - const 200 === statusCode - -testCreateAccountPendingActivationKey :: Opt.Opts -> Brig -> Http () -testCreateAccountPendingActivationKey (Opt.setRestrictUserCreation . Opt.optSettings -> Just True) _ = pure () -testCreateAccountPendingActivationKey _ brig = do - uid <- userId <$> randomUser brig - phn <- randomPhone - -- update phone - let phoneUpdate = RequestBodyLBS . encode $ PhoneUpdate phn - put (brig . path "/self/phone" . contentJson . zUser uid . zConn "c" . body phoneUpdate) - !!! do - const 400 === statusCode - const (Just "invalid-phone") === fmap Error.label . responseJsonMaybe + login brig (defEmailLogin (unsafeEmailAddress "test" "example.com")) SessionCookie !!! const 200 === statusCode testUserLocaleUpdate :: Brig -> UserJournalWatcher -> Http () testUserLocaleUpdate brig userJournalWatcher = do @@ -1047,7 +994,7 @@ testGetByIdentity brig = do const 200 === statusCode const (Just [uid]) === getUids where - getUids r = fmap (userId . accountUser) <$> responseJsonMaybe r + getUids r = fmap userId <$> responseJsonMaybe r testPasswordSet :: Brig -> Http () testPasswordSet brig = do @@ -1108,7 +1055,7 @@ testPasswordChange brig = do -- login with new password login brig - (PasswordLogin (PasswordLoginData (LoginByEmail email) newPass Nothing Nothing)) + (MkLogin (LoginByEmail email) newPass Nothing Nothing) PersistentCookie !!! const 200 === statusCode -- try to change the password to itself should fail @@ -1145,7 +1092,7 @@ testSendActivationCode opts brig = do -- Code for email pre-verification requestActivationCode brig 200 . Left =<< randomEmail -- Standard email registration flow - if Opt.setRestrictUserCreation (Opt.optSettings opts) == Just True + if opts.settings.restrictUserCreation == Just True then do registerUser "Alice" brig !!! const 403 === statusCode else do @@ -1156,10 +1103,7 @@ testSendActivationCode opts brig = do testSendActivationCodeInvalidEmailOrPhone :: Brig -> Http () testSendActivationCodeInvalidEmailOrPhone brig = do - let Just invalidEmail = parseEmail "?@?" - let invalidPhone = Phone "1234" - -- Code for phone pre-verification - requestActivationCode brig 400 (Right invalidPhone) + let invalidEmail = unsafeEmailAddress "?" "?" -- Code for email pre-verification requestActivationCode brig 400 (Left invalidEmail) @@ -1275,7 +1219,7 @@ testDeleteWithProfilePic brig cargohold = do (qUnqualified $ ast ^. Asset.assetKey) (Just AssetComplete) ] - userUpdate = UserUpdate Nothing Nothing newAssets Nothing + userUpdate = UserUpdate Nothing Nothing Nothing newAssets Nothing update = RequestBodyLBS . encode $ userUpdate -- Update profile with the uploaded asset put (brig . path "/self" . contentJson . zUser uid . zConn "c" . body update) @@ -1308,11 +1252,9 @@ testUpdateSSOId brig galley = do assertEqual "updateSSOId/ssoid" ssoid ssoid' assertEqual "updateSSOId/email" (userEmail user) mEmail (owner, teamid) <- createUserWithTeam brig - let mkMember :: Bool -> Bool -> Http User - mkMember hasEmail hasPhone = do + let mkMember :: Bool -> Http User + mkMember hasEmail = do member <- createTeamMember brig galley owner teamid noPermissions - when hasPhone $ do - updatePhone brig (userId member) =<< randomPhone unless hasEmail $ do error "not implemented" selfUser <$> (responseJsonError =<< get (brig . path "/self" . zUser (userId member))) @@ -1320,7 +1262,7 @@ testUpdateSSOId brig galley = do ssoids2 = [UserSSOId (mkSampleUref "2" "1"), UserSSOId (mkSampleUref "2" "2")] users <- sequence - [ mkMember True False + [ mkMember True -- the following two could be implemented by creating the user implicitly via SSO login. -- , mkMember False False ] @@ -1329,19 +1271,19 @@ testUpdateSSOId brig galley = do testDomainsBlockedForRegistration :: Opt.Opts -> Brig -> Http () testDomainsBlockedForRegistration opts brig = withDomainsBlockedForRegistration opts ["bad1.domain.com", "bad2.domain.com"] $ do - badEmail1 <- randomEmail <&> \e -> e {emailDomain = "bad1.domain.com"} - badEmail2 <- randomEmail <&> \e -> e {emailDomain = "bad2.domain.com"} + badEmail1 <- randomEmail <&> \e -> unsafeEmailAddress (localPart e) "bad1.domain.com" + badEmail2 <- randomEmail <&> \e -> unsafeEmailAddress (localPart e) "bad2.domain.com" post (brig . path "/activate/send" . contentJson . body (p badEmail1)) !!! do const 451 === statusCode const (Just "domain-blocked-for-registration") === (^? AesonL.key "label" . AesonL._String) . (responseJsonUnsafe @Value) post (brig . path "/activate/send" . contentJson . body (p badEmail2)) !!! do const 451 === statusCode const (Just "domain-blocked-for-registration") === (^? AesonL.key "label" . AesonL._String) . (responseJsonUnsafe @Value) - goodEmail <- randomEmail <&> \e -> e {emailDomain = "good.domain.com"} + goodEmail <- randomEmail <&> \e -> unsafeEmailAddress (localPart e) "good.domain.com" post (brig . path "/activate/send" . contentJson . body (p goodEmail)) !!! do const 200 === statusCode where - p email = RequestBodyLBS . encode $ SendActivationCode (Left email) Nothing False + p email = RequestBodyLBS . encode $ SendActivationCode email Nothing -- | FUTUREWORK: @setRestrictUserCreation@ perhaps needs to be tested in one place only, since it's the -- first thing that we check on the /register endpoint. Other tests that make use of @setRestrictUserCreation@ @@ -1351,7 +1293,7 @@ testRestrictedUserCreation opts brig = do -- We create a team before to help in other tests (teamOwner, createdTeam) <- createUserWithTeam brig - let opts' = opts {Opt.optSettings = (Opt.optSettings opts) {Opt.setRestrictUserCreation = Just True}} + let opts' = opts {Opt.settings = opts.settings {Opt.restrictUserCreation = Just True}} withSettingsOverrides opts' $ do e <- randomEmail @@ -1432,11 +1374,11 @@ testTooManyMembersForLegalhold opts brig = do -- would return in that case. inviteeEmail <- randomEmail let invite = stdInvitationRequest inviteeEmail - inv <- + inv :: Invitation <- responseJsonError =<< postInvitation brig tid owner invite DB.ClientState -> Http () -testLoginWith6CharPassword brig db = do +testLoginWith6CharPassword :: Opts.Opts -> Brig -> DB.ClientState -> Http () +testLoginWith6CharPassword opts brig db = do (uid, Just email) <- (userId &&& userEmail) <$> randomUser brig checkLogin email defPassword 200 let pw6 = plainTextPassword6Unsafe "123456" @@ -177,11 +180,11 @@ testLoginWith6CharPassword brig db = do checkLogin email defPassword 403 checkLogin email pw6 200 where - checkLogin :: Email -> PlainTextPassword6 -> Int -> Http () + checkLogin :: EmailAddress -> PlainTextPassword6 -> Int -> Http () checkLogin email pw expectedStatusCode = login brig - (PasswordLogin (PasswordLoginData (LoginByEmail email) pw Nothing Nothing)) + (MkLogin (LoginByEmail email) pw Nothing Nothing) PersistentCookie !!! const expectedStatusCode === statusCode @@ -193,7 +196,7 @@ testLoginWith6CharPassword brig db = do updatePassword :: (MonadClient m) => UserId -> PlainTextPassword6 -> m () updatePassword u t = do - p <- liftIO $ mkSafePasswordScrypt t + p <- liftIO $ runM . runHashPassword opts.settings.passwordHashingOptions $ hashPassword6 t retry x5 $ write userPasswordUpdate (params LocalQuorum (p, u)) userPasswordUpdate :: PrepQuery W (Password, UserId) () @@ -352,26 +355,12 @@ testEmailLogin brig = do assertSanePersistentCookie @ZAuth.User (decodeCookie rs) assertSaneAccessToken now (userId u) (decodeToken rs) -- Login again, but with capitalised email address - let Email loc dom = email - let email' = Email (Text.toUpper loc) dom + let loc = localPart email + dom = domainPart email + email' = unsafeEmailAddress (encodeUtf8 . Text.toUpper . decodeUtf8 $ loc) dom login brig (defEmailLogin email') PersistentCookie !!! const 200 === statusCode -testPhoneLogin :: Brig -> Http () -testPhoneLogin brig = do - p <- randomPhone - let newUser = - RequestBodyLBS . encode $ - object - [ "name" .= ("Alice" :: Text), - "phone" .= fromPhone p - ] - -- phone logins are not supported anymore - post (brig . path "/i/users" . contentJson . Http.body newUser) - !!! do - const 400 === statusCode - const (Just "invalid-phone") === errorLabel - testHandleLogin :: Brig -> Http () testHandleLogin brig = do usr <- Public.userId <$> randomUser brig @@ -379,35 +368,23 @@ testHandleLogin brig = do let update = RequestBodyLBS . encode $ HandleUpdate hdl put (brig . path "/self/handle" . contentJson . zUser usr . zConn "c" . Http.body update) !!! const 200 === statusCode - let l = PasswordLogin (PasswordLoginData (LoginByHandle (fromJust $ parseHandle hdl)) defPassword Nothing Nothing) + let l = MkLogin (LoginByHandle (fromJust $ parseHandle hdl)) defPassword Nothing Nothing login brig l PersistentCookie !!! const 200 === statusCode -- | Check that local part after @+@ is ignored by equality on email addresses if the domain is -- untrusted. testLoginUntrustedDomain :: Brig -> Http () testLoginUntrustedDomain brig = do - Just (Email loc dom) <- userEmail <$> createUserUntrustedEmail "Homer" brig + Just email <- userEmail <$> createUserUntrustedEmail "Homer" brig + let loc = decodeUtf8 $ localPart email + dom = domainPart email -- login without "+" suffix - let email' = Email (Text.takeWhile (/= '+') loc) dom + let email' = unsafeEmailAddress (encodeUtf8 $ Text.takeWhile (/= '+') loc) dom login brig (defEmailLogin email') PersistentCookie !!! const 200 === statusCode -testSendLoginCode :: Brig -> Http () -testSendLoginCode brig = do - p <- randomPhone - let newUser = - RequestBodyLBS . encode $ - object - [ "name" .= ("Alice" :: Text), - "phone" .= fromPhone p, - "password" .= ("topsecretdefaultpassword" :: Text) - ] - post (brig . path "/i/users" . contentJson . Http.body newUser) - !!! do - const 400 === statusCode - const (Just "invalid-phone") === errorLabel - -- The testLoginFailure test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- Test that trying to log in with a wrong password or non-existent email fails. testLoginFailure :: Brig -> Http () @@ -417,24 +394,25 @@ testLoginFailure brig = do let badpw = plainTextPassword6Unsafe "wrongpassword" login brig - (PasswordLogin (PasswordLoginData (LoginByEmail email) badpw Nothing Nothing)) + (MkLogin (LoginByEmail email) badpw Nothing Nothing) PersistentCookie !!! const 403 === statusCode -- login with wrong / non-existent email - let badmail = Email "wrong" "wire.com" + let badmail = unsafeEmailAddress "wrong" "wire.com" login brig - ( PasswordLogin - (PasswordLoginData (LoginByEmail badmail) defPassword Nothing Nothing) + ( MkLogin (LoginByEmail badmail) defPassword Nothing Nothing ) PersistentCookie !!! const 403 === statusCode +-- @END + testThrottleLogins :: Opts.Opts -> Brig -> Http () testThrottleLogins conf b = do -- Get the maximum amount of times we are allowed to login before -- throttling begins - let l = Opts.setUserCookieLimit (Opts.optSettings conf) + let l = Opts.userCookieLimit (Opts.settings conf) u <- randomUser b let Just e = userEmail u -- Login exactly that amount of times, as fast as possible @@ -455,6 +433,7 @@ testThrottleLogins conf b = do login b (defEmailLogin e) SessionCookie !!! const 200 === statusCode -- The testLimitRetries test conforms to the following testing standards: +-- @SF.Channel @TSFI.RESTfulAPI @TSFI.NTP @S2 -- -- The following test tests the login retries. It checks that a user can make -- only a prespecified number of attempts to log in with an invalid password, @@ -465,7 +444,7 @@ testThrottleLogins conf b = do -- the aforementioned user. testLimitRetries :: (HasCallStack) => Opts.Opts -> Brig -> Http () testLimitRetries conf brig = do - let Just opts = Opts.setLimitFailedLogins . Opts.optSettings $ conf + let Just opts = conf.settings.limitFailedLogins unless (Opts.timeout opts <= 30) $ error "`loginRetryTimeout` is the number of seconds this test is running. Please pick a value < 30." usr <- randomUser brig @@ -487,7 +466,7 @@ testLimitRetries conf brig = do -- throttling should stop and login should work again do let Just retryAfterSecs = fromByteString =<< getHeader "Retry-After" resp - retryTimeout = Opts.Timeout $ fromIntegral retryAfterSecs + retryTimeout = Timeout $ fromIntegral retryAfterSecs liftIO $ do assertBool ("throttle delay (1): " <> show (retryTimeout, Opts.timeout opts)) @@ -509,6 +488,8 @@ testLimitRetries conf brig = do liftIO $ threadDelay (1000000 * 2) login brig (defEmailLogin email) SessionCookie !!! const 200 === statusCode +-- @END + ------------------------------------------------------------------------------- -- LegalHold Login @@ -575,7 +556,7 @@ testWrongPasswordLegalHoldLogin brig galley = do legalHoldLogin brig (LegalHoldLogin alice (plainTextPassword6 "wrong-password") Nothing) PersistentCookie !!! do const 403 === statusCode const (Just "invalid-credentials") === errorLabel - -- attempt a legalhold login with a no password + -- attempt a legalhold login without a password legalHoldLogin brig (LegalHoldLogin alice Nothing Nothing) PersistentCookie !!! do const 403 === statusCode const (Just "missing-auth") === errorLabel @@ -599,16 +580,35 @@ testLegalHoldLogout brig galley = do -- right password. testEmailSsoLogin :: Brig -> Http () testEmailSsoLogin brig = do - -- Create a user - uid <- Public.userId <$> randomUser brig + teamid <- snd <$> createUserWithTeam brig + let ssoid = UserSSOId mkSimpleSampleUref + -- creating user with sso_id, team_id + profile :: SelfProfile <- + responseJsonError + =<< postUser "dummy" True False (Just ssoid) (Just teamid) brig Http () +testEmailSsoLoginNonSsoUser brig = do + -- Create a user + uid <- Public.userId <$> randomUser brig + -- Login and do some checks + void $ + ssoLogin brig (SsoLogin uid Nothing) PersistentCookie + ZAuth.Env -> Brig -> Http () @@ -652,6 +653,8 @@ testInvalidCookie z b = do const 403 === statusCode const (Just "expired") =~= responseBody +-- @END + testInvalidToken :: ZAuth.Env -> Brig -> Http () testInvalidToken z b = do user <- Public.userId <$> randomUser b @@ -789,7 +792,7 @@ testNewPersistentCookie config b = getAndTestDBSupersededCookieAndItsValidSuccessor :: Opts.Opts -> Brig -> Nginz -> Http (Http.Cookie, Http.Cookie) getAndTestDBSupersededCookieAndItsValidSuccessor config b n = do u <- randomUser b - let renewAge = Opts.setUserCookieRenewAge $ Opts.optSettings config + let renewAge = config.settings.userCookieRenewAge let minAge = fromIntegral $ (renewAge + 1) * 1000000 Just email = userEmail u _rs <- @@ -1036,7 +1039,7 @@ testAccessWithExistingClientId brig = do testNewSessionCookie :: Opts.Opts -> Brig -> Http () testNewSessionCookie config b = do u <- randomUser b - let renewAge = Opts.setUserCookieRenewAge $ Opts.optSettings config + let renewAge = config.settings.userCookieRenewAge let minAge = fromIntegral $ renewAge * 1000000 + 1 Just email = userEmail u _rs <- @@ -1054,7 +1057,7 @@ testSuspendInactiveUsers config brig cookieType endPoint = do -- (context information: cookies are stored by user, not by device; so if there is a -- cookie that is old, it means none of the devices of the user has used it for a request.) - let Just suspendAge = Opts.suspendTimeout <$> Opts.setSuspendInactiveUsers (Opts.optSettings config) + let Just suspendAge = Opts.suspendTimeout <$> config.settings.suspendInactiveUsers unless (suspendAge <= 30) $ error "`suspendCookiesOlderThanSecs` is the number of seconds this test is running. Please pick a value < 30." @@ -1065,7 +1068,7 @@ testSuspendInactiveUsers config brig cookieType endPoint = do do @@ -1164,6 +1167,7 @@ testRemoveCookiesByLabelAndId b = do listCookies b (userId u) >>= liftIO . ([lbl] @=?) . map cookieLabel -- The testTooManyCookies test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- The test asserts that there is an upper limit for the number of user cookies -- per cookie type. It does that by concurrently attempting to create more @@ -1173,7 +1177,7 @@ testRemoveCookiesByLabelAndId b = do testTooManyCookies :: Opts.Opts -> Brig -> Http () testTooManyCookies config b = do u <- randomUser b - let l = Opts.setUserCookieLimit (Opts.optSettings config) + let l = config.settings.userCookieLimit let Just e = userEmail u carry = 2 pwlP = emailLogin e defPassword (Just "persistent") @@ -1213,6 +1217,8 @@ testTooManyCookies config b = do ) xxx -> error ("Unexpected status code when logging in: " ++ show xxx) +-- @END + testLogout :: Brig -> Http () testLogout b = do Just email <- userEmail <$> randomUser b diff --git a/services/brig/test/integration/API/User/Client.hs b/services/brig/test/integration/API/User/Client.hs index fef7075b728..c73e81861f0 100644 --- a/services/brig/test/integration/API/User/Client.hs +++ b/services/brig/test/integration/API/User/Client.hs @@ -30,8 +30,7 @@ import API.User.Util import API.User.Util qualified as Util import Bilge hiding (accept, head, timeout) import Bilge.Assert -import Brig.Options qualified as Opt -import Brig.Options qualified as Opts +import Brig.Options as Opt import Cassandra qualified as DB import Control.Lens hiding (Wrapped, (#)) import Crypto.JWT hiding (Ed25519, header, params) @@ -55,7 +54,6 @@ import Data.Set qualified as Set import Data.String.Conversions import Data.Text.Ascii (AsciiChars (validate), encodeBase64UrlUnpadded, toText) import Data.Text.Encoding qualified as T -import Data.Time (addUTCTime) import Data.Time.Clock.POSIX import Data.UUID (toByteString) import Data.UUID qualified as UUID @@ -65,11 +63,12 @@ import Network.Wai.Utilities.Error qualified as Error import System.Logger qualified as Log import Test.QuickCheck (arbitrary, generate) import Test.Tasty hiding (Timeout) -import Test.Tasty.Cannon hiding (Cannon) +import Test.Tasty.Cannon hiding (Cannon, Timeout) import Test.Tasty.Cannon qualified as WS import Test.Tasty.HUnit import UnliftIO (mapConcurrently) import Util +import Util.Timeout import Wire.API.Internal.Notification import Wire.API.MLS.CipherSuite import Wire.API.Routes.Version @@ -86,7 +85,7 @@ import Wire.API.Wrapped (Wrapped (..)) import Wire.VerificationCode qualified as Code import Wire.VerificationCodeGen -tests :: ConnectionLimit -> Opt.Timeout -> Opt.Opts -> Manager -> DB.ClientState -> Nginz -> Brig -> Cannon -> Galley -> TestTree +tests :: ConnectionLimit -> Timeout -> Opt.Opts -> Manager -> DB.ClientState -> Nginz -> Brig -> Cannon -> Galley -> TestTree tests _cl _at opts p db n b c g = testGroup "client" @@ -156,13 +155,13 @@ testAddGetClientVerificationCode db brig galley = do let k = mkKey email codeValue <- Code.codeValue <$$> lookupCode db k Code.AccountLogin checkLoginSucceeds $ - PasswordLogin $ - PasswordLoginData (LoginByEmail email) defPassword (Just defCookieLabel) codeValue + MkLogin (LoginByEmail email) defPassword (Just defCookieLabel) codeValue c <- addClient' codeValue getClient brig uid (clientId c) !!! do const 200 === statusCode const (Just c) === responseJsonMaybe +-- @SF.Channel @TSFI.RESTfulAPI @S2 -- -- Test that device cannot be added with missing second factor email verification code when this feature is enabled testAddGetClientMissingCode :: Brig -> Galley -> Http () @@ -179,6 +178,9 @@ testAddGetClientMissingCode brig galley = do const 403 === statusCode const (Just "code-authentication-required") === fmap Error.label . responseJsonMaybe +-- @END + +-- @SF.Channel @TSFI.RESTfulAPI @S2 -- -- Test that device cannot be added with wrong second factor email verification code when this feature is enabled testAddGetClientWrongCode :: Brig -> Galley -> Http () @@ -196,6 +198,9 @@ testAddGetClientWrongCode brig galley = do const 403 === statusCode const (Just "code-authentication-failed") === fmap Error.label . responseJsonMaybe +-- @END + +-- @SF.Channel @TSFI.RESTfulAPI @S2 -- -- Test that device cannot be added with expired second factor email verification code when this feature is enabled testAddGetClientCodeExpired :: DB.ClientState -> Opt.Opts -> Brig -> Galley -> Http () @@ -212,14 +217,15 @@ testAddGetClientCodeExpired db opts brig galley = do let k = mkKey email codeValue <- (.codeValue) <$$> lookupCode db k Code.AccountLogin checkLoginSucceeds $ - PasswordLogin $ - PasswordLoginData (LoginByEmail email) defPassword (Just defCookieLabel) codeValue - let verificationTimeout = round (Opt.setVerificationTimeout (Opt.optSettings opts)) - threadDelay $ ((verificationTimeout + 1) * 1000_000) + MkLogin (LoginByEmail email) defPassword (Just defCookieLabel) codeValue + let timeout = round (verificationTimeout opts.settings) + threadDelay $ ((timeout + 1) * 1000_000) addClient' codeValue !!! do const 403 === statusCode const (Just "code-authentication-failed") === fmap Error.label . responseJsonMaybe +-- @END + data AddGetClient = AddGetClient { addWithPassword :: Bool, addWithMLSKeys :: Bool @@ -253,7 +259,7 @@ testAddGetClient params brig cannon = do let etype = j ^? key "type" . _String let eclient = j ^? key "client" etype @?= Just "user.client-add" - fmap fromJSON eclient @?= Just (Success (Versioned @'V5 c)) + fmap fromJSON eclient @?= Just (Success (Versioned @'V6 c)) pure c liftIO $ clientMLSPublicKeys c @?= keys getClient brig uid (clientId c) !!! do @@ -285,7 +291,7 @@ testGetUserClientsQualified opts brig = do _c11 :: Client <- responseJsonError =<< addClient brig uid1 (defNewClient PermanentClientType [pk11] lk11) _c12 :: Client <- responseJsonError =<< addClient brig uid1 (defNewClient PermanentClientType [pk12] lk12) _c13 :: Client <- responseJsonError =<< addClient brig uid1 (defNewClient TemporaryClientType [pk13] lk13) - let localdomain = opts ^. Opt.optionSettings & Opt.setFederationDomain + let localdomain = opts.settings.federationDomain getUserClientsQualified brig uid2 localdomain uid1 !!! do const 200 === statusCode assertTrue_ $ \res -> do @@ -390,7 +396,7 @@ testListClientsBulk opts brig = do c21 <- responseJsonError =<< addClient brig uid2 (defNewClient PermanentClientType [pk21] lk21) c22 <- responseJsonError =<< addClient brig uid2 (defNewClient PermanentClientType [pk22] lk22) - let domain = Opt.setFederationDomain $ Opt.optSettings opts + let domain = opts.settings.federationDomain uid3 <- userId <$> randomUser brig let mkPubClient cl = PubClient (clientId cl) (clientClass cl) let expectedResponse :: QualifiedUserMap (Set PubClient) = @@ -433,7 +439,7 @@ testClientsWithoutPrekeys brig cannon db opts = do uid2 <- userId <$> randomUser brig - let domain = opts ^. Opt.optionSettings & Opt.setFederationDomain + let domain = opts.settings.federationDomain let userClients = QualifiedUserClients $ @@ -525,7 +531,7 @@ testClientsWithoutPrekeysV4 brig cannon db opts = do uid2 <- userId <$> randomUser brig - let domain = opts ^. Opt.optionSettings & Opt.setFederationDomain + let domain = opts.settings.federationDomain let userClients = QualifiedUserClients $ @@ -620,7 +626,7 @@ testClientsWithoutPrekeysFailToListV4 brig cannon db opts = do uid2 <- fakeRemoteUser - let domain = opts ^. Opt.optionSettings & Opt.setFederationDomain + let domain = opts.settings.federationDomain let userClients1 = QualifiedUserClients $ @@ -704,7 +710,7 @@ testListClientsBulkV2 opts brig = do c21 <- responseJsonError =<< addClient brig uid2 (defNewClient PermanentClientType [pk21] lk21) c22 <- responseJsonError =<< addClient brig uid2 (defNewClient PermanentClientType [pk22] lk22) - let domain = Opt.setFederationDomain $ Opt.optSettings opts + let domain = opts.settings.federationDomain uid3 <- userId <$> randomUser brig let mkPubClient cl = PubClient (clientId cl) (clientClass cl) let expectedResponse :: WrappedQualifiedUserMap (Set PubClient) = @@ -768,7 +774,7 @@ testGetUserPrekeys brig = do testGetUserPrekeysQualified :: Brig -> Opt.Opts -> Http () testGetUserPrekeysQualified brig opts = do - let domain = opts ^. Opt.optionSettings & Opt.setFederationDomain + let domain = opts.settings.federationDomain [(uid, _c, _lpk, cpk)] <- generateClients 1 brig get (brig . paths ["users", toByteString' domain, toByteString' uid, "prekeys"] . zUser uid) !!! do const 200 === statusCode @@ -789,7 +795,7 @@ testGetClientPrekey brig = do testGetClientPrekeyQualified :: Brig -> Opt.Opts -> Http () testGetClientPrekeyQualified brig opts = do - let domain = opts ^. Opt.optionSettings & Opt.setFederationDomain + let domain = opts.settings.federationDomain [(uid, c, _lpk, cpk)] <- generateClients 1 brig get (brig . paths ["users", toByteString' domain, toByteString' uid, "prekeys", toByteString' (clientId c)] . zUser uid) !!! do const 200 === statusCode @@ -826,7 +832,7 @@ testMultiUserGetPrekeys brig = do testMultiUserGetPrekeysQualified :: Brig -> Opt.Opts -> Http () testMultiUserGetPrekeysQualified brig opts = do - let domain = opts ^. Opt.optionSettings & Opt.setFederationDomain + let domain = opts.settings.federationDomain xs <- generateClients 3 brig let userClients = @@ -860,7 +866,7 @@ testMultiUserGetPrekeysQualified brig opts = do testMultiUserGetPrekeysQualifiedV4 :: Brig -> Opt.Opts -> Http () testMultiUserGetPrekeysQualifiedV4 brig opts = do - let domain = opts ^. Opt.optionSettings & Opt.setFederationDomain + let domain = opts.settings.federationDomain xs <- generateClients 3 brig let userClients = @@ -897,6 +903,7 @@ testMultiUserGetPrekeysQualifiedV4 brig opts = do const (Right $ expectedUserClientMap) === responseJsonEither -- The testTooManyClients test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- The test validates the upper bound on the number of permanent clients per -- user. It does so by trying to create one permanent client more than allowed. @@ -906,7 +913,7 @@ testTooManyClients :: Opt.Opts -> Brig -> Http () testTooManyClients opts brig = do uid <- userId <$> randomUser brig -- We can always change the permanent client limit - let newOpts = opts & Opt.optionSettings . Opt.userMaxPermClients ?~ 1 + let newOpts = opts & settingsLens . userMaxPermClientsLens ?~ 1 withSettingsOverrides newOpts $ do -- There is only one temporary client, adding a new one -- replaces the previous one. @@ -977,7 +984,10 @@ testRegularPrekeysCannotBeSentAsLastPrekeysDuringUpdate brig = do !!! const 400 === statusCode +-- @END + -- The testRemoveClient test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- This test validates creating and deleting a client. A client is created and -- consequently deleted. Deleting a second time yields response 404 not found. @@ -1023,7 +1033,10 @@ testRemoveClient hasPwd brig cannon = do newClientCookie = Just defCookieLabel } +-- @END + -- The testRemoveClientShortPwd test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- The test checks if a client can be deleted by providing a too short password. -- This is done by using a single-character password, whereas the minimum is 6 @@ -1056,7 +1069,10 @@ testRemoveClientShortPwd brig = do newClientCookie = Just defCookieLabel } +-- @END + -- The testRemoveClientIncorrectPwd test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- The test checks if a client can be deleted by providing a syntax-valid, but -- incorrect password. The client deletion attempt fails with a 403 error @@ -1089,6 +1105,8 @@ testRemoveClientIncorrectPwd brig = do newClientCookie = Just defCookieLabel } +-- @END + testUpdateClient :: Opt.Opts -> Brig -> Http () testUpdateClient opts brig = do uid <- userId <$> randomUser brig @@ -1139,7 +1157,7 @@ testUpdateClient opts brig = do const Nothing === (preview (key "mls_public_keys") <=< responseJsonMaybe @Value) -- via `/users/:domain/:uid/clients/:client`, only `id` and `class` are visible: - let localdomain = opts ^. Opt.optionSettings & Opt.setFederationDomain + let localdomain = opts.settings.federationDomain get (brig . paths ["users", toByteString' localdomain, toByteString' uid, "clients", toByteString' (clientId c)]) !!! do const 200 === statusCode const (Just $ clientId c) === (fmap pubClientId . responseJsonMaybe) @@ -1169,7 +1187,7 @@ testUpdateClient opts brig = do -- update supported client capabilities work let checkUpdate :: (HasCallStack) => Maybe [ClientCapability] -> Bool -> [ClientCapability] -> Http () checkUpdate capsIn respStatusOk capsOut = do - let update'' = defUpdateClient {updateClientCapabilities = Set.fromList <$> capsIn} + let update'' = defUpdateClient {updateClientCapabilities = ClientCapabilityList . Set.fromList <$> capsIn} put ( apiVersion "v1" . brig @@ -1218,7 +1236,7 @@ testUpdateClient opts brig = do assertEqual "" (clientId c) cid' assertEqual "" expectedPrekey prekey' - caps = Just $ Set.fromList [ClientSupportsLegalholdImplicitConsent] + caps = Just $ ClientCapabilityList $ Set.fromList [ClientSupportsLegalholdImplicitConsent] label = "label-bc1b7b0c-b7bf-11eb-9a1d-233d397f934a" prekey = somePrekeys !! 4 @@ -1281,6 +1299,7 @@ testMissingClient brig = do . responseHeaders -- The testAddMultipleTemporary test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- Legacy (galley) -- -- Add temporary client, check that all services (both galley and @@ -1338,6 +1357,8 @@ testAddMultipleTemporary brig galley cannon = do . zUser u pure $ Vec.length <$> (preview _Array =<< responseJsonMaybe @Value r) +-- @END + testPreKeyRace :: Brig -> Http () testPreKeyRace brig = do uid <- userId <$> randomUser brig @@ -1409,9 +1430,9 @@ instance A.ToJSON DPoPClaimsSet where ins k v (Object o) = Object $ M.insert k (A.toJSON v) o ins _ _ a = a -testCreateAccessToken :: Opts.Opts -> Nginz -> Brig -> Http () +testCreateAccessToken :: Opt.Opts -> Nginz -> Brig -> Http () testCreateAccessToken opts n brig = do - let localDomain = opts ^. Opt.optionSettings & Opt.setFederationDomain + let localDomain = opts.settings.federationDomain (u, tid) <- Util.createUserWithTeam' brig handle <- do Just h <- userHandle <$> Util.setRandomHandle brig u diff --git a/services/brig/test/integration/API/User/Connection.hs b/services/brig/test/integration/API/User/Connection.hs index e9023104eb9..76aebdaff09 100644 --- a/services/brig/test/integration/API/User/Connection.hs +++ b/services/brig/test/integration/API/User/Connection.hs @@ -26,7 +26,6 @@ import API.User.Util import Bilge hiding (accept, timeout) import Bilge.Assert import Brig.Data.Connection (remoteConnectionInsert) -import Brig.Options qualified as Opt import Cassandra qualified as DB import Control.Arrow ((&&&)) import Data.ByteString.Conversion @@ -34,13 +33,13 @@ import Data.Domain import Data.Id import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) import Data.Qualified -import Data.Time.Clock (getCurrentTime) import Data.UUID.V4 qualified as UUID import Imports import Network.Wai.Utilities.Error qualified as Error import Test.Tasty hiding (Timeout) import Test.Tasty.HUnit import Util +import Util.Timeout import Wire.API.Connection import Wire.API.Conversation import Wire.API.Federation.API.Brig @@ -51,7 +50,7 @@ import Wire.API.User as User tests :: ConnectionLimit -> - Opt.Timeout -> + Timeout -> Manager -> Brig -> Cannon -> diff --git a/services/brig/test/integration/API/User/Handles.hs b/services/brig/test/integration/API/User/Handles.hs index db34f2f9277..c505e9b824a 100644 --- a/services/brig/test/integration/API/User/Handles.hs +++ b/services/brig/test/integration/API/User/Handles.hs @@ -41,18 +41,19 @@ import Imports import Network.Wai.Utilities.Error qualified as Error import Network.Wai.Utilities.Error qualified as Wai import Test.Tasty hiding (Timeout) -import Test.Tasty.Cannon hiding (Cannon) +import Test.Tasty.Cannon hiding (Cannon, Timeout) import Test.Tasty.Cannon qualified as WS import Test.Tasty.HUnit import UnliftIO (mapConcurrently) import Util +import Util.Timeout import Wire.API.Internal.Notification hiding (target) import Wire.API.Team.Feature (FeatureStatus (..)) import Wire.API.Team.SearchVisibility import Wire.API.User import Wire.API.User.Handle -tests :: ConnectionLimit -> Opt.Timeout -> Opt.Opts -> Manager -> Brig -> Cannon -> Galley -> TestTree +tests :: ConnectionLimit -> Timeout -> Opt.Opts -> Manager -> Brig -> Cannon -> Galley -> TestTree tests _cl _at conf p b c g = testGroup "handles" @@ -61,14 +62,15 @@ tests _cl _at conf p b c g = test p "handles/query" $ testHandleQuery conf b, test p "handles/query - team-search-visibility SearchVisibilityStandard" $ testHandleQuerySearchVisibilityStandard conf b, test p "handles/query - team-search-visibility SearchVisibilityNoNameOutsideTeam" $ testHandleQuerySearchVisibilityNoNameOutsideTeam conf b g, - test p "GET /users/handles/ 200" $ testGetUserByUnqualifiedHandle b, - test p "GET /users/handles/ 404" $ testGetUserByUnqualifiedHandleFailure b, + test p "GET /handles/ 200" $ testGetUserByUnqualifiedHandle b, + test p "GET /handles/ 404" $ testGetUserByUnqualifiedHandleFailure b, test p "GET /users/by-handle// : 200" $ testGetUserByQualifiedHandle b, test p "GET /users/by-handle// : 404" $ testGetUserByQualifiedHandleFailure b, test p "GET /users/by-handle// : no federation" $ testGetUserByQualifiedHandleNoFederation conf b ] -- The next line contains a mapping from the testHandleUpdate test to the following test standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- The test validates various updates to the user's handle. First, it attempts -- to set invalid handles. This fails. Then it successfully sets a valid handle. @@ -103,7 +105,7 @@ testHandleUpdate brig cannon = do -- The owner of the handle can always retry the update put (brig . path "/self/handle" . contentJson . zUser uid . zConn "c" . body update) !!! const 200 === statusCode - Bilge.head (brig . paths ["users", "handles", toByteString' hdl] . zUser uid) + Bilge.head (brig . paths ["handles", toByteString' hdl] . zUser uid) !!! const 200 === statusCode -- For other users, the handle is unavailable uid2 <- userId <$> randomUser brig @@ -118,7 +120,7 @@ testHandleUpdate brig cannon = do let update2 = RequestBodyLBS . encode $ HandleUpdate hdl2 put (brig . path "/self/handle" . contentJson . zUser uid . zConn "c" . body update2) !!! const 200 === statusCode - Bilge.head (brig . paths ["users", "handles", toByteString' hdl] . zUser uid) + Bilge.head (brig . paths ["handles", toByteString' hdl] . zUser uid) !!! const 404 === statusCode -- The owner appears by the new handle in search Search.refreshIndex brig @@ -139,6 +141,8 @@ testHandleUpdate brig cannon = do put (brig . path "/self/handle" . contentJson . zUser uid2 . zConn "c" . body update) !!! const 200 === statusCode +-- @END + testHandleRace :: Brig -> Http () testHandleRace brig = do us <- replicateM 10 (userId <$> randomUser brig) @@ -159,7 +163,7 @@ testHandleQuery opts brig = do uid <- userId <$> randomUser brig hdl <- randomHandle -- Query for the handle availability (must be free) - Bilge.head (brig . paths ["users", "handles", toByteString' hdl] . zUser uid) + Bilge.head (brig . paths ["handles", toByteString' hdl] . zUser uid) !!! const 404 === statusCode -- Set handle let update = RequestBodyLBS . encode $ HandleUpdate hdl @@ -170,7 +174,7 @@ testHandleQuery opts brig = do const 200 === statusCode const (Just (fromJust $ parseHandle hdl)) === (userHandle <=< responseJsonMaybe) -- Query for the handle availability (must be taken) - Bilge.head (brig . paths ["users", "handles", toByteString' hdl] . zUser uid) + Bilge.head (brig . paths ["handles", toByteString' hdl] . zUser uid) !!! const 200 === statusCode -- Query user profiles by handles get (apiVersion "v1" . brig . path "/users" . queryItem "handles" (toByteString' hdl) . zUser uid) !!! do @@ -193,7 +197,7 @@ testHandleQuery opts brig = do -- Usually, you can search outside your team assertCanFind brig user3 user4 -- Usually, you can search outside your team but not if this config option is set - let newOpts = opts & ((Opt.optionSettings . Opt.searchSameTeamOnly) ?~ True) + let newOpts = opts & ((Opt.settingsLens . Opt.searchSameTeamOnlyLens) ?~ True) withSettingsOverrides newOpts $ assertCannotFind brig user3 user4 diff --git a/services/brig/test/integration/API/User/PasswordReset.hs b/services/brig/test/integration/API/User/PasswordReset.hs deleted file mode 100644 index b478af41749..00000000000 --- a/services/brig/test/integration/API/User/PasswordReset.hs +++ /dev/null @@ -1,126 +0,0 @@ -{-# 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.User.PasswordReset - ( tests, - ) -where - -import API.User.Util -import Bilge hiding (accept, timeout) -import Bilge.Assert -import Brig.Options qualified as Opt -import Cassandra qualified as DB -import Data.Aeson as A -import Data.Aeson.KeyMap qualified as KeyMap -import Data.Misc -import Imports -import Test.Tasty hiding (Timeout) -import Util -import Wire.API.User -import Wire.API.User.Auth - -tests :: - DB.ClientState -> - ConnectionLimit -> - Opt.Timeout -> - Opt.Opts -> - Manager -> - Brig -> - Cannon -> - Galley -> - TestTree -tests _cs _cl _at _conf p b _c _g = - testGroup - "password-reset" - [ test p "post /password-reset[/complete] - 201[/200]" $ testPasswordReset b, - test p "post /password-reset after put /access/self/email - 400" $ testPasswordResetAfterEmailUpdate b, - test p "post /password-reset/complete - password too short - 400" $ testPasswordResetInvalidPasswordLength b - ] - -testPasswordReset :: Brig -> Http () -testPasswordReset brig = do - u <- randomUser brig - let Just email = userEmail u - let uid = userId u - -- initiate reset - let newpw = plainTextPassword8Unsafe "newsecret" - do - initiatePasswordReset brig email !!! const 201 === statusCode - -- even though a password reset is now in progress - -- we expect a successful response from a subsequent request to not leak any information - -- about the requested email - initiatePasswordReset brig email !!! const 201 === statusCode - - passwordResetData <- preparePasswordReset brig email uid newpw - completePasswordReset brig passwordResetData !!! const 200 === statusCode - -- try login - login brig (defEmailLogin email) PersistentCookie - !!! const 403 === statusCode - login - brig - (PasswordLogin (PasswordLoginData (LoginByEmail email) (plainTextPassword8To6 newpw) Nothing Nothing)) - PersistentCookie - !!! const 200 === statusCode - -- reset password again to the same new password, get 400 "must be different" - do - initiatePasswordReset brig email !!! const 201 === statusCode - passwordResetData <- preparePasswordReset brig email uid newpw - completePasswordReset brig passwordResetData !!! const 409 === statusCode - -testPasswordResetAfterEmailUpdate :: Brig -> Http () -testPasswordResetAfterEmailUpdate brig = do - u <- randomUser brig - let uid = userId u - let Just email = userEmail u - eml <- randomEmail - initiateEmailUpdateLogin brig eml (emailLogin email defPassword Nothing) uid !!! const 202 === statusCode - initiatePasswordReset brig email !!! const 201 === statusCode - passwordResetData <- preparePasswordReset brig email uid (plainTextPassword8Unsafe "newsecret") - -- activate new email - activateEmail brig eml - checkEmail brig uid eml - -- attempting to complete password reset should fail - completePasswordReset brig passwordResetData !!! const 400 === statusCode - -testPasswordResetInvalidPasswordLength :: Brig -> Http () -testPasswordResetInvalidPasswordLength brig = do - u <- randomUser brig - let Just email = userEmail u - let uid = userId u - -- for convenience, we create a valid password first that we replace with an invalid one in the JSON later - let newpw = plainTextPassword8Unsafe "newsecret" - initiatePasswordReset brig email !!! const 201 === statusCode - passwordResetData <- preparePasswordReset brig email uid newpw - let shortPassword = String "123456" - let reqBody = toJSON passwordResetData & addJsonKey "password" shortPassword - postCompletePasswordReset reqBody !!! const 400 === statusCode - where - addJsonKey :: Key -> Value -> Value -> Object - addJsonKey key val (Object xs) = KeyMap.insert key val xs - addJsonKey _ _ _ = error "invalid JSON object" - - postCompletePasswordReset :: Object -> (MonadHttp m) => m ResponseLBS - postCompletePasswordReset bdy = - post - ( brig - . path "/password-reset/complete" - . contentJson - . body (RequestBodyLBS (encode bdy)) - ) diff --git a/services/brig/test/integration/API/User/Property.hs b/services/brig/test/integration/API/User/Property.hs deleted file mode 100644 index 071ea2d356d..00000000000 --- a/services/brig/test/integration/API/User/Property.hs +++ /dev/null @@ -1,170 +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 API.User.Property - ( tests, - ) -where - -import API.User.Util -import Bilge hiding (accept, timeout) -import Bilge.Assert -import Brig.Options -import Brig.Options qualified as Opt -import Data.Aeson -import Data.ByteString.Char8 qualified as C -import Data.String.Conversions -import Data.Text qualified as T -import Imports -import Network.Wai.Utilities.Error qualified as Error -import Test.Tasty hiding (Timeout) -import Util -import Wire.API.User - -tests :: ConnectionLimit -> Opt.Timeout -> Opt.Opts -> Manager -> Brig -> Cannon -> Galley -> TestTree -tests _cl _at opts p b _c _g = - testGroup - "property" - [ test p "put/get /properties/:key - 200" $ testSetGetProperty b, - test p "delete /properties/:key - 200" $ testDeleteProperty b, - test p "get /properties - 200" $ testListPropertyKeys b, - test p "get /properties-values - 200" $ testListPropertyKeysAndValues b, - test p "delete /properties - 200" $ testClearProperties b, - test p "put /properties/:key - 403" $ testPropertyLimits opts b, - test p "size limits" $ testSizeLimits opts b - ] - -testSetGetProperty :: Brig -> Http () -testSetGetProperty brig = do - u <- randomUser brig - setProperty brig (userId u) "foo" objectProp - !!! const 200 === statusCode - getProperty brig (userId u) "foo" !!! do - const 200 === statusCode - const (Just objectProp) === responseJsonMaybe - -- String Literals - setProperty brig (userId u) "foo" (String "foo") - !!! const 200 === statusCode - getProperty brig (userId u) "foo" !!! do - const 200 === statusCode - const (Just "\"foo\"") === responseBody - -- Boolean Literals - setProperty brig (userId u) "foo" (Bool True) - !!! const 200 === statusCode - getProperty brig (userId u) "foo" !!! do - const 200 === statusCode - const (Just "true") === responseBody - -- Numeric Literals - setProperty brig (userId u) "foo" (Number 42) - !!! const 200 === statusCode - getProperty brig (userId u) "foo" !!! do - const 200 === statusCode - const (Just "42") === responseBody - where - objectProp = - object - [ "key.1" .= ("val1" :: Text), - "key.2" .= ("val2" :: Text) - ] - -testDeleteProperty :: Brig -> Http () -testDeleteProperty brig = do - u <- randomUser brig - setProperty brig (userId u) "foo" (Bool True) - !!! const 200 === statusCode - deleteProperty brig (userId u) "foo" - !!! const 200 === statusCode - getProperty brig (userId u) "foo" - !!! const 404 === statusCode - -testListPropertyKeys :: Brig -> Http () -testListPropertyKeys = - testListProperties' - "/properties" - (toJSON ["bar" :: Text, "foo"]) - -testListPropertyKeysAndValues :: Brig -> Http () -testListPropertyKeysAndValues = - testListProperties' - "/properties-values" - (object ["bar" .= String "hello", "foo" .= True]) - -testListProperties' :: ByteString -> Value -> Brig -> Http () -testListProperties' endpoint rval brig = do - u <- randomUser brig - setProperty brig (userId u) "foo" (Bool True) - !!! const 200 === statusCode - setProperty brig (userId u) "bar" (String "hello") - !!! const 200 === statusCode - get (brig . path endpoint . zUser (userId u)) !!! do - const 200 === statusCode - const (Just rval) === responseJsonMaybe - -testClearProperties :: Brig -> Http () -testClearProperties brig = do - u <- randomUser brig - setProperty brig (userId u) "foo" (Bool True) - !!! const 200 === statusCode - setProperty brig (userId u) "bar" (String "hello") - !!! const 200 === statusCode - delete (brig . path "/properties" . zUser (userId u) . zConn "conn") - !!! const 200 === statusCode - getProperty brig (userId u) "foo" - !!! const 404 === statusCode - getProperty brig (userId u) "bar" - !!! const 404 === statusCode - -testPropertyLimits :: Opt.Opts -> Brig -> Http () -testPropertyLimits opts brig = do - u <- randomUser brig - let maxKeyLen = fromIntegral $ fromMaybe defMaxKeyLen . setPropertyMaxKeyLen $ optSettings opts - maxValueLen = fromIntegral $ fromMaybe defMaxValueLen . setPropertyMaxValueLen $ optSettings opts - -- Maximum key length - setProperty brig (userId u) (C.replicate (maxKeyLen + 1) 'x') (String "y") !!! do - const 403 === statusCode - const (Just "property-key-too-large") === fmap Error.label . responseJsonMaybe - -- Maximum value length - setProperty brig (userId u) "foo" (String (T.replicate (maxValueLen + 1) "x")) !!! do - const 403 === statusCode - const (Just "property-value-too-large") === fmap Error.label . responseJsonMaybe - -- Maximum count - forM_ [1 .. 16 :: Int] $ \i -> - setProperty brig (userId u) ("foo" <> C.pack (show i)) (Number (fromIntegral i)) - !!! const 200 === statusCode - setProperty brig (userId u) "bar" (String "hello") !!! do - const 403 === statusCode - const (Just "too-many-properties") === fmap Error.label . responseJsonMaybe - -testSizeLimits :: (HasCallStack) => Opt.Opts -> Brig -> Http () -testSizeLimits opts brig = do - let maxKeyLen = fromIntegral $ fromMaybe defMaxKeyLen . setPropertyMaxKeyLen $ optSettings opts - maxValueLen = fromIntegral $ fromMaybe defMaxValueLen . setPropertyMaxValueLen $ optSettings opts - badKey = cs $ replicate (maxKeyLen + 2) '_' - okKey = cs $ replicate (maxKeyLen - 2) '_' - -- we use String Values here that have an encoding that is 2 characters longer than - -- the decoded string value (because of the quotes). - badValue = String . cs $ replicate maxValueLen '_' - okValue = String . cs $ replicate (maxValueLen - 3) '_' - u <- randomUser brig - setProperty brig (userId u) okKey okValue - !!! const 200 === statusCode - setProperty brig (userId u) badKey okValue - !!! const 403 === statusCode - setProperty brig (userId u) okKey badValue - !!! const 403 === statusCode - setProperty brig (userId u) badKey badValue - !!! const 403 === statusCode diff --git a/services/brig/test/integration/API/User/RichInfo.hs b/services/brig/test/integration/API/User/RichInfo.hs index cad0d8053b6..453ba3394d9 100644 --- a/services/brig/test/integration/API/User/RichInfo.hs +++ b/services/brig/test/integration/API/User/RichInfo.hs @@ -34,11 +34,12 @@ import Imports import Test.Tasty hiding (Timeout) import Test.Tasty.HUnit import Util +import Util.Timeout import Wire.API.Team.Permission import Wire.API.User import Wire.API.User.RichInfo -tests :: ConnectionLimit -> Opt.Timeout -> Opt.Opts -> Manager -> Brig -> Cannon -> Galley -> TestTree +tests :: ConnectionLimit -> Timeout -> Opt.Opts -> Manager -> Brig -> Cannon -> Galley -> TestTree tests _cl _at conf p b _c g = testGroup "rich info" @@ -116,7 +117,7 @@ testDedupeDuplicateFieldNames brig = do testRichInfoSizeLimit :: (HasCallStack) => Brig -> Opt.Opts -> Http () testRichInfoSizeLimit brig conf = do - let maxSize :: Int = setRichInfoLimit $ optSettings conf + let maxSize :: Int = conf.settings.richInfoLimit (owner, _) <- createUserWithTeam brig let bad1 = mkRichInfoAssocList diff --git a/services/brig/test/integration/API/User/Util.hs b/services/brig/test/integration/API/User/Util.hs index d862c73ddd1..cb6f1aa1712 100644 --- a/services/brig/test/integration/API/User/Util.hs +++ b/services/brig/test/integration/API/User/Util.hs @@ -23,8 +23,6 @@ module API.User.Util where import Bilge hiding (accept, timeout) import Bilge.Assert -import Brig.Options (Opts) -import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) import Brig.ZAuth (Token) import Cassandra qualified as DB import Codec.MIME.Type qualified as MIME @@ -42,16 +40,11 @@ import Data.Handle (parseHandle) import Data.Id import Data.Kind import Data.List1 qualified as List1 -import Data.Misc import Data.Qualified import Data.Range (unsafeRange) import Data.String.Conversions -import Data.Text.Ascii qualified as Ascii import Data.Vector qualified as Vec import Data.ZAuth.Token qualified as ZAuth -import Federation.Util (withTempMockFederator) -import Federator.MockServer (FederatedRequest (..)) -import GHC.TypeLits (KnownSymbol) import Imports import Test.Tasty.Cannon qualified as WS import Test.Tasty.HUnit @@ -65,7 +58,7 @@ import Wire.API.Federation.Component import Wire.API.Internal.Notification (Notification (..)) import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.MultiTablePaging (LocalOrRemoteTable, MultiTablePagingState) -import Wire.API.Team.Feature (featureNameBS) +import Wire.API.Team.Feature (IsFeatureConfig, featureNameBS) import Wire.API.Team.Feature qualified as Public import Wire.API.User import Wire.API.User qualified as Public @@ -73,9 +66,7 @@ import Wire.API.User.Activation import Wire.API.User.Auth import Wire.API.User.Client import Wire.API.User.Client.DPoPAccessToken (Proof) -import Wire.API.User.Client.Prekey import Wire.API.User.Handle -import Wire.API.User.Password import Wire.VerificationCode qualified as Code import Wire.VerificationCodeStore.Cassandra qualified as VerificationCodeStore @@ -92,7 +83,7 @@ checkHandles brig uid hs num = let hs' = unsafeRange hs num' = unsafeRange num js = RequestBodyLBS $ encode $ CheckHandles hs' num' - in post (brig . path "/users/handles" . contentJson . zUser uid . body js) + in post (brig . path "/handles" . contentJson . zUser uid . body js) randomUserWithHandle :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => @@ -135,16 +126,7 @@ registerUser name brig = do ] post (brig . path "/register" . contentJson . body p) -initiatePasswordReset :: Brig -> Email -> (MonadHttp m) => m ResponseLBS -initiatePasswordReset brig email = - post - ( brig - . path "/password-reset" - . contentJson - . body (RequestBodyLBS . encode $ NewPasswordReset email) - ) - -activateEmail :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> Email -> m () +activateEmail :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> EmailAddress -> m () activateEmail brig email = do act <- getActivationCode brig (Left email) case act of @@ -154,13 +136,13 @@ activateEmail brig email = do const 200 === statusCode const (Just False) === fmap activatedFirst . responseJsonMaybe -checkEmail :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> UserId -> Email -> m () +checkEmail :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> UserId -> EmailAddress -> m () checkEmail brig uid expectedEmail = get (brig . path "/self" . zUser uid) !!! do const 200 === statusCode const (Just expectedEmail) === (userEmail <=< responseJsonMaybe) -initiateEmailUpdateLogin :: Brig -> Email -> Login -> UserId -> (MonadIO m, MonadCatch m, MonadHttp m) => m ResponseLBS +initiateEmailUpdateLogin :: Brig -> EmailAddress -> Login -> UserId -> (MonadIO m, MonadCatch m, MonadHttp m) => m ResponseLBS initiateEmailUpdateLogin brig email loginCreds uid = do (cky, tok) <- do rsp <- @@ -169,7 +151,7 @@ initiateEmailUpdateLogin brig email loginCreds uid = do pure (decodeCookie rsp, decodeToken rsp) initiateEmailUpdateCreds brig email (cky, tok) uid -initiateEmailUpdateCreds :: Brig -> Email -> (Bilge.Cookie, Brig.ZAuth.Token ZAuth.Access) -> UserId -> (MonadHttp m) => m ResponseLBS +initiateEmailUpdateCreds :: Brig -> EmailAddress -> (Bilge.Cookie, Brig.ZAuth.Token ZAuth.Access) -> UserId -> (MonadHttp m) => m ResponseLBS initiateEmailUpdateCreds brig email (cky, tok) uid = do put $ unversioned @@ -180,38 +162,13 @@ initiateEmailUpdateCreds brig email (cky, tok) uid = do . zUser uid . Bilge.json (EmailUpdate email) -initiateEmailUpdateNoSend :: (MonadHttp m, MonadIO m, MonadCatch m) => Brig -> Email -> UserId -> m ResponseLBS +initiateEmailUpdateNoSend :: (MonadHttp m, MonadIO m, MonadCatch m) => Brig -> EmailAddress -> UserId -> m ResponseLBS initiateEmailUpdateNoSend brig email uid = let emailUpdate = RequestBodyLBS . encode $ EmailUpdate email in put (brig . path "/i/self/email" . contentJson . zUser uid . body emailUpdate) - Brig -> - Email -> - UserId -> - PlainTextPassword8 -> - m CompletePasswordReset -preparePasswordReset brig email uid newpw = do - let qry = queryItem "email" (toByteString' email) - r <- get $ brig . path "/i/users/password-reset-code" . qry - let lbs = fromMaybe "" $ responseBody r - let Just pwcode = PasswordResetCode . Ascii.unsafeFromText <$> (lbs ^? key "code" . _String) - let ident = PasswordResetIdentityKey (mkPasswordResetKey uid) - let complete = CompletePasswordReset ident pwcode newpw - pure complete - -completePasswordReset :: Brig -> CompletePasswordReset -> (MonadHttp m) => m ResponseLBS -completePasswordReset brig passwordResetData = - post - ( brig - . path "/password-reset/complete" - . contentJson - . body (RequestBodyLBS $ encode passwordResetData) - ) - -removeBlacklist :: Brig -> Email -> (MonadIO m, MonadHttp m) => m () +removeBlacklist :: Brig -> EmailAddress -> (MonadIO m, MonadHttp m) => m () removeBlacklist brig email = void $ delete (brig . path "/i/users/blacklist" . queryItem "email" (toByteString' email)) @@ -321,14 +278,6 @@ getProperty brig u k = . paths ["/properties", k] . zUser u -deleteProperty :: Brig -> UserId -> ByteString -> (MonadHttp m) => m ResponseLBS -deleteProperty brig u k = - delete $ - brig - . paths ["/properties", k] - . zConn "conn" - . zUser u - countCookies :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> UserId -> CookieLabel -> m (Maybe Int) countCookies brig u label = do r <- @@ -375,50 +324,6 @@ receiveConnectionAction brig fedBrigClient uid1 quid2 action expectedReaction ex res @?= F.NewConnectionResponseOk expectedReaction assertConnectionQualified brig uid1 quid2 expectedRel -sendConnectionAction :: - (HasCallStack) => - Brig -> - Opts -> - UserId -> - Qualified UserId -> - Maybe F.RemoteConnectionAction -> - Relation -> - Http () -sendConnectionAction brig opts uid1 quid2 reaction expectedRel = do - let mockConnectionResponse = F.NewConnectionResponseOk reaction - mockResponse = encode mockConnectionResponse - (res, reqs) <- - liftIO . withTempMockFederator opts mockResponse $ - postConnectionQualified brig uid1 quid2 - - liftIO $ do - req <- assertOne reqs - frTargetDomain req @?= qDomain quid2 - frComponent req @?= Brig - frRPC req @?= "send-connection-action" - eitherDecode (frBody req) - @?= Right (F.NewConnectionRequest uid1 Nothing (qUnqualified quid2) F.RemoteConnect) - - liftIO $ assertBool "postConnectionQualified failed" $ statusCode res `elem` [200, 201] - assertConnectionQualified brig uid1 quid2 expectedRel - -sendConnectionUpdateAction :: - (HasCallStack) => - Brig -> - Opts -> - UserId -> - Qualified UserId -> - Maybe F.RemoteConnectionAction -> - Relation -> - Http () -sendConnectionUpdateAction brig opts uid1 quid2 reaction expectedRel = do - let mockConnectionResponse = F.NewConnectionResponseOk reaction - mockResponse = encode mockConnectionResponse - void $ - liftIO . withTempMockFederator opts mockResponse $ - putConnectionQualified brig uid1 quid2 expectedRel !!! const 200 === statusCode - assertConnectionQualified brig uid1 quid2 expectedRel - assertEmailVisibility :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> User -> User -> Bool -> m () assertEmailVisibility brig a b visible = get (apiVersion "v1" . brig . paths ["users", pack . show $ userId b] . zUser (userId a)) !!! do @@ -462,25 +367,6 @@ downloadAsset c usr ast = . zConn "conn" ) -requestLegalHoldDevice :: Brig -> UserId -> UserId -> LastPrekey -> (MonadHttp m) => m ResponseLBS -requestLegalHoldDevice brig requesterId targetUserId lastPrekey' = - post $ - brig - . paths ["i", "clients", "legalhold", toByteString' targetUserId, "request"] - . contentJson - . body payload - where - payload = - RequestBodyLBS . encode $ - LegalHoldClientRequest requesterId lastPrekey' - -deleteLegalHoldDevice :: Brig -> UserId -> (MonadHttp m) => m ResponseLBS -deleteLegalHoldDevice brig uid = - delete $ - brig - . paths ["i", "clients", "legalhold", toByteString' uid] - . contentJson - matchDeleteUserNotification :: Qualified UserId -> Notification -> Assertion matchDeleteUserNotification quid n = do let j = Object $ List1.head (ntfPayload n) @@ -517,7 +403,7 @@ generateVerificationCode' brig req = do setTeamSndFactorPasswordChallenge :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Galley -> TeamId -> Public.FeatureStatus -> m () setTeamSndFactorPasswordChallenge galley tid status = do - let js = RequestBodyLBS $ encode $ Public.WithStatusNoLock status Public.SndFactorPasswordChallengeConfig Public.FeatureTTLUnlimited + let js = RequestBodyLBS $ encode $ Public.Feature status Public.SndFactorPasswordChallengeConfig put (galley . paths ["i", "teams", toByteString' tid, "features", featureNameBS @Public.SndFactorPasswordChallengeConfig] . contentJson . body js) !!! const 200 === statusCode setTeamFeatureLockStatus :: @@ -526,7 +412,7 @@ setTeamFeatureLockStatus :: MonadIO m, MonadHttp m, HasCallStack, - KnownSymbol (Public.FeatureSymbol cfg) + IsFeatureConfig cfg ) => Galley -> TeamId -> diff --git a/services/brig/test/integration/API/UserPendingActivation.hs b/services/brig/test/integration/API/UserPendingActivation.hs index 81f6e995c51..b82eb251957 100644 --- a/services/brig/test/integration/API/UserPendingActivation.hs +++ b/services/brig/test/integration/API/UserPendingActivation.hs @@ -24,7 +24,7 @@ module API.UserPendingActivation where import API.Team.Util (getTeam) import Bilge hiding (query) import Bilge.Assert (( Brig -> ScimToken -> TeamId -> Scim.User.User SparTag -> Email -> HttpT IO (WithMeta (WithId UserId (Scim.User.User SparTag)), Invitation, InvitationCode) +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 (inInvitation inv) + Just inviteeCode <- getInvitationCode brig' tid inv.invitationId pure (scimStoredUser, inv, inviteeCode) assertUserExist :: (HasCallStack) => String -> ClientState -> UserId -> Bool -> HttpT IO () @@ -125,7 +125,7 @@ assertUserExist msg db' uid shouldExist = liftIO $ do waitUserExpiration :: (MonadUnliftIO m) => Opts -> m () waitUserExpiration opts' = do - let timeoutSecs = round @Double . realToFrac . setTeamInvitationTimeout . optSettings $ opts' + let timeoutSecs = round @Double . realToFrac $ opts'.settings.teamInvitationTimeout Control.Exception.assert (timeoutSecs < 30) $ do threadDelay $ (timeoutSecs + 3) * 1_000_000 @@ -141,15 +141,15 @@ userExists uid = do usersSelect :: PrepQuery R (Identity UserId) (UserId, Maybe AccountStatus) usersSelect = "SELECT id, status FROM user where id = ?" -getInvitationByEmail :: Brig -> Email -> Http Invitation +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 @@ -194,7 +194,7 @@ randomScimUserWithSubjectAndRichInfo :: 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 + _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) <- @@ -213,23 +213,16 @@ randomScimUserWithSubjectAndRichInfo richInfo = do ( (Scim.User.empty @SparTag userSchemas ("scimuser_" <> suffix) (ScimUserExtra richInfo)) { Scim.User.displayName = Just ("ScimUser" <> suffix), Scim.User.externalId = Just externalId, - Scim.User.emails = emails, Scim.User.phoneNumbers = phones }, subj ) -randomScimEmail :: (MonadRandom m) => m Email.Email +randomScimEmail :: (MonadRandom m) => m EmailAddress randomScimEmail = do - let typ :: Maybe Text = Nothing - -- TODO: where should we catch users with more than one - -- primary email? - primary :: Maybe Scim.ScimBool = Nothing - value :: Email.EmailAddress2 <- do - localpart <- cs <$> replicateM 15 (getRandomR ('a', 'z')) - domainpart <- (<> ".com") . cs <$> replicateM 15 (getRandomR ('a', 'z')) - pure . Email.EmailAddress2 $ Email.unsafeEmailAddress localpart domainpart - pure Email.Email {..} + 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 @@ -344,7 +337,7 @@ createToken spar zusr payload = do Email -> Name -> InvitationCode -> Bool -> Http () +registerInvitation :: Brig -> EmailAddress -> Name -> InvitationCode -> Bool -> Http () registerInvitation brig email name inviteeCode shouldSucceed = do void $ post @@ -355,7 +348,7 @@ registerInvitation brig email name inviteeCode shouldSucceed = do ) Email -> InvitationCode -> Aeson.Value +acceptWithName :: Name -> EmailAddress -> InvitationCode -> Aeson.Value acceptWithName name email code = Aeson.object [ "name" Aeson..= fromName name, diff --git a/services/brig/test/integration/Federation/Util.hs b/services/brig/test/integration/Federation/Util.hs index ace4d04fbbe..c7041eb96e5 100644 --- a/services/brig/test/integration/Federation/Util.hs +++ b/services/brig/test/integration/Federation/Util.hs @@ -98,9 +98,6 @@ generateClientPrekeys brig prekeys = do clients <- traverse (responseJsonError <=< addClient brig (qUnqualified quser)) nclients pure (quser, zipWith mkClientPrekey prekeys clients) -assertRightT :: (MonadIO m, Show a, HasCallStack) => ExceptT a m b -> m b -assertRightT = assertRight <=< runExceptT - getConvQualified :: Galley -> UserId -> Qualified ConvId -> Http ResponseLBS getConvQualified g u (Qualified cnvId domain) = get $ @@ -116,32 +113,3 @@ connectUsersEnd2End brig1 brig2 quid1 quid2 = do !!! const 201 === statusCode putConnectionQualified brig2 (qUnqualified quid2) quid1 Accepted !!! const 200 === statusCode - -sendCommitBundle :: (HasCallStack) => FilePath -> FilePath -> Maybe FilePath -> Galley -> UserId -> ClientId -> ByteString -> Http () -sendCommitBundle tmp subGroupStateFn welcomeFn galley uid cid commit = do - subGroupStateRaw <- liftIO $ BS.readFile $ tmp subGroupStateFn - subGroupState <- either (liftIO . assertFailure . T.unpack) pure . decodeMLS' $ subGroupStateRaw - subCommit <- either (liftIO . assertFailure . T.unpack) pure . decodeMLS' $ commit - mbWelcome <- - for - welcomeFn - $ \fn -> do - bs <- liftIO $ BS.readFile $ tmp fn - msg :: Message <- either (liftIO . assertFailure . T.unpack) pure . decodeMLS' $ bs - case msg.content of - MessageWelcome welcome -> pure welcome - _ -> liftIO . assertFailure $ "Expected a welcome" - - let subGroupBundle = CommitBundle subCommit mbWelcome subGroupState - post - ( galley - . paths - ["mls", "commit-bundles"] - . zUser uid - . zClient cid - . zConn "conn" - . header "Z-Type" "access" - . Bilge.content "message/mls" - . lbytes (encodeMLS subGroupBundle) - ) - !!! const 201 === statusCode diff --git a/services/brig/test/integration/Index/Create.hs b/services/brig/test/integration/Index/Create.hs index 51961e9533d..fab0ae91860 100644 --- a/services/brig/test/integration/Index/Create.hs +++ b/services/brig/test/integration/Index/Create.hs @@ -24,7 +24,7 @@ import Brig.Index.Options import Brig.Index.Options qualified as IndexOpts import Brig.Options (Opts (galley)) import Brig.Options qualified as BrigOpts -import Control.Lens ((.~), (^.)) +import Control.Lens ((.~)) import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import Database.Bloodhound qualified as ES @@ -49,7 +49,7 @@ spec brigOpts = testCreateIndexWhenNotPresent :: BrigOpts.Opts -> Assertion testCreateIndexWhenNotPresent brigOpts = do - let (ES.Server esURL) = brigOpts ^. BrigOpts.elasticsearchL . BrigOpts.urlL + let (ES.Server esURL) = brigOpts.elasticsearch.url case parseURI strictURIParserOptions (Text.encodeUtf8 esURL) of Left e -> fail $ "Invalid ES URL: " <> show esURL <> "\nerror: " <> show e Right esURI -> do @@ -90,7 +90,7 @@ testCreateIndexWhenNotPresent brigOpts = do testCreateIndexWhenPresent :: BrigOpts.Opts -> Assertion testCreateIndexWhenPresent brigOpts = do - let (ES.Server esURL) = brigOpts ^. BrigOpts.elasticsearchL . BrigOpts.urlL + let (ES.Server esURL) = brigOpts.elasticsearch.url case parseURI strictURIParserOptions (Text.encodeUtf8 esURL) of Left e -> fail $ "Invalid ES URL: " <> show esURL <> "\nerror: " <> show e Right esURI -> do diff --git a/services/brig/test/integration/Run.hs b/services/brig/test/integration/Run.hs index 1b3e0cd563d..36adc72a8ec 100644 --- a/services/brig/test/integration/Run.hs +++ b/services/brig/test/integration/Run.hs @@ -123,7 +123,7 @@ runTests iConf brigOpts otherArgs = do let Opts.TurnServersFiles turnFile turnFileV2 = case Opts.serversSource $ Opts.turn brigOpts of Opts.TurnSourceFiles files -> files Opts.TurnSourceDNS _ -> error "The integration tests can only be run when TurnServers are sourced from files" - localDomain = brigOpts ^. Opts.optionSettings . Opts.federationDomain + localDomain = brigOpts.settings.federationDomain awsOpts = Opts.aws brigOpts lg <- Logger.new Logger.defSettings -- TODO: use mkLogger'? db <- defInitCassandra (brigOpts.cassandra) lg diff --git a/services/brig/test/integration/SMTP.hs b/services/brig/test/integration/SMTP.hs index 4911ffbcebc..95b50ac5e4b 100644 --- a/services/brig/test/integration/SMTP.hs +++ b/services/brig/test/integration/SMTP.hs @@ -215,9 +215,6 @@ mailStoringApp receivedMailRef mail = do mailRejectingApp :: Postie.Application mailRejectingApp = const (pure Postie.Rejected) -mailAcceptingApp :: Postie.Application -mailAcceptingApp = const (pure Postie.Accepted) - delayingApp :: (TimeUnit t) => t -> Postie.Application delayingApp delay = const diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index f2aae1e00de..18f9eef6912 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -25,15 +25,15 @@ module Util where import Bilge hiding (host, port) import Bilge.Assert import Brig.AWS.Types -import Brig.App (applog, fsWatcher, sftEnv, turnEnv) +import Brig.App (Env (..)) import Brig.Calling as Calling -import Brig.Options qualified as Opt +import Brig.Options as Opt import Brig.Run qualified as Run import Brig.Types.Activation import Brig.ZAuth qualified as ZAuth import Control.Concurrent.Async import Control.Exception (throw) -import Control.Lens ((^.), (^?), (^?!)) +import Control.Lens ((^?), (^?!)) import Control.Monad.Catch (MonadCatch, MonadMask) import Control.Monad.Catch qualified as Catch import Control.Monad.State qualified as State @@ -56,9 +56,10 @@ import Data.Handle (Handle (..)) import Data.Id import Data.List1 (List1) import Data.List1 qualified as List1 +import Data.Mailbox import Data.Misc import Data.Proxy -import Data.Qualified hiding (isLocal) +import Data.Qualified import Data.Range import Data.Sequence qualified as Seq import Data.String.Conversions @@ -84,7 +85,6 @@ import Network.Wai qualified as Wai import Network.Wai.Handler.Warp qualified as Warp import Network.Wai.Test (Session) import Network.Wai.Test qualified as WaiTest -import Network.Wai.Utilities.Error qualified as Wai import OpenSSL.BN (randIntegerZeroToNMinusOne) import Servant.Client (ClientError (FailureResponse)) import Servant.Client qualified as Servant @@ -104,7 +104,6 @@ import Test.Tasty.Pending (flakyTestCase) import Text.Printf (printf) import UnliftIO.Async qualified as Async import Util.Options -import Web.Internal.HttpApiData import Wire.API.Connection import Wire.API.Conversation import Wire.API.Conversation.Role (roleNameWireAdmin) @@ -112,7 +111,6 @@ import Wire.API.Federation.API import Wire.API.Federation.Domain import Wire.API.Federation.Version import Wire.API.Internal.Notification -import Wire.API.MLS.SubConversation import Wire.API.Routes.MultiTablePaging import Wire.API.Team.Member hiding (userId) import Wire.API.User hiding (AccountStatus (..)) @@ -187,8 +185,8 @@ runFedClient (FedClient mgr ep) domain = where servantClientMToHttp :: Domain -> Servant.ClientM a -> Http a servantClientMToHttp originDomain action = liftIO $ do - let brigHost = Text.unpack $ ep ^. host - brigPort = fromInteger . toInteger $ ep ^. port + let brigHost = Text.unpack ep.host + brigPort = fromInteger . toInteger $ ep.port baseUrl = Servant.BaseUrl Servant.Http brigHost brigPort "/federation" clientEnv = Servant.ClientEnv mgr baseUrl Nothing (makeClientRequest originDomain) eitherRes <- Servant.runClientM action clientEnv @@ -232,6 +230,12 @@ instance ToJSON SESNotification where ] ] +instance ToJSON Mailbox where + toJSON (Mailbox mName addr) = + case mName of + Nothing -> toJSON addr + Just ns -> String $ "\"" <> T.unwords ns <> "\" <" <> T.decodeUtf8 (toByteString' addr) <> ">" + test :: Manager -> TestName -> Http a -> TestTree test m n h = testCase n (void $ runHttpT m h) @@ -265,8 +269,8 @@ localAndRemoteUserWithConvId brig shouldBeLocal = do let go = do other <- Qualified <$> randomId <*> pure (Domain "far-away.example.com") let convId = one2OneConvId BaseProtocolProteusTag quid other - isLocal = qDomain quid == qDomain convId - if shouldBeLocal == isLocal + isLocalUntagged = qDomain quid == qDomain convId + if shouldBeLocal == isLocalUntagged then pure (qUnqualified quid, other, convId) else go go @@ -311,7 +315,7 @@ createUser' hasPwd name brig = do Text -> Email -> Brig -> Http User +createUserWithEmail :: (HasCallStack) => Text -> EmailAddress -> Brig -> Http User createUserWithEmail name email brig = do r <- postUserWithEmail True True name (Just email) False Nothing Nothing brig @@ -332,7 +336,7 @@ createAnonUserExpiry expires name brig = do r <- post (brig . path "/register" . contentJson . body p) Brig -> Int -> Either Email Phone -> Http () +requestActivationCode :: (HasCallStack) => Brig -> Int -> Either EmailAddress Phone -> Http () requestActivationCode brig expectedStatus ep = post (brig . path "/activate/send" . contentJson . body (RequestBodyLBS . encode $ bdy ep)) !!! const expectedStatus === statusCode @@ -343,7 +347,7 @@ requestActivationCode brig expectedStatus ep = getActivationCode :: (MonadCatch m, MonadHttp m, HasCallStack) => Brig -> - Either Email Phone -> + Either EmailAddress Phone -> m (Maybe (ActivationKey, ActivationCode)) getActivationCode brig ep = do let qry = either (queryItem "email" . toByteString') (queryItem "phone" . toByteString') ep @@ -353,12 +357,6 @@ getActivationCode brig ep = do let acode = ActivationCode . Ascii.unsafeFromText <$> (lbs ^? key "code" . _String) pure $ (,) <$> akey <*> acode -getPhoneLoginCode :: Brig -> Phone -> Http (Maybe LoginCode) -getPhoneLoginCode brig p = do - r <- get $ brig . path "/i/users/login-code" . queryItem "phone" (toByteString' p) - let lbs = fromMaybe "" $ responseBody r - pure (LoginCode <$> (lbs ^? key "code" . _String)) - assertUpdateNotification :: (HasCallStack) => WS.WebSocket -> UserId -> UserUpdate -> IO () assertUpdateNotification ws uid upd = WS.assertMatch (5 # Second) ws $ \n -> do let j = Object $ List1.head (ntfPayload n) @@ -366,6 +364,7 @@ assertUpdateNotification ws uid upd = WS.assertMatch (5 # Second) ws $ \n -> do let u = j ^?! key "user" u ^? key "id" . _String @?= Just (UUID.toText (toUUID uid)) u ^? key "name" . _String @?= fromName <$> uupName upd + u ^? key "text_status" . _String @?= fromTextStatus <$> uupTextStatus upd u ^? key "accent_id" . _Integral @?= fromColourId <$> uupAccentId upd u ^? key "assets" @?= Just (toJSON (uupAssets upd)) @@ -411,7 +410,7 @@ postUserWithEmail :: Bool -> Bool -> Text -> - Maybe Email -> + Maybe EmailAddress -> Bool -> Maybe UserSSOId -> Maybe TeamId -> @@ -536,23 +535,6 @@ decodeToken' r = fromMaybe (error "invalid access_token") $ do data LoginCodeType = LoginCodeSMS | LoginCodeVoice deriving (Eq) -sendLoginCode :: Brig -> Phone -> LoginCodeType -> Bool -> Http ResponseLBS -sendLoginCode b p typ force = - post $ - b - . path "/login/send" - . contentJson - . body js - where - js = - RequestBodyLBS - . encode - $ object - [ "phone" .= fromPhone p, - "voice_call" .= (typ == LoginCodeVoice), - "force" .= force - ] - postConnection :: Brig -> UserId -> UserId -> (MonadHttp m) => m ResponseLBS postConnection brig from to = post $ @@ -641,23 +623,6 @@ createUserWithHandle brig = do -- when using this function. pure (handle, userWithHandle) -getUserInfoFromHandle :: - (MonadIO m, MonadCatch m, MonadHttp m, HasCallStack) => - Brig -> - Domain -> - Handle -> - m UserProfile -getUserInfoFromHandle brig domain handle = do - u <- randomId - responseJsonError - =<< get - ( apiVersion "v1" - . brig - . paths ["users", "by-handle", toByteString' (domainText domain), toByteString' handle] - . zUser u - . expect2xx - ) - addClient :: (MonadHttp m, HasCallStack) => Brig -> @@ -732,47 +697,6 @@ getConversationQualified galley usr cnv = . paths ["conversations", toByteString' (qDomain cnv), toByteString' (qUnqualified cnv)] . zAuthAccess usr "conn" -createMLSConversation :: (MonadHttp m) => Galley -> UserId -> ClientId -> m ResponseLBS -createMLSConversation galley zusr c = do - let conv = - NewConv - [] - mempty - (checked "gossip") - mempty - Nothing - Nothing - Nothing - Nothing - roleNameWireAdmin - BaseProtocolMLSTag - post $ - galley - . path "/conversations" - . zUser zusr - . zConn "conn" - . zClient c - . json conv - -createMLSSubConversation :: - (MonadIO m, MonadHttp m) => - Galley -> - UserId -> - Qualified ConvId -> - SubConvId -> - m ResponseLBS -createMLSSubConversation galley zusr qcnv sconv = - get $ - galley - . paths - [ "conversations", - toByteString' (qDomain qcnv), - toByteString' (qUnqualified qcnv), - "subconversations", - toHeader sconv - ] - . zUser zusr - createConversation :: (MonadHttp m) => Galley -> UserId -> [Qualified UserId] -> m ResponseLBS createConversation galley zusr usersToAdd = do let conv = @@ -824,7 +748,14 @@ isMember g usr cnv = do res <- get $ g - . paths ["i", "conversations", toByteString' cnv, "members", toByteString' (tUnqualified usr)] + . paths + [ "i", + "conversations", + toByteString' (tDomain usr), + toByteString' cnv, + "members", + toByteString' (tUnqualified usr) + ] . expect2xx case responseJsonMaybe res of Nothing -> pure False @@ -883,27 +814,31 @@ zClient = header "Z-Client" . toByteString' zConn :: ByteString -> Request -> Request zConn = header "Z-Connection" -mkEmailRandomLocalSuffix :: (MonadIO m) => Text -> m Email +mkEmailRandomLocalSuffix :: (MonadIO m) => Text -> m EmailAddress mkEmailRandomLocalSuffix e = do uid <- liftIO UUID.nextRandom - case parseEmail e of - Just (Email loc dom) -> pure $ Email (loc <> "+" <> UUID.toText uid) dom + case emailAddressText e of + Just mail -> + pure $ + unsafeEmailAddress + ((localPart mail) <> "+" <> UUID.toASCIIBytes uid) + (domainPart mail) Nothing -> error $ "Invalid email address: " ++ Text.unpack e -- | Generate emails that are in the trusted whitelist of domains whose @+@ suffices count for email -- disambiguation. See also: 'Brig.Email.mkEmailKey'. -randomEmail :: (MonadIO m) => m Email +randomEmail :: (MonadIO m) => m EmailAddress randomEmail = mkSimulatorEmail "success" -- | To test the behavior of email addresses with untrusted domains (two emails are equal even if -- their local part after @+@ differs), we need to generate them. -randomUntrustedEmail :: (MonadIO m) => m Email +randomUntrustedEmail :: (MonadIO m) => m EmailAddress randomUntrustedEmail = do -- NOTE: local part cannot be longer than 64 octets rd <- liftIO (randomIO :: IO Integer) - pure $ Email (Text.pack $ show rd) "zinfra.io" + pure $ unsafeEmailAddress (pack $ show rd) "zinfra.io" -mkSimulatorEmail :: (MonadIO m) => Text -> m Email +mkSimulatorEmail :: (MonadIO m) => Text -> m EmailAddress mkSimulatorEmail loc = mkEmailRandomLocalSuffix (loc <> "@simulator.amazonses.com") randomPhone :: (MonadIO m) => m Phone @@ -921,19 +856,11 @@ randomActivationCode = . printf "%06d" <$> randIntegerZeroToNMinusOne 1000000 -updatePhone :: (HasCallStack) => Brig -> UserId -> Phone -> Http () -updatePhone brig uid phn = do - -- update phone - let phoneUpdate = RequestBodyLBS . encode $ PhoneUpdate phn - put (brig . path "/self/phone" . contentJson . zUser uid . zConn "c" . body phoneUpdate) !!! do - const 400 === statusCode - const (Just "invalid-phone") === fmap Wai.label . responseJsonMaybe - -defEmailLogin :: Email -> Login +defEmailLogin :: EmailAddress -> Login defEmailLogin e = emailLogin e defPassword (Just defCookieLabel) -emailLogin :: Email -> PlainTextPassword6 -> Maybe CookieLabel -> Login -emailLogin e pw cl = PasswordLogin (PasswordLoginData (LoginByEmail e) pw cl Nothing) +emailLogin :: EmailAddress -> PlainTextPassword6 -> Maybe CookieLabel -> Login +emailLogin e pw cl = MkLogin (LoginByEmail e) pw cl Nothing somePrekeys :: [Prekey] somePrekeys = @@ -965,10 +892,6 @@ somePrekeys = Prekey (PrekeyId 26) "pQABARgaAqEAWCBMSQoQ6B35plB80i1O3AWlJSftCEbCbju97Iykg5+NWQOhAKEAWCCy39UyMEgetquvTo7P19bcyfnWBzQMOEG1v+0wub0magT2" ] --- | The client ID of the first of 'someLastPrekeys' -someClientId :: ClientId -someClientId = ClientId 0x1dbfbe22c8a35cb2 - someLastPrekeys :: [LastPrekey] someLastPrekeys = [ lastPrekey "pQABARn//wKhAFggnCcZIK1pbtlJf4wRQ44h4w7/sfSgj5oWXMQaUGYAJ/sDoQChAFgglacihnqg/YQJHkuHNFU7QD6Pb3KN4FnubaCF2EVOgRkE9g==", @@ -1083,9 +1006,9 @@ withSettingsOverrides :: (MonadIO m, HasCallStack) => Opt.Opts -> WaiTest.Sessio withSettingsOverrides opts action = liftIO $ do (brigApp, env) <- Run.mkApp opts sftDiscovery <- - forM (env ^. sftEnv) $ \sftEnv' -> - Async.async $ Calling.startSFTServiceDiscovery (env ^. applog) sftEnv' - turnDiscovery <- Calling.startTurnDiscovery (env ^. applog) (env ^. fsWatcher) (env ^. turnEnv) + forM env.sftEnv $ \sftEnv' -> + Async.async $ Calling.startSFTServiceDiscovery env.appLogger sftEnv' + turnDiscovery <- Calling.startTurnDiscovery env.appLogger env.fsWatcher env.turnEnv res <- WaiTest.runSession action brigApp mapM_ Async.cancel sftDiscovery mapM_ Async.cancel turnDiscovery @@ -1095,7 +1018,7 @@ withSettingsOverrides opts action = liftIO $ do -- compile. withDomainsBlockedForRegistration :: (MonadIO m) => Opt.Opts -> [Text] -> WaiTest.Session a -> m a withDomainsBlockedForRegistration opts domains sess = do - let opts' = opts {Opt.optSettings = (Opt.optSettings opts) {Opt.setCustomerExtensions = Just blocked}} + let opts' = opts {Opt.settings = opts.settings {customerExtensions = Just blocked}} blocked = Opt.CustomerExtensions (Opt.DomainsBlockedForRegistration (unsafeMkDomain <$> domains)) unsafeMkDomain = either error id . mkDomain withSettingsOverrides opts' sess @@ -1117,9 +1040,6 @@ aFewTimes (\_ -> pure . not . good) (const action) -retryT :: (MonadIO m, MonadMask m) => m a -> m a -retryT = recoverAll (exponentialBackoff 8000 <> limitRetries 3) . const - assertOne :: (HasCallStack, MonadIO m, Show a) => [a] -> m a assertOne [a] = pure a assertOne xs = liftIO . assertFailure $ "Expected exactly one element, found " <> show xs diff --git a/services/brig/test/integration/Util/AWS.hs b/services/brig/test/integration/Util/AWS.hs index ace8a3f23d1..458bd82d0c4 100644 --- a/services/brig/test/integration/Util/AWS.hs +++ b/services/brig/test/integration/Util/AWS.hs @@ -89,14 +89,14 @@ assertLocaleUpdateJournaled :: (HasCallStack, MonadUnliftIO m) => UserJournalWat assertLocaleUpdateJournaled userJournalWatcher uid loc label = assertMessage userJournalWatcher label (userUpdateMatcher uid) (userLocaleUpdateJournaled uid loc) -userEmailUpdateJournaled :: (HasCallStack, MonadIO m) => UserId -> Email -> String -> Maybe PU.UserEvent -> m () +userEmailUpdateJournaled :: (HasCallStack, MonadIO m) => UserId -> EmailAddress -> String -> Maybe PU.UserEvent -> m () userEmailUpdateJournaled uid em l (Just ev) = liftIO $ do assertEventType l PU.UserEvent'USER_UPDATE ev assertUserId l uid ev assertEmail l (Just em) ev userEmailUpdateJournaled _ _ l Nothing = liftIO $ assertFailure $ l <> ": Expected 1 UserUpdate, got nothing" -assertEmailUpdateJournaled :: (HasCallStack, MonadUnliftIO m) => UserJournalWatcher -> UserId -> Email -> String -> m () +assertEmailUpdateJournaled :: (HasCallStack, MonadUnliftIO m) => UserJournalWatcher -> UserId -> EmailAddress -> String -> m () assertEmailUpdateJournaled userJournalWatcher uid em label = assertMessage userJournalWatcher label (userUpdateMatcher uid) (userEmailUpdateJournaled uid em) @@ -130,8 +130,8 @@ assertName :: String -> Maybe Name -> PU.UserEvent -> IO () assertName l (Just nm) ev = assertEqual (l <> "name should exist") nm (Name $ fromMaybe "failed to decode name" $ fromByteString $ ev ^. PU.name) assertName l Nothing ev = assertEqual (l <> "name should not exist") Nothing (ev ^. PU.maybe'name) -assertEmail :: String -> Maybe Email -> PU.UserEvent -> IO () -assertEmail l (Just em) ev = assertEqual (l <> "email should exist") em (fromMaybe (error "Failed to convert to email") $ parseEmail $ Text.decodeLatin1 $ fromMaybe "failed to decode email value" $ fromByteString $ ev ^. PU.email) +assertEmail :: String -> Maybe EmailAddress -> PU.UserEvent -> IO () +assertEmail l (Just em) ev = assertEqual (l <> "email should exist") em (fromMaybe (error "Failed to convert to email") $ emailAddressText $ Text.decodeLatin1 $ fromMaybe "failed to decode email value" $ fromByteString $ ev ^. PU.email) assertEmail l Nothing ev = assertEqual (l <> "email should not exist") Nothing (ev ^. PU.maybe'email) assertLocale :: String -> Maybe Locale -> PU.UserEvent -> IO () diff --git a/services/brig/test/unit/Run.hs b/services/brig/test/unit/Run.hs index 64092fef3b5..a371d3130cc 100644 --- a/services/brig/test/unit/Run.hs +++ b/services/brig/test/unit/Run.hs @@ -25,8 +25,6 @@ import Test.Brig.Calling qualified import Test.Brig.Calling.Internal qualified import Test.Brig.InternalNotification qualified import Test.Brig.MLS qualified -import Test.Brig.Roundtrip qualified -import Test.Brig.User.Search.Index.Types qualified import Test.Tasty main :: IO () @@ -34,10 +32,8 @@ main = defaultMain $ testGroup "Tests" - [ Test.Brig.User.Search.Index.Types.tests, - Test.Brig.Calling.tests, + [ Test.Brig.Calling.tests, Test.Brig.Calling.Internal.tests, - Test.Brig.Roundtrip.tests, Test.Brig.MLS.tests, Test.Brig.InternalNotification.tests ] diff --git a/services/brig/test/unit/Test/Brig/Calling.hs b/services/brig/test/unit/Test/Brig/Calling.hs index 3b22294d16c..0dcf489a12a 100644 --- a/services/brig/test/unit/Test/Brig/Calling.hs +++ b/services/brig/test/unit/Test/Brig/Calling.hs @@ -291,12 +291,13 @@ testSFTStaticDeprecatedEndpoint :: IO () testSFTStaticDeprecatedEndpoint = do env <- fst <$> sftStaticEnv turnUri <- generate arbitrary + uid <- generate arbitrary cfg <- runM @IO . ignoreLogs . interpretSFTInMemory mempty . throwErrorInIO @_ @NoTurnServers - $ newConfig env (Discovered turnUri) Nothing Nothing Nothing HideAllSFTServers CallsConfigDeprecated True + $ newConfig uid env (Discovered turnUri) Nothing Nothing Nothing HideAllSFTServers CallsConfigDeprecated True assertEqual "when SFT static URL is disabled, sft_servers should be empty." Set.empty @@ -305,6 +306,7 @@ testSFTStaticDeprecatedEndpoint = do -- The v2 endpoint `GET /calls/config/v2` without an SFT static URL testSFTStaticV2NoStaticUrl :: IO () testSFTStaticV2NoStaticUrl = do + uid <- generate arbitrary env <- fst <$> sftStaticEnv let entry1 = SrvEntry 0 0 (SrvTarget "sft1.foo.example.com." 443) entry2 = SrvEntry 0 0 (SrvTarget "sft2.foo.example.com." 443) @@ -323,7 +325,7 @@ testSFTStaticV2NoStaticUrl = do . ignoreLogs . interpretSFTInMemory mempty . throwErrorInIO @_ @NoTurnServers - $ newConfig env (Discovered turnUri) Nothing (Just sftEnv) (Just . unsafeRange $ 2) ListAllSFTServers (CallsConfigV2 Nothing) True + $ newConfig uid env (Discovered turnUri) Nothing (Just sftEnv) (Just . unsafeRange $ 2) ListAllSFTServers (CallsConfigV2 Nothing) True assertEqual "when SFT static URL is disabled, sft_servers_all should be from SFT environment" (Just . fmap ((^. sftURL) . sftServerFromSrvTarget . srvTarget) . toList $ servers) @@ -334,12 +336,13 @@ testSFTStaticV2StaticUrlError :: IO () testSFTStaticV2StaticUrlError = do (env, staticUrl) <- sftStaticEnv turnUri <- generate arbitrary + uid <- generate arbitrary cfg <- runM @IO . ignoreLogs . interpretSFTInMemory mempty -- an empty lookup map, meaning there was an error . throwErrorInIO @_ @NoTurnServers - $ newConfig env (Discovered turnUri) (Just staticUrl) Nothing (Just . unsafeRange $ 2) ListAllSFTServers (CallsConfigV2 Nothing) True + $ newConfig uid env (Discovered turnUri) (Just staticUrl) Nothing (Just . unsafeRange $ 2) ListAllSFTServers (CallsConfigV2 Nothing) True assertEqual "when SFT static URL is enabled (and setSftListAllServers is enabled), but returns error, sft_servers_all should be omitted" Nothing @@ -353,12 +356,13 @@ testSFTStaticV2StaticUrlList = do -- for sft_servers_all servers <- generate $ replicateM 10 arbitrary turnUri <- generate arbitrary + uid <- generate arbitrary cfg <- runM @IO . ignoreLogs . interpretSFTInMemory (Map.singleton staticUrl (SFTGetResponse $ Right servers)) . throwErrorInIO @_ @NoTurnServers - $ newConfig env (Discovered turnUri) (Just staticUrl) Nothing (Just . unsafeRange $ 3) ListAllSFTServers (CallsConfigV2 Nothing) True + $ newConfig uid env (Discovered turnUri) (Just staticUrl) Nothing (Just . unsafeRange $ 3) ListAllSFTServers (CallsConfigV2 Nothing) True assertEqual "when SFT static URL and setSftListAllServers are enabled, sft_servers_all should be from /sft_servers_all.json" ((^. sftURL) <$$> Just servers) @@ -371,12 +375,13 @@ testSFTStaticV2ListAllServersDisabled = do -- for sft_servers_all servers <- generate $ replicateM 10 arbitrary turnUri <- generate arbitrary + uid <- generate arbitrary cfg <- runM @IO . ignoreLogs . interpretSFTInMemory (Map.singleton staticUrl (SFTGetResponse . Right $ servers)) . throwErrorInIO @_ @NoTurnServers - $ newConfig env (Discovered turnUri) (Just staticUrl) Nothing (Just . unsafeRange $ 3) HideAllSFTServers (CallsConfigV2 Nothing) True + $ newConfig uid env (Discovered turnUri) (Just staticUrl) Nothing (Just . unsafeRange $ 3) HideAllSFTServers (CallsConfigV2 Nothing) True assertEqual "when SFT static URL is enabled and setSftListAllServers is \"disabled\" then sft_servers_all is missing" Nothing diff --git a/services/brig/test/unit/Test/Brig/Calling/Internal.hs b/services/brig/test/unit/Test/Brig/Calling/Internal.hs index 1967f708268..406108db991 100644 --- a/services/brig/test/unit/Test/Brig/Calling/Internal.hs +++ b/services/brig/test/unit/Test/Brig/Calling/Internal.hs @@ -21,9 +21,11 @@ module Test.Brig.Calling.Internal where import Brig.Calling.Internal import Data.Misc (mkHttpsUrl) +import Data.Text qualified as T import Imports import Test.Tasty import Test.Tasty.HUnit +import Test.Tasty.QuickCheck import URI.ByteString.QQ as URI import Wire.API.Call.Config (sftServer) import Wire.Network.DNS.SRV (SrvTarget (SrvTarget)) @@ -44,5 +46,26 @@ tests = "the dot should be stripped from sft server" expectedServer (sftServerFromSrvTarget $ SrvTarget "sft2.env.example.com" 443) - ] + ], + testCase "base26" $ do + "a" @=? base26 0 + "ba" @=? base26 26 + "cfox" @=? base26 38919, + testProperty "base26 . unbase26 === id" $ \(Base26 s) -> base26 (unbase26 s) === s, + testProperty "unbase26 . base26 === id" $ \(NonNegative n) -> unbase26 (base26 n) === n ] + +newtype Base26 = Base26 Text + deriving (Eq, Show) + +mkBase26 :: String -> Base26 +mkBase26 s = Base26 $ case dropWhile (== 'a') s of + "" -> "a" + str -> T.pack str + +instance Arbitrary Base26 where + arbitrary = + mkBase26 <$> listOf1 (fmap chr (chooseInt (ord 'a', ord 'z'))) + +unbase26 :: Text -> Integer +unbase26 = foldl' (\v c -> fromIntegral (ord c - ord 'a') + v * 26) 0 . T.unpack diff --git a/services/brig/test/unit/Test/Brig/Roundtrip.hs b/services/brig/test/unit/Test/Brig/Roundtrip.hs deleted file mode 100644 index d878178bec8..00000000000 --- a/services/brig/test/unit/Test/Brig/Roundtrip.hs +++ /dev/null @@ -1,43 +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 Test.Brig.Roundtrip (tests) where - -import Brig.Options qualified as Options -import Data.Aeson (FromJSON, ToJSON, parseJSON, toJSON) -import Data.Aeson.Types (parseEither) -import Imports -import Test.Tasty qualified as T -import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty, (===)) -import Type.Reflection (typeRep) - -tests :: T.TestTree -tests = - T.localOption (T.Timeout (60 * 1000000) "60s") . T.testGroup "JSON roundtrip tests" $ - [ testRoundTrip @Options.AccountFeatureConfigs - ] - -testRoundTrip :: - forall a. - (Arbitrary a, Typeable a, ToJSON a, FromJSON a, Eq a, Show a) => - T.TestTree -testRoundTrip = testProperty msg trip - where - msg = show (typeRep @a) - trip (v :: a) = - counterexample (show $ toJSON v) $ - Right v === (parseEither parseJSON . toJSON) v diff --git a/services/brig/test/unit/Test/Brig/User/Search/Index/Types.hs b/services/brig/test/unit/Test/Brig/User/Search/Index/Types.hs deleted file mode 100644 index 74c7b92c732..00000000000 --- a/services/brig/test/unit/Test/Brig/User/Search/Index/Types.hs +++ /dev/null @@ -1,84 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Test.Brig.User.Search.Index.Types where - -import Brig.User.Search.Index -import Data.Aeson -import Data.Fixed -import Data.Handle -import Data.Id -import Data.Json.Util -import Data.Time.Clock -import Data.Time.Clock.POSIX -import Data.UUID -import Imports -import Test.Tasty -import Test.Tasty.HUnit -import Wire.API.Team.Role -import Wire.API.User - -tests :: TestTree -tests = - testGroup - "UserDoc, IndexUser: conversion, serialization" - [ testCase "aeson roundtrip: UserDoc" $ - assertEqual - "failed" - (eitherDecode' (encode userDoc1)) - (Right userDoc1), - testCase "backwards comptibility test: UserDoc" $ - assertBool "failed" (isRight (eitherDecode' userDoc1ByteString :: Either String UserDoc)), - testCase "IndexUser to UserDoc" $ - assertEqual - "failed" - (indexToDoc indexUser1) - userDoc1 - ] - -mkTime :: Int -> UTCTime -mkTime = posixSecondsToUTCTime . secondsToNominalDiffTime . MkFixed . (* 1000000000) . fromIntegral - -userDoc1 :: UserDoc -userDoc1 = - UserDoc - { udId = Id . fromJust . fromText $ "0a96b396-57d6-11ea-a04b-7b93d1a5c19c", - udTeam = Just . Id . fromJust . fromText $ "17c59b18-57d6-11ea-9220-8bbf5eee961a", - udName = Just . Name $ "Carl Phoomp", - udNormalized = Just $ "carl phoomp", - udHandle = Just . fromJust . parseHandle $ "phoompy", - udEmail = Just $ Email "phoompy" "example.com", - udColourId = Just . ColourId $ 32, - udAccountStatus = Just Active, - udSAMLIdP = Just "https://issuer.net/214234", - udManagedBy = Just ManagedByScim, - udCreatedAt = Just (toUTCTimeMillis (mkTime 1598737800000)), - udRole = Just RoleAdmin, - udSearchVisibilityInbound = Nothing, - udScimExternalId = Nothing, - udSso = Nothing, - udEmailUnvalidated = Nothing - } - --- Dont touch this. This represents serialized legacy data. -userDoc1ByteString :: LByteString -userDoc1ByteString = "{\"email\":\"phoompy@example.com\",\"account_status\":\"active\",\"handle\":\"phoompy\",\"managed_by\":\"scim\",\"role\":\"admin\",\"accent_id\":32,\"name\":\"Carl Phoomp\",\"created_at\":\"2020-08-29T21:50:00.000Z\",\"team\":\"17c59b18-57d6-11ea-9220-8bbf5eee961a\",\"id\":\"0a96b396-57d6-11ea-a04b-7b93d1a5c19c\",\"normalized\":\"carl phoomp\",\"saml_idp\":\"https://issuer.net/214234\"}" - -indexUser1 :: IndexUser -indexUser1 = docToIndex userDoc1 diff --git a/services/cannon/cannon.cabal b/services/cannon/cannon.cabal index d0af6581163..1eb1b4cdd26 100644 --- a/services/cannon/cannon.cabal +++ b/services/cannon/cannon.cabal @@ -32,6 +32,7 @@ library default-extensions: AllowAmbiguousTypes BangPatterns + BlockArguments ConstraintKinds DataKinds DefaultSignatures @@ -77,44 +78,46 @@ library -Wredundant-constraints -Wunused-packages build-depends: - aeson >=2.0.1.0 - , api-field-json-th >=0.1.0.2 - , async >=2.0 - , base >=4.6 && <5 - , bilge >=0.12 - , bytestring >=0.10 - , bytestring-conversion >=0.2 - , conduit >=1.3.4.2 - , data-timeout >=0.3 - , exceptions >=0.6 + aeson >=2.0.1.0 + , api-field-json-th >=0.1.0.2 + , async >=2.0 + , base >=4.6 && <5 + , bilge >=0.12 + , bytestring >=0.10 + , bytestring-conversion >=0.2 + , conduit >=1.3.4.2 + , data-timeout >=0.3 + , exceptions >=0.6 , extended , extra - , gundeck-types - , hashable >=1.2 - , http-types >=0.8 + , hashable >=1.2 + , hs-opentelemetry-instrumentation-wai + , hs-opentelemetry-sdk + , http-types >=0.8 , imports - , lens >=4.4 - , lens-family-core >=1.1 - , metrics-wai >=0.4 - , mwc-random >=0.13 + , lens >=4.4 + , lens-family-core >=1.1 + , metrics-wai >=0.4 + , mwc-random >=0.13 , prometheus-client - , retry >=0.7 + , retry >=0.7 , safe-exceptions , servant-conduit , servant-server - , strict >=0.3.2 - , text >=1.1 - , tinylog >=0.10 - , types-common >=0.16 + , strict >=0.3.2 + , text >=1.1 + , tinylog >=0.10 + , types-common >=0.16 , unix , unliftio - , vector >=0.10 - , wai >=3.0 - , wai-extra >=3.0 - , wai-utilities >=0.11 - , warp >=3.0 - , websockets >=0.11.2 + , vector >=0.10 + , wai >=3.0 + , wai-extra >=3.0 + , wai-utilities >=0.11 + , warp >=3.0 + , websockets >=0.11.2 , wire-api + , wire-otel default-language: GHC2021 diff --git a/services/cannon/default.nix b/services/cannon/default.nix index 9278d2c1c94..c0e94ff02f7 100644 --- a/services/cannon/default.nix +++ b/services/cannon/default.nix @@ -17,8 +17,9 @@ , extended , extra , gitignoreSource -, gundeck-types , hashable +, hs-opentelemetry-instrumentation-wai +, hs-opentelemetry-sdk , http-types , imports , lens @@ -50,6 +51,7 @@ , warp , websockets , wire-api +, wire-otel }: mkDerivation { pname = "cannon"; @@ -70,8 +72,9 @@ mkDerivation { exceptions extended extra - gundeck-types hashable + hs-opentelemetry-instrumentation-wai + hs-opentelemetry-sdk http-types imports lens @@ -96,6 +99,7 @@ mkDerivation { warp websockets wire-api + wire-otel ]; executableHaskellDepends = [ base imports types-common ]; testHaskellDepends = [ diff --git a/services/cannon/src/Cannon/App.hs b/services/cannon/src/Cannon/App.hs index 842d38135a3..770bf0ff499 100644 --- a/services/cannon/src/Cannon/App.hs +++ b/services/cannon/src/Cannon/App.hs @@ -29,7 +29,7 @@ import Control.Monad.Catch import Data.Aeson hiding (Error, Key, (.=)) import Data.ByteString.Conversion import Data.ByteString.Lazy (toStrict) -import Data.Id (ClientId) +import Data.Id import Data.Text.Lazy qualified as Text import Data.Timeout import Imports hiding (threadDelay) @@ -155,7 +155,7 @@ rejectOnError :: PendingConnection -> HandshakeException -> IO a rejectOnError p x = do let f lb mg = toStrict . encode $ mkError status400 lb mg case x of - NotSupported -> rejectRequest p (f "protocol not supported" "N/A") + NotSupported -> rejectRequest p (f "protocol not supported" defRequestId) MalformedRequest _ m -> rejectRequest p (f "malformed-request" (Text.pack m)) OtherHandshakeException m -> rejectRequest p (f "other-error" (Text.pack m)) _ -> pure () diff --git a/services/cannon/src/Cannon/Run.hs b/services/cannon/src/Cannon/Run.hs index 05984cedcb1..eefd22f4af5 100644 --- a/services/cannon/src/Cannon/Run.hs +++ b/services/cannon/src/Cannon/Run.hs @@ -27,7 +27,7 @@ import Cannon.API.Public import Cannon.App (maxPingInterval) import Cannon.Dict qualified as D import Cannon.Options -import Cannon.Types (Cannon, applog, clients, env, mkEnv, runCannon', runCannonToServant) +import Cannon.Types (Cannon, applog, clients, env, mkEnv, runCannon, runCannonToServant) import Cannon.WS hiding (env) import Control.Concurrent import Control.Concurrent.Async qualified as Async @@ -45,6 +45,9 @@ import Network.Wai qualified as Wai import Network.Wai.Handler.Warp hiding (run) import Network.Wai.Middleware.Gzip qualified as Gzip import Network.Wai.Utilities.Server +import OpenTelemetry.Instrumentation.Wai +import OpenTelemetry.Trace hiding (Server) +import OpenTelemetry.Trace qualified as Otel import Prometheus qualified as Prom import Servant import System.IO.Strict qualified as Strict @@ -57,11 +60,12 @@ import Wire.API.Routes.Internal.Cannon qualified as Internal import Wire.API.Routes.Public.Cannon import Wire.API.Routes.Version import Wire.API.Routes.Version.Wai +import Wire.OpenTelemetry (withTracer) type CombinedAPI = CannonAPI :<|> Internal.API run :: Opts -> IO () -run o = do +run o = withTracer \tracer -> do when (o ^. drainOpts . millisecondsBetweenBatches == 0) $ error "drainOpts.millisecondsBetweenBatches must not be set to 0." when (o ^. drainOpts . gracePeriodSeconds == 0) $ @@ -74,14 +78,16 @@ run o = do <*> newManager defaultManagerSettings {managerConnCount = 128} <*> createSystemRandom <*> mkClock - refreshMetricsThread <- Async.async $ runCannon' e refreshMetrics + refreshMetricsThread <- Async.async $ runCannon e refreshMetrics s <- newSettings $ Server (o ^. cannon . host) (o ^. cannon . port) (applog e) (Just idleTimeout) + otelMiddleWare <- newOpenTelemetryWaiMiddleware let middleware :: Wai.Middleware middleware = versionMiddleware (foldMap expandVersionExp (o ^. disabledAPIVersions)) . requestIdMiddleware g defaultRequestIdHeaderName . servantPrometheusMiddleware (Proxy @CombinedAPI) + . otelMiddleWare . Gzip.gzip Gzip.def . catchErrors g defaultRequestIdHeaderName app :: Application @@ -94,7 +100,7 @@ run o = do E.handle uncaughtExceptionHandler $ do void $ installHandler sigTERM (signalHandler (env e) tid) Nothing void $ installHandler sigINT (signalHandler (env e) tid) Nothing - runSettings s app `finally` do + inSpan tracer "cannon" defaultSpanArguments {kind = Otel.Server} (runSettings s app) `finally` do -- FUTUREWORK(@akshaymankar, @fisx): we may want to call `runSettingsWithShutdown` here, -- but it's a sensitive change, and it looks like this is closing all the websockets at -- the same time and then calling the drain script. I suspect this might be due to some diff --git a/services/cannon/src/Cannon/Types.hs b/services/cannon/src/Cannon/Types.hs index e085a0d9f20..6fa37b78a65 100644 --- a/services/cannon/src/Cannon/Types.hs +++ b/services/cannon/src/Cannon/Types.hs @@ -23,20 +23,17 @@ module Cannon.Types applog, dict, env, - logger, Cannon, mapConcurrentlyCannon, mkEnv, runCannon, - runCannon', - options, clients, wsenv, runCannonToServant, ) where -import Bilge (Manager, RequestId (..)) +import Bilge (Manager) import Bilge.RPC (HasRequestId (..)) import Cannon.Dict (Dict) import Cannon.Options @@ -45,11 +42,9 @@ import Cannon.WS qualified as WS import Control.Concurrent.Async (mapConcurrently) import Control.Lens ((^.)) import Control.Monad.Catch +import Data.Id import Data.Text.Encoding import Imports -import Network.Wai -import Network.Wai.Utilities.Request qualified as Wai -import Network.Wai.Utilities.Server import Prometheus import Servant qualified import System.Logger qualified as Logger @@ -106,20 +101,11 @@ mkEnv :: Clock -> Env mkEnv external o l d p g t = - Env o l d (RequestId "N/A") $ + Env o l d (RequestId defRequestId) $ WS.env external (o ^. cannon . port) (encodeUtf8 $ o ^. gundeck . host) (o ^. gundeck . port) l p d g t (o ^. drainOpts) -runCannon :: Env -> Cannon a -> Request -> IO a -runCannon e c r = do - let rid = Wai.getRequestId defaultRequestIdHeaderName r - e' = e {reqId = rid} - runCannon' e' c - -runCannon' :: Env -> Cannon a -> IO a -runCannon' e c = runReaderT (unCannon c) e - -options :: Cannon Opts -options = Cannon $ asks opts +runCannon :: Env -> Cannon a -> IO a +runCannon e c = runReaderT (unCannon c) e clients :: Cannon (Dict Key Websocket) clients = Cannon $ asks dict @@ -130,10 +116,7 @@ wsenv = Cannon $ do r <- asks reqId pure $ WS.setRequestId r e -logger :: Cannon Logger -logger = Cannon $ asks applog - -- | Natural transformation from 'Cannon' to 'Handler' monad. -- Used to call 'Cannon' from servant. runCannonToServant :: Cannon.Types.Env -> Cannon x -> Servant.Handler x -runCannonToServant env c = liftIO $ runCannon' env c +runCannonToServant env c = liftIO $ runCannon env c diff --git a/services/cannon/src/Cannon/WS.hs b/services/cannon/src/Cannon/WS.hs index 2b9a816df20..ea106f4cf03 100644 --- a/services/cannon/src/Cannon/WS.hs +++ b/services/cannon/src/Cannon/WS.hs @@ -62,11 +62,10 @@ import Data.ByteString.Char8 (pack) import Data.ByteString.Conversion import Data.ByteString.Lazy qualified as L import Data.Hashable -import Data.Id (ClientId, ConnId (..), UserId) +import Data.Id (ClientId, ConnId (..), UserId, defRequestId) import Data.List.Extra (chunksOf) import Data.Text.Encoding (decodeUtf8) import Data.Timeout (TimeoutUnit (..), (#)) -import Gundeck.Types import Imports hiding (threadDelay) import Network.HTTP.Types.Method import Network.HTTP.Types.Status @@ -76,6 +75,7 @@ import System.Logger qualified as Logger import System.Logger.Class hiding (Error, Settings, close, (.=)) import System.Random.MWC (GenIO, uniform) import UnliftIO.Async (async, cancel, pooledMapConcurrentlyN_) +import Wire.API.Presence ----------------------------------------------------------------------------- -- Key @@ -192,7 +192,7 @@ env :: Clock -> DrainOpts -> Env -env leh lp gh gp = Env leh lp (host gh . port gp $ empty) (RequestId "N/A") +env leh lp gh gp = Env leh lp (host gh . port gp $ empty) (RequestId defRequestId) runWS :: (MonadIO m) => Env -> WS a -> m a runWS e m = liftIO $ runReaderT (_conn m) e @@ -329,7 +329,7 @@ regInfo k c = do let h = externalHostname e p = portnum e r = "http://" <> h <> ":" <> pack (show p) <> "/i/push/" - pure . lbytes . encode . object $ + pure . Bilge.json . object $ [ "user_id" .= decodeUtf8 (keyUserBytes k), "device_id" .= decodeUtf8 (keyConnBytes k), "resource" .= decodeUtf8 (r <> keyUserBytes k <> "/" <> keyConnBytes k), diff --git a/services/cargohold/cargohold.cabal b/services/cargohold/cargohold.cabal index 2a8a5b2ba93..39b953edecd 100644 --- a/services/cargohold/cargohold.cabal +++ b/services/cargohold/cargohold.cabal @@ -81,8 +81,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -fplugin=TransitiveAnns.Plugin -Wredundant-constraints - -Wunused-packages + -Wredundant-constraints -Wunused-packages build-depends: aeson >=2.0.1.0 @@ -125,7 +124,6 @@ library , time >=1.4 , tinylog >=0.10 , transformers - , transitive-anns , types-common >=0.16 , types-common-aws , unliftio @@ -280,11 +278,9 @@ executable cargohold-integration , imports , kan-extensions , lens >=3.8 - , mime >=0.4 , mmorph , mtl , optparse-applicative - , safe , servant-client , tagged >=0.8 , tasty >=1.0 diff --git a/services/cargohold/default.nix b/services/cargohold/default.nix index 32c9e73b371..69316cc540f 100644 --- a/services/cargohold/default.nix +++ b/services/cargohold/default.nix @@ -46,7 +46,6 @@ , prometheus-client , resourcet , retry -, safe , servant , servant-client , servant-server @@ -58,7 +57,6 @@ , time , tinylog , transformers -, transitive-anns , types-common , types-common-aws , unliftio @@ -119,7 +117,6 @@ mkDerivation { time tinylog transformers - transitive-anns types-common types-common-aws unliftio @@ -152,11 +149,9 @@ mkDerivation { imports kan-extensions lens - mime mmorph mtl optparse-applicative - safe servant-client tagged tasty diff --git a/services/cargohold/src/CargoHold/API/Error.hs b/services/cargohold/src/CargoHold/API/Error.hs index 4fed14f95bd..ef38babfe79 100644 --- a/services/cargohold/src/CargoHold/API/Error.hs +++ b/services/cargohold/src/CargoHold/API/Error.hs @@ -41,34 +41,6 @@ unverifiedUser = errorToWai @'UnverifiedUser userNotFound :: Error userNotFound = errorToWai @'UserNotFound -invalidMD5 :: Error -invalidMD5 = mkError status400 "client-error" "Invalid MD5." - -requestTimeout :: Error -requestTimeout = - mkError - status408 - "request-timeout" - "The request timed out. The server was still expecting more data \ - \but none was sent over an extended period of time. Idle connections \ - \will be closed." - -uploadTooSmall :: Error -uploadTooSmall = - mkError - status403 - "client-error" - "The current chunk size is \ - \smaller than the minimum allowed." - -uploadTooLarge :: Error -uploadTooLarge = - mkError - status413 - "client-error" - "The current chunk size + offset \ - \is larger than the full upload size." - noMatchingAssetEndpoint :: Error noMatchingAssetEndpoint = errorToWai @'NoMatchingAssetEndpoint diff --git a/services/cargohold/src/CargoHold/API/Public.hs b/services/cargohold/src/CargoHold/API/Public.hs index 607e8947087..e0d63d3167d 100644 --- a/services/cargohold/src/CargoHold/API/Public.hs +++ b/services/cargohold/src/CargoHold/API/Public.hs @@ -41,7 +41,6 @@ import Servant.API import Servant.Server hiding (Handler) import URI.ByteString as URI import Wire.API.Asset -import Wire.API.Federation.API import Wire.API.Routes.AssetBody import Wire.API.Routes.Internal.Brig (brigInternalClient) import Wire.API.Routes.Internal.Cargohold @@ -61,26 +60,41 @@ servantSitemap = :<|> mainAPI where userAPI :: forall tag. (tag ~ 'UserPrincipalTag) => ServerT (BaseAPIv3 tag) Handler - userAPI = uploadAssetV3 @tag :<|> downloadAssetV3 @tag :<|> deleteAssetV3 @tag + userAPI = + Named @'("assets-upload-v3", tag) uploadAssetV3 + :<|> Named @'("assets-download-v3", tag) downloadAssetV3 + :<|> Named @'("assets-delete-v3", tag) deleteAssetV3 botAPI :: forall tag. (tag ~ 'BotPrincipalTag) => ServerT (BaseAPIv3 tag) Handler - botAPI = uploadAssetV3 @tag :<|> downloadAssetV3 @tag :<|> deleteAssetV3 @tag + botAPI = + Named @'("assets-upload-v3", tag) uploadAssetV3 + :<|> Named @'("assets-download-v3", tag) downloadAssetV3 + :<|> Named @'("assets-delete-v3", tag) deleteAssetV3 providerAPI :: forall tag. (tag ~ 'ProviderPrincipalTag) => ServerT (BaseAPIv3 tag) Handler - providerAPI = uploadAssetV3 @tag :<|> downloadAssetV3 @tag :<|> deleteAssetV3 @tag - legacyAPI = legacyDownloadPlain :<|> legacyDownloadPlain :<|> legacyDownloadOtr + providerAPI = + Named @'("assets-upload-v3", tag) uploadAssetV3 + :<|> Named @'("assets-download-v3", tag) downloadAssetV3 + :<|> Named @'("assets-delete-v3", tag) deleteAssetV3 + legacyAPI = + Named @"assets-download-legacy" legacyDownloadPlain + :<|> Named @"assets-conv-download-legacy" legacyDownloadPlain + :<|> Named @"assets-conv-otr-download-legacy" legacyDownloadOtr qualifiedAPI :: ServerT QualifiedAPI Handler - qualifiedAPI = callsFed (exposeAnnotations downloadAssetV4) :<|> deleteAssetV4 + qualifiedAPI = + Named @"assets-download-v4" + downloadAssetV4 + :<|> Named @"assets-delete-v4" deleteAssetV4 mainAPI :: ServerT MainAPI Handler mainAPI = - renewTokenV3 - :<|> deleteTokenV3 - :<|> uploadAssetV3 @'UserPrincipalTag - :<|> callsFed (exposeAnnotations downloadAssetV4) - :<|> deleteAssetV4 + Named @"tokens-renew" renewTokenV3 + :<|> Named @"tokens-delete" deleteTokenV3 + :<|> Named @"assets-upload" (uploadAssetV3 @'UserPrincipalTag) + :<|> Named @"assets-download" downloadAssetV4 + :<|> Named @"assets-delete" deleteAssetV4 internalSitemap :: ServerT InternalAPI Handler internalSitemap = - pure () - :<|> Named @"iGetAsset" iDownloadAssetV3 + Named @"i_status" (pure ()) + :<|> Named @"i_get_asset" iDownloadAssetV3 -- | Like 'downloadAssetV3' below, but it works without user session token, and has a -- different route type. diff --git a/services/cargohold/src/CargoHold/API/Util.hs b/services/cargohold/src/CargoHold/API/Util.hs index a6df369b1f4..b6d5876da49 100644 --- a/services/cargohold/src/CargoHold/API/Util.hs +++ b/services/cargohold/src/CargoHold/API/Util.hs @@ -23,17 +23,16 @@ where import CargoHold.App import Control.Error -import Control.Lens import Data.Qualified import Imports import Wire.API.Federation.Error ensureLocal :: Qualified a -> Handler (Local a) ensureLocal value = do - loc <- view localUnit + loc <- asks (.localUnit) foldQualified loc pure (\_ -> throwE federationNotImplemented) value qualifyLocal :: a -> Handler (Local a) qualifyLocal x = do - loc <- view localUnit + loc <- asks (.localUnit) pure (qualifyAs loc x) diff --git a/services/cargohold/src/CargoHold/API/V3.hs b/services/cargohold/src/CargoHold/API/V3.hs index fcb9105c7d5..2fb5492e43a 100644 --- a/services/cargohold/src/CargoHold/API/V3.hs +++ b/services/cargohold/src/CargoHold/API/V3.hs @@ -41,7 +41,7 @@ import qualified Codec.MIME.Type as MIME import qualified Conduit import Control.Applicative (optional) import Control.Error -import Control.Lens (set, view, (^.)) +import Control.Lens (set, (^.)) import Control.Monad.Trans.Resource import Crypto.Random (getRandomBytes) import Data.Aeson (eitherDecodeStrict') @@ -70,7 +70,7 @@ upload own bdy = do let cl = fromIntegral $ hdrLength hdrs when (cl <= 0) $ throwE invalidLength - maxBytes <- view (CargoHold.App.settings . maxTotalBytes) + maxBytes <- asks (.options.settings.maxTotalBytes) when (cl > maxBytes) $ throwE assetTooLarge ast <- liftIO $ Id <$> nextRandom diff --git a/services/cargohold/src/CargoHold/AWS.hs b/services/cargohold/src/CargoHold/AWS.hs index 587937d7aa2..e94392d5755 100644 --- a/services/cargohold/src/CargoHold/AWS.hs +++ b/services/cargohold/src/CargoHold/AWS.hs @@ -1,5 +1,4 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE TemplateHaskell #-} -- This file is part of the Wire Server implementation. -- @@ -20,19 +19,14 @@ module CargoHold.AWS ( -- * Monad - Env, + Env (..), mkEnv, amazonkaEnvWithDownloadEndpoint, Amazon, - amazonkaEnv, execute, - s3Bucket, - cloudFront, Error (..), - amazonkaDownloadEndpoint, -- * AWS - send, sendCatch, exec, execStream, @@ -58,23 +52,21 @@ import qualified System.Logger.Class as Log import Util.Options (AWSEndpoint (..)) data Env = Env - { _logger :: !Logger, - _s3Bucket :: !Text, - _amazonkaEnv :: !AWS.Env, + { logger :: !Logger, + s3Bucket :: !Text, + amazonkaEnv :: !AWS.Env, -- | Endpoint for downloading assets (for the external world). -- This gets used with Minio, which Cargohold can reach using a cluster-internal endpoint, -- but clients can't, so we need to use a public one for pre-signed URLs we redirect to. - _amazonkaDownloadEndpoint :: !AWSEndpoint, - _cloudFront :: !(Maybe CloudFront) + amazonkaDownloadEndpoint :: !AWSEndpoint, + cloudFront :: !(Maybe CloudFront) } -makeLenses ''Env - -- | Override the endpoint in the '_amazonkaEnv' with '_amazonkaDownloadEndpoint'. -- TODO: Choose the correct s3 addressing style amazonkaEnvWithDownloadEndpoint :: Env -> AWS.Env amazonkaEnvWithDownloadEndpoint e = - AWS.overrideService (setAWSEndpoint (e ^. amazonkaDownloadEndpoint)) (e ^. amazonkaEnv) + AWS.overrideService (setAWSEndpoint e.amazonkaDownloadEndpoint) e.amazonkaEnv setAWSEndpoint :: AWSEndpoint -> AWS.Service -> AWS.Service setAWSEndpoint e = AWS.setEndpoint (_awsSecure e) (_awsHost e) (_awsPort e) @@ -96,7 +88,7 @@ newtype Amazon a = Amazon ) instance MonadLogger Amazon where - log l m = view logger >>= \g -> Logger.log g l m + log l m = asks (.logger) >>= \g -> Logger.log g l m mkEnv :: Logger -> @@ -116,7 +108,7 @@ mkEnv lgr s3End s3AddrStyle s3Download bucket cfOpts mgr = do cf <- mkCfEnv cfOpts pure (Env g bucket e s3Download cf) where - mkCfEnv (Just o) = Just <$> initCloudFront (o ^. privateKey) (o ^. keyPairId) 300 (o ^. domain) + mkCfEnv (Just o) = Just <$> initCloudFront o.privateKey o.keyPairId 300 o.domain mkCfEnv Nothing = pure Nothing mkAwsEnv g s3 = do baseEnv <- @@ -164,16 +156,6 @@ sendCatch :: m (Either AWS.Error (AWSResponse r)) sendCatch env = AWS.trying AWS._Error . AWS.send env -send :: - (AWSRequest r, Typeable r, Typeable (AWSResponse r)) => - AWS.Env -> - r -> - Amazon (AWSResponse r) -send env r = throwA =<< sendCatch env r - -throwA :: Either AWS.Error a -> Amazon a -throwA = either (throwM . GeneralError) pure - exec :: ( AWSRequest r, Typeable r, @@ -187,11 +169,11 @@ exec :: (Text -> r) -> m (AWSResponse r) exec env request = do - let req = request env._s3Bucket - resp <- execute env (sendCatch (env ^. amazonkaEnv) req) + let req = request env.s3Bucket + resp <- execute env (sendCatch env.amazonkaEnv req) case resp of Left err -> do - Logger.info (view logger env) $ + Logger.info env.logger $ Log.field "remote" (Log.val "S3") ~~ Log.msg (show err) ~~ Log.msg (show req) @@ -210,11 +192,11 @@ execStream :: (Text -> r) -> ResourceT IO (AWSResponse r) execStream env request = do - let req = request env._s3Bucket - resp <- sendCatch (env ^. amazonkaEnv) req + let req = request env.s3Bucket + resp <- sendCatch env.amazonkaEnv req case resp of Left err -> do - Logger.info (view logger env) $ + Logger.info env.logger $ Log.field "remote" (Log.val "S3") ~~ Log.msg (show err) ~~ Log.msg (show req) @@ -235,8 +217,8 @@ execCatch :: (Text -> r) -> m (Maybe (AWSResponse r)) execCatch env request = do - let req = request env._s3Bucket - resp <- execute env (retrying retry5x (const canRetry) (const (sendCatch (env ^. amazonkaEnv) req))) + let req = request env.s3Bucket + resp <- execute env (retrying retry5x (const canRetry) (const (sendCatch env.amazonkaEnv req))) case resp of Left err -> do Log.info $ diff --git a/services/cargohold/src/CargoHold/App.hs b/services/cargohold/src/CargoHold/App.hs index 85c31799667..5acb66a57ed 100644 --- a/services/cargohold/src/CargoHold/App.hs +++ b/services/cargohold/src/CargoHold/App.hs @@ -22,24 +22,22 @@ module CargoHold.App ( -- * Environment - Env, + Env (..), newEnv, closeEnv, - aws, - multiIngress, - httpManager, - http2Manager, - appLogger, - requestId, - localUnit, - options, - settings, + awsLens, + multiIngressLens, + httpManagerLens, + http2ManagerLens, + appLoggerLens, + requestIdLens, + localUnitLens, + optionsLens, -- * App Monad AppT, App, runAppT, - runAppResourceT, executeBrigInteral, -- * Handler Monad @@ -49,7 +47,7 @@ module CargoHold.App where import Amazonka (S3AddressingStyle (S3AddressingStylePath)) -import Bilge (Manager, MonadHttp, RequestId (..), newManager, withResponse) +import Bilge (Manager, MonadHttp, newManager, withResponse) import qualified Bilge import Bilge.RPC (HasRequestId (..)) import qualified CargoHold.AWS as AWS @@ -57,9 +55,9 @@ import CargoHold.Options (AWSOpts, Opts, S3Compatibility (..), brig) import qualified CargoHold.Options as Opt import Control.Error (ExceptT, exceptT) import Control.Exception (throw) -import Control.Lens (Lens', makeLenses, non, view, (?~), (^.)) +import Control.Lens (lensField, lensRules, makeLensesWith, non, (.~), (?~), (^.)) import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow) -import Control.Monad.Trans.Resource (ResourceT, runResourceT, transResourceT) +import Data.Id import qualified Data.Map as Map import Data.Qualified import HTTP2.Client.Manager (Http2Manager, http2ManagerWithSSLCtx) @@ -74,6 +72,7 @@ import qualified Servant.Client as Servant import System.Logger.Class hiding (settings) import qualified System.Logger.Extended as Log import Util.Options +import Util.SuffixNamer import Wire.API.Routes.Internal.Brig (BrigInternalClient) import qualified Wire.API.Routes.Internal.Brig as IBrig @@ -81,31 +80,28 @@ import qualified Wire.API.Routes.Internal.Brig as IBrig -- Environment data Env = Env - { _aws :: AWS.Env, - _appLogger :: Logger, - _httpManager :: Manager, - _http2Manager :: Http2Manager, - _requestId :: RequestId, - _options :: Opt.Opts, - _localUnit :: Local (), - _multiIngress :: Map String AWS.Env + { aws :: AWS.Env, + appLogger :: Logger, + httpManager :: Manager, + http2Manager :: Http2Manager, + requestId :: RequestId, + options :: Opt.Opts, + localUnit :: Local (), + multiIngress :: Map String AWS.Env } -makeLenses ''Env - -settings :: Lens' Env Opt.Settings -settings = options . Opt.settings +makeLensesWith (lensRules & lensField .~ suffixNamer) ''Env newEnv :: Opts -> IO Env newEnv opts = do - logger <- Log.mkLogger (opts ^. Opt.logLevel) (opts ^. Opt.logNetStrings) (opts ^. Opt.logFormat) + logger <- Log.mkLogger opts.logLevel opts.logNetStrings opts.logFormat checkOpts opts logger - httpMgr <- initHttpManager (opts ^. Opt.aws . Opt.s3Compatibility) + httpMgr <- initHttpManager opts.aws.s3Compatibility http2Mgr <- initHttp2Manager - awsEnv <- initAws (opts ^. Opt.aws) logger httpMgr + awsEnv <- initAws opts.aws logger httpMgr multiIngressAWS <- initMultiIngressAWS logger httpMgr - let localDomain = toLocalUnsafe (opts ^. Opt.settings . Opt.federationDomain) () - pure $ Env awsEnv logger httpMgr http2Mgr (RequestId "N/A") opts localDomain multiIngressAWS + let localDomain = toLocalUnsafe opts.settings.federationDomain () + pure $ Env awsEnv logger httpMgr http2Mgr (RequestId defRequestId) opts localDomain multiIngressAWS where initMultiIngressAWS :: Logger -> Manager -> IO (Map String AWS.Env) initMultiIngressAWS logger httpMgr = @@ -114,10 +110,10 @@ newEnv opts = do ( \(k, v) -> initAws (patchS3DownloadEndpoint v) logger httpMgr >>= \v' -> pure (k, v') ) - (Map.assocs (opts ^. Opt.aws . Opt.multiIngress . non Map.empty)) + (Map.assocs (opts ^. Opt.awsLens . Opt.multiIngressLens . non Map.empty)) patchS3DownloadEndpoint :: AWSEndpoint -> AWSOpts - patchS3DownloadEndpoint e = (opts ^. Opt.aws) & Opt.s3DownloadEndpoint ?~ e + patchS3DownloadEndpoint e = (opts ^. Opt.awsLens) & Opt.s3DownloadEndpointLens ?~ e -- | Validate (some) options (`Opts`) -- @@ -134,19 +130,19 @@ checkOpts opts lgr = do error errorMsg where multiIngressConfigured :: Bool - multiIngressConfigured = (not . null) (opts ^. (Opt.aws . Opt.multiIngress . non Map.empty)) + multiIngressConfigured = (not . null) (opts ^. (Opt.awsLens . Opt.multiIngressLens . non Map.empty)) cloudFrontConfigured :: Bool - cloudFrontConfigured = isJust (opts ^. (Opt.aws . Opt.cloudFront)) + cloudFrontConfigured = isJust opts.aws.cloudFront singleAwsDownloadEndpointConfigured :: Bool - singleAwsDownloadEndpointConfigured = isJust (opts ^. (Opt.aws . Opt.s3DownloadEndpoint)) + singleAwsDownloadEndpointConfigured = isJust opts.aws.s3DownloadEndpoint initAws :: AWSOpts -> Logger -> Manager -> IO AWS.Env -initAws o l = AWS.mkEnv l (o ^. Opt.s3Endpoint) addrStyle downloadEndpoint (o ^. Opt.s3Bucket) (o ^. Opt.cloudFront) +initAws o l = AWS.mkEnv l o.s3Endpoint addrStyle downloadEndpoint o.s3Bucket o.cloudFront where - downloadEndpoint = fromMaybe (o ^. Opt.s3Endpoint) (o ^. Opt.s3DownloadEndpoint) - addrStyle = maybe S3AddressingStylePath Opt.unwrapS3AddressingStyle (o ^. Opt.s3AddressingStyle) + downloadEndpoint = fromMaybe o.s3Endpoint o.s3DownloadEndpoint + addrStyle = maybe S3AddressingStylePath Opt.unwrapS3AddressingStyle o.s3AddressingStyle initHttpManager :: Maybe S3Compatibility -> IO Manager initHttpManager s3Compat = @@ -187,7 +183,7 @@ initSSLContext = do pure ctx closeEnv :: Env -> IO () -closeEnv e = Log.close $ e ^. appLogger +closeEnv e = Log.close e.appLogger ------------------------------------------------------------------------------- -- App Monad @@ -209,8 +205,8 @@ type App = AppT IO instance MonadLogger App where log l m = do - g <- view appLogger - r <- view requestId + g <- asks (.appLogger) + r <- asks (.requestId) Log.log g l $ "request" .= unRequestId r ~~ m instance MonadLogger (ExceptT e App) where @@ -218,11 +214,11 @@ instance MonadLogger (ExceptT e App) where instance MonadHttp App where handleRequestWithCont req handler = do - manager <- view httpManager + manager <- asks (.httpManager) liftIO $ withResponse req manager handler instance HasRequestId App where - getRequestId = view requestId + getRequestId = asks (.requestId) instance MonadHttp (ExceptT e App) where handleRequestWithCont req handler = lift $ Bilge.handleRequestWithCont req handler @@ -233,13 +229,10 @@ instance HasRequestId (ExceptT e App) where runAppT :: Env -> AppT m a -> m a runAppT e (AppT a) = runReaderT a e -runAppResourceT :: (MonadIO m) => Env -> ResourceT App a -> m a -runAppResourceT e rma = liftIO . runResourceT $ transResourceT (runAppT e) rma - executeBrigInteral :: BrigInternalClient a -> App (Either Servant.ClientError a) executeBrigInteral action = do - httpMgr <- view httpManager - brigEndpoint <- view (options . brig) + httpMgr <- asks (.httpManager) + brigEndpoint <- asks (.options.brig) liftIO $ IBrig.runBrigInternalClient httpMgr brigEndpoint action ------------------------------------------------------------------------------- diff --git a/services/cargohold/src/CargoHold/Federation.hs b/services/cargohold/src/CargoHold/Federation.hs index 7ce7d4aae6b..5382e139011 100644 --- a/services/cargohold/src/CargoHold/Federation.hs +++ b/services/cargohold/src/CargoHold/Federation.hs @@ -21,7 +21,6 @@ import CargoHold.App import CargoHold.Options import Control.Error import Control.Exception (throw) -import Control.Lens import Control.Monad.Codensity import Data.Id import Data.Qualified @@ -75,12 +74,12 @@ downloadRemoteAsset usr rkey tok = do mkFederatorClientEnv :: Remote x -> Handler FederatorClientEnv mkFederatorClientEnv remote = do - loc <- view localUnit + loc <- asks (.localUnit) endpoint <- - view (options . federator) + asks (.options.federator) >>= maybe (throwE federationNotConfigured) pure - mgr <- view http2Manager - rid <- view requestId + mgr <- asks (.http2Manager) + rid <- asks (.requestId) pure FederatorClientEnv { ceOriginDomain = tDomain loc, diff --git a/services/cargohold/src/CargoHold/Options.hs b/services/cargohold/src/CargoHold/Options.hs index 7a3f5cd08fa..4b5ccb1d7bc 100644 --- a/services/cargohold/src/CargoHold/Options.hs +++ b/services/cargohold/src/CargoHold/Options.hs @@ -29,23 +29,21 @@ import Data.Domain import Imports import System.Logger.Extended (Level, LogFormat) import Util.Options -import Util.Options.Common +import Util.SuffixNamer import Wire.API.Routes.Version -- | AWS CloudFront settings. data CloudFrontOpts = CloudFrontOpts { -- | Domain - _domain :: CF.Domain, + domain :: CF.Domain, -- | Keypair ID - _keyPairId :: CF.KeyPairId, + keyPairId :: CF.KeyPairId, -- | Path to private key - _privateKey :: FilePath + privateKey :: FilePath } deriving (Show, Generic) -deriveFromJSON toOptionFieldName ''CloudFrontOpts - -makeLenses ''CloudFrontOpts +deriveFromJSON defaultOptions ''CloudFrontOpts newtype OptS3AddressingStyle = OptS3AddressingStyle { unwrapS3AddressingStyle :: S3AddressingStyle @@ -62,7 +60,7 @@ instance FromJSON OptS3AddressingStyle where other -> fail $ "invalid S3AddressingStyle: " <> show other data AWSOpts = AWSOpts - { _s3Endpoint :: !AWSEndpoint, + { s3Endpoint :: !AWSEndpoint, -- | S3 can either by addressed in path style, i.e. -- https:////, or vhost style, i.e. -- https://./. AWS's S3 offering has @@ -88,18 +86,18 @@ data AWSOpts = AWSOpts -- -- When this option is unspecified, we default to path style addressing to -- ensure smooth transition for older deployments. - _s3AddressingStyle :: !(Maybe OptS3AddressingStyle), + s3AddressingStyle :: !(Maybe OptS3AddressingStyle), -- | S3 endpoint for generating download links. Useful if Cargohold is configured to use -- an S3 replacement running inside the internal network (in which case internally we -- would use one hostname for S3, and when generating an asset link for a client app, we -- would use another hostname). - _s3DownloadEndpoint :: !(Maybe AWSEndpoint), + s3DownloadEndpoint :: !(Maybe AWSEndpoint), -- | S3 bucket name - _s3Bucket :: !Text, + s3Bucket :: !Text, -- | Enable this option for compatibility with specific S3 backends. - _s3Compatibility :: !(Maybe S3Compatibility), + s3Compatibility :: !(Maybe S3Compatibility), -- | AWS CloudFront options - _cloudFront :: !(Maybe CloudFrontOpts), + cloudFront :: !(Maybe CloudFrontOpts), -- | @Z-Host@ header to s3 download endpoint `Map` -- -- This logic is: If the @Z-Host@ header is provided and found in this map, @@ -107,7 +105,7 @@ data AWSOpts = AWSOpts -- otherwise a 404 is retuned. This option is only useful -- in the context of multi-ingress setups where one backend / deployment is -- reachable under several domains. - _multiIngress :: !(Maybe (Map String AWSEndpoint)) + multiIngress :: !(Maybe (Map String AWSEndpoint)) } deriving (Show, Generic) @@ -122,15 +120,20 @@ instance FromJSON S3Compatibility where "scality-ring" -> pure S3CompatibilityScalityRing other -> fail $ "invalid S3Compatibility: " <> show other -deriveFromJSON toOptionFieldName ''AWSOpts +deriveFromJSON defaultOptions ''AWSOpts -makeLenses ''AWSOpts +makeLensesFor + [ ("multiIngress", "multiIngressLens"), + ("s3DownloadEndpoint", "s3DownloadEndpointLens"), + ("cloudFront", "cloudFrontLens") + ] + ''AWSOpts data Settings = Settings { -- | Maximum allowed size for uploads, in bytes - _maxTotalBytes :: !Int, + maxTotalBytes :: !Int, -- | TTL for download links, in seconds - _downloadLinkTTL :: !Word, + downloadLinkTTL :: !Word, -- | FederationDomain is required, even when not wanting to federate with other backends -- (in that case the 'allowedDomains' can be set to empty in Federator) -- Federation domain is used to qualify local IDs and handles, @@ -141,37 +144,35 @@ data Settings = Settings -- Remember to keep it the same in all services. -- This is referred to as the 'backend domain' in the public documentation; See -- https://docs.wire.com/how-to/install/configure-federation.html#choose-a-backend-domain-name - _federationDomain :: !Domain, - _disabledAPIVersions :: !(Set VersionExp) + federationDomain :: !Domain, + disabledAPIVersions :: !(Set VersionExp) } deriving (Show, Generic) -deriveFromJSON toOptionFieldName ''Settings - -makeLenses ''Settings +deriveFromJSON defaultOptions ''Settings -- | Options consist of information the server needs to operate, and 'Settings' -- modify the behavior. data Opts = Opts { -- | Hostname and port to bind to - _cargohold :: !Endpoint, - _aws :: !AWSOpts, - _settings :: !Settings, + cargohold :: !Endpoint, + aws :: !AWSOpts, + settings :: !Settings, -- | Federator endpoint - _federator :: Maybe Endpoint, + federator :: Maybe Endpoint, -- | Brig endpoint - _brig :: !Endpoint, + brig :: !Endpoint, -- Logging -- | Log level (Debug, Info, etc) - _logLevel :: !Level, + logLevel :: !Level, -- | Use netstrings encoding: -- - _logNetStrings :: !(Maybe (Last Bool)), - _logFormat :: !(Maybe (Last LogFormat)) --- ^ Log format + logNetStrings :: !(Maybe (Last Bool)), + logFormat :: !(Maybe (Last LogFormat)) --- ^ Log format } deriving (Show, Generic) -deriveFromJSON toOptionFieldName ''Opts +deriveFromJSON defaultOptions ''Opts -makeLenses ''Opts +makeLensesWith (lensRules & lensField .~ suffixNamer) ''Opts diff --git a/services/cargohold/src/CargoHold/Run.hs b/services/cargohold/src/CargoHold/Run.hs index eeee6b32ab2..a678a7dd053 100644 --- a/services/cargohold/src/CargoHold/Run.hs +++ b/services/cargohold/src/CargoHold/Run.hs @@ -27,10 +27,10 @@ import qualified Amazonka as AWS import CargoHold.API.Federation import CargoHold.API.Public import CargoHold.AWS (amazonkaEnv) -import CargoHold.App hiding (settings) +import CargoHold.App import CargoHold.Options hiding (aws) import Control.Exception (bracket) -import Control.Lens ((.~), (^.)) +import Control.Lens ((.~)) import Control.Monad.Codensity import Data.Metrics.AWS (gaugeTokenRemaing) import Data.Metrics.Servant @@ -58,14 +58,14 @@ type CombinedAPI = FederationAPI :<|> CargoholdAPI :<|> InternalAPI run :: Opts -> IO () run o = lowerCodensity $ do (app, e) <- mkApp o - void $ Codensity $ Async.withAsync (collectAuthMetrics (e ^. aws . amazonkaEnv)) + void $ Codensity $ Async.withAsync (collectAuthMetrics e.aws.amazonkaEnv) liftIO $ do s <- Server.newSettings $ defaultServer - (unpack $ o ^. cargohold . host) - (o ^. cargohold . port) - (e ^. appLogger) + (unpack . host $ o.cargohold) + (port o.cargohold) + e.appLogger runSettingsWithShutdown s app Nothing mkApp :: Opts -> Codensity IO (Application, Env) @@ -75,18 +75,18 @@ mkApp o = Codensity $ \k -> where middleware :: Env -> Wai.Middleware middleware e = - versionMiddleware (foldMap expandVersionExp (o ^. settings . disabledAPIVersions)) - . requestIdMiddleware (e ^. appLogger) defaultRequestIdHeaderName + versionMiddleware (foldMap expandVersionExp o.settings.disabledAPIVersions) + . requestIdMiddleware e.appLogger defaultRequestIdHeaderName . servantPrometheusMiddleware (Proxy @CombinedAPI) . GZip.gzip GZip.def - . catchErrors (e ^. appLogger) defaultRequestIdHeaderName + . catchErrors e.appLogger defaultRequestIdHeaderName servantApp :: Env -> Application servantApp e0 r cont = do let rid = getRequestId defaultRequestIdHeaderName r - e = requestId .~ rid $ e0 + e = requestIdLens .~ rid $ e0 Servant.serveWithContext (Proxy @CombinedAPI) - ((o ^. settings . federationDomain) :. Servant.EmptyContext) + (o.settings.federationDomain :. Servant.EmptyContext) ( hoistServerWithDomain @FederationAPI (toServantHandler e) federationSitemap :<|> hoistServerWithDomain @CargoholdAPI (toServantHandler e) servantSitemap :<|> hoistServerWithDomain @InternalAPI (toServantHandler e) internalSitemap diff --git a/services/cargohold/src/CargoHold/S3.hs b/services/cargohold/src/CargoHold/S3.hs index 849b77dcda8..35b16733a48 100644 --- a/services/cargohold/src/CargoHold/S3.hs +++ b/services/cargohold/src/CargoHold/S3.hs @@ -43,7 +43,7 @@ import CargoHold.API.Error import CargoHold.AWS (amazonkaEnvWithDownloadEndpoint) import qualified CargoHold.AWS as AWS import CargoHold.App hiding (Env, Handler) -import CargoHold.Options (downloadLinkTTL) +import CargoHold.Options import qualified CargoHold.Types.V3 as V3 import qualified Codec.MIME.Parse as MIME import qualified Codec.MIME.Type as MIME @@ -144,7 +144,7 @@ downloadV3 :: V3.AssetKey -> ExceptT Error App (ConduitM () ByteString (ResourceT IO) ()) downloadV3 (s3Key . mkKey -> key) = do - env <- view aws + env <- asks (.aws) pure . flattenResourceT $ view (getObjectResponse_body . _ResponseBody) <$> AWS.execStream env req where req :: Text -> GetObject @@ -216,10 +216,9 @@ updateMetadataV3 (s3Key . mkKey -> key) (S3AssetMeta prc tok _) = do signedURL :: (ToByteString p) => p -> Maybe Text -> ExceptT Error App URI signedURL path mbHost = do e <- awsEnvForHost - let b = view AWS.s3Bucket e now <- liftIO getCurrentTime - ttl <- view (settings . downloadLinkTTL) - let req = newGetObject (BucketName b) (ObjectKey . Text.decodeLatin1 $ toByteString' path) + ttl <- asks (.options.settings.downloadLinkTTL) + let req = newGetObject (BucketName e.s3Bucket) (ObjectKey . Text.decodeLatin1 $ toByteString' path) signed <- presignURL (amazonkaEnvWithDownloadEndpoint e) now (Seconds (fromIntegral ttl)) req toUri signed @@ -235,9 +234,9 @@ signedURL path mbHost = do awsEnvForHost :: ExceptT Error App AWS.Env awsEnvForHost = do - multiIngressConf <- view multiIngress + multiIngressConf <- asks (.multiIngress) if null multiIngressConf - then view aws + then asks (.aws) else awsEnvForHost' mbHost multiIngressConf where awsEnvForHost' :: Maybe Text -> Map String AWS.Env -> ExceptT Error App AWS.Env @@ -262,7 +261,7 @@ signedURL path mbHost = do "host" .= host ~~ "s3DownloadEndpoint" - .= show (hostAwsEnv ^. AWS.amazonkaDownloadEndpoint) + .= show hostAwsEnv.amazonkaDownloadEndpoint ~~ msg (val "awsEnvForHost - multiIngress lookup succeed, using specific AWS env.") pure hostAwsEnv @@ -359,7 +358,7 @@ exec :: (Text -> r) -> ExceptT Error App (AWSResponse r) exec req = do - env <- view aws + env <- asks (.aws) AWS.exec env req execCatch :: @@ -371,7 +370,7 @@ execCatch :: (Text -> r) -> ExceptT Error App (Maybe (AWSResponse r)) execCatch req = do - env <- view aws + env <- asks (.aws) AWS.execCatch env req -------------------------------------------------------------------------------- diff --git a/services/cargohold/src/CargoHold/Util.hs b/services/cargohold/src/CargoHold/Util.hs index 58b27d4fad1..78c2d1a3e6b 100644 --- a/services/cargohold/src/CargoHold/Util.hs +++ b/services/cargohold/src/CargoHold/Util.hs @@ -21,7 +21,6 @@ import CargoHold.AWS import CargoHold.App import qualified CargoHold.CloudFront as CloudFront import qualified CargoHold.S3 as S3 -import Control.Lens import Data.ByteString.Conversion import Imports import URI.ByteString hiding (urlEncode) @@ -29,7 +28,7 @@ import URI.ByteString hiding (urlEncode) genSignedURL :: (ToByteString p) => p -> Maybe Text -> Handler URI genSignedURL path mbHost = do uri <- - view (aws . cloudFront) >>= \case + asks (.aws.cloudFront) >>= \case Nothing -> S3.signedURL path mbHost Just cf -> CloudFront.signedURL cf path pure $! uri diff --git a/services/cargohold/test/integration/API/Util.hs b/services/cargohold/test/integration/API/Util.hs index 2c51dc9b29f..45aac302da6 100644 --- a/services/cargohold/test/integration/API/Util.hs +++ b/services/cargohold/test/integration/API/Util.hs @@ -16,45 +16,25 @@ -- with this program. If not, see . module API.Util - ( randomUser, - uploadSimple, - decodeHeaderOrFail, - getContentType, - applicationText, - applicationOctetStream, - deleteAssetV3, - deleteAsset, - downloadAsset, + ( downloadAsset, withMockFederator, ) where import Bilge hiding (body, host, port) -import qualified Bilge import CargoHold.Options import CargoHold.Run -import qualified Codec.MIME.Parse as MIME -import qualified Codec.MIME.Type as MIME import Control.Lens hiding ((.=)) import Control.Monad.Codensity -import Data.Aeson (object, (.=)) -import Data.ByteString.Builder -import qualified Data.ByteString.Char8 as C import Data.ByteString.Conversion -import qualified Data.ByteString.Lazy as Lazy import Data.Default import Data.Id import Data.Qualified -import Data.Text.Encoding (decodeLatin1, encodeUtf8) import qualified Data.UUID as UUID -import Data.UUID.V4 (nextRandom) import Federator.MockServer import Imports hiding (head) import qualified Network.HTTP.Media as HTTP -import Network.HTTP.Types.Header -import Network.HTTP.Types.Method import Network.Wai.Utilities.MockServer -import Safe (readNote) import TestSetup import Util.Options import Wire.API.Asset @@ -64,93 +44,10 @@ import Wire.API.Asset -- The changes to the asset routes forbidding non-verified users from uploading -- assets breaks a lot of existing tests. -- --- FUTUREWORK: Move all the cargohold tests to the new integration test suite. --- https://wearezeta.atlassian.net/browse/WPB-5382 -randomUser :: TestM UserId -randomUser = do - (Endpoint (encodeUtf8 -> eHost) ePort) <- view tsBrig - e <- liftIO $ mkEmail "success" "simulator.amazonses.com" - let p = - object - [ "name" .= e, - "email" .= e, - "password" .= ("secret-8-chars-long-at-least" :: Text) - ] - r <- post (Bilge.host eHost . Bilge.port ePort . path "/i/users" . json p) - pure - . readNote "unable to parse Location header" - . C.unpack - $ getHeader' "Location" r - where - mkEmail loc dom = do - uid <- nextRandom - pure $ loc <> "+" <> UUID.toText uid <> "@" <> dom - -uploadSimple :: - (Request -> Request) -> - UserId -> - AssetSettings -> - (MIME.Type, ByteString) -> - TestM (Response (Maybe Lazy.ByteString)) -uploadSimple c usr sts (ct, bs) = - let mp = buildMultipartBody sts ct (Lazy.fromStrict bs) - in uploadRaw c usr (toLazyByteString mp) - -decodeHeaderOrFail :: (HasCallStack, FromByteString a) => HeaderName -> Response b -> a -decodeHeaderOrFail h = - fromMaybe (error $ "decodeHeaderOrFail: missing or invalid header: " ++ show h) - . fromByteString - . getHeader' h - -uploadRaw :: - (Request -> Request) -> - UserId -> - Lazy.ByteString -> - TestM (Response (Maybe Lazy.ByteString)) -uploadRaw c usr bs = do - cargohold' <- viewUnversionedCargohold - post $ - apiVersion "v1" - . c - . cargohold' - . method POST - . zUser usr - . zConn "conn" - . content "multipart/mixed" - . lbytes bs - -getContentType :: Response a -> Maybe MIME.Type -getContentType = MIME.parseContentType . decodeLatin1 . getHeader' "Content-Type" - -applicationText :: MIME.Type -applicationText = MIME.Type (MIME.Application "text") [] - -applicationOctetStream :: MIME.Type -applicationOctetStream = MIME.Type (MIME.Application "octet-stream") [] zUser :: UserId -> Request -> Request zUser = header "Z-User" . UUID.toASCIIBytes . toUUID -zConn :: ByteString -> Request -> Request -zConn = header "Z-Connection" - -deleteAssetV3 :: UserId -> Qualified AssetKey -> TestM (Response (Maybe Lazy.ByteString)) -deleteAssetV3 u k = do - c <- viewUnversionedCargohold - delete $ apiVersion "v1" . c . zUser u . paths ["assets", "v3", toByteString' (qUnqualified k)] - -deleteAsset :: UserId -> Qualified AssetKey -> TestM (Response (Maybe Lazy.ByteString)) -deleteAsset u k = do - c <- viewCargohold - delete $ - c - . zUser u - . paths - [ "assets", - toByteString' (qDomain k), - toByteString' (qUnqualified k) - ] - class IsAssetLocation key where locationPath :: key -> Request -> Request @@ -210,14 +107,14 @@ downloadAsset = downloadAssetWith id withSettingsOverrides :: (Opts -> Opts) -> TestM a -> TestM a withSettingsOverrides f action = do ts <- ask - let opts = f (view tsOpts ts) + let opts = f ts.opts liftIO . lowerCodensity $ do (app, _) <- mkApp opts p <- withMockServer app - liftIO $ runTestM (ts & tsEndpoint %~ setLocalEndpoint p) action + liftIO $ runTestM (ts & endpointLens %~ setLocalEndpoint p) action setLocalEndpoint :: Word16 -> Endpoint -> Endpoint -setLocalEndpoint p = (port .~ p) . (host .~ "127.0.0.1") +setLocalEndpoint port endpoint = endpoint {port = port, host = "127.0.0.1"} withMockFederator :: (FederatedRequest -> IO (HTTP.MediaType, LByteString)) -> @@ -226,5 +123,5 @@ withMockFederator :: withMockFederator respond action = do withTempMockFederator def {handler = respond} $ \p -> withSettingsOverrides - (federator . _Just %~ setLocalEndpoint (fromIntegral p)) + (federatorLens . _Just %~ setLocalEndpoint (fromIntegral p)) action diff --git a/services/cargohold/test/integration/App.hs b/services/cargohold/test/integration/App.hs index b5ba0132d88..3f2fb0ec810 100644 --- a/services/cargohold/test/integration/App.hs +++ b/services/cargohold/test/integration/App.hs @@ -30,9 +30,9 @@ testMultiIngressCloudFrontFails :: TestM () testMultiIngressCloudFrontFails = do ts <- ask let opts = - view tsOpts ts - & (Opts.aws . Opts.cloudFront) ?~ cloudFrontOptions - & (Opts.aws . Opts.multiIngress) ?~ multiIngressMap + view optsLens ts + & (Opts.awsLens . Opts.cloudFrontLens) ?~ cloudFrontOptions + & (Opts.awsLens . Opts.multiIngressLens) ?~ multiIngressMap msg <- liftIO $ catch @@ -46,9 +46,9 @@ testMultiIngressCloudFrontFails = do cloudFrontOptions :: CloudFrontOpts cloudFrontOptions = CloudFrontOpts - { _domain = Domain (T.pack "example.com"), - _keyPairId = KeyPairId (T.pack "anyId"), - _privateKey = "any/path" + { domain = Domain (T.pack "example.com"), + keyPairId = KeyPairId (T.pack "anyId"), + privateKey = "any/path" } multiIngressMap :: Map String AWSEndpoint @@ -64,9 +64,9 @@ testMultiIngressS3DownloadEndpointFails :: TestM () testMultiIngressS3DownloadEndpointFails = do ts <- ask let opts = - view tsOpts ts - & (Opts.aws . Opts.s3DownloadEndpoint) ?~ toAWSEndpoint "http://fake-s3:4570" - & (Opts.aws . Opts.multiIngress) ?~ multiIngressMap + view optsLens ts + & (Opts.awsLens . Opts.s3DownloadEndpointLens) ?~ toAWSEndpoint "http://fake-s3:4570" + & (Opts.awsLens . Opts.multiIngressLens) ?~ multiIngressMap msg <- liftIO $ catch diff --git a/services/cargohold/test/integration/TestSetup.hs b/services/cargohold/test/integration/TestSetup.hs index 93f361e34c3..629158039c0 100644 --- a/services/cargohold/test/integration/TestSetup.hs +++ b/services/cargohold/test/integration/TestSetup.hs @@ -19,10 +19,10 @@ module TestSetup ( test, - tsManager, - tsEndpoint, - tsBrig, - tsOpts, + managerLens, + endpointLens, + TestSetup.brigLens, + optsLens, TestSetup (..), Cargohold, TestM, @@ -31,10 +31,7 @@ module TestSetup viewCargohold, createTestSetup, runFederationClient, - withFederationClient, - withFederationError, apiVersion, - unversioned, ) where @@ -45,7 +42,6 @@ import Control.Lens import Control.Monad.Codensity import Control.Monad.Except import Control.Monad.Morph -import qualified Data.Aeson as Aeson import qualified Data.ByteString.Char8 as B8 import Data.ByteString.Conversion import qualified Data.Text as T @@ -55,12 +51,12 @@ import Imports import Network.HTTP.Client hiding (responseBody) import qualified Network.HTTP.Client as HTTP import Network.HTTP.Client.TLS -import qualified Network.Wai.Utilities.Error as Wai import Servant.Client.Streaming import Test.Tasty import Test.Tasty.HUnit import Util.Options (Endpoint (..)) import Util.Options.Common +import Util.SuffixNamer import Util.Test import Web.HttpApiData import Wire.API.Federation.Domain @@ -74,13 +70,13 @@ mkRequest :: Endpoint -> Request -> Request mkRequest (Endpoint h p) = Bilge.host (encodeUtf8 h) . Bilge.port p data TestSetup = TestSetup - { _tsManager :: Manager, - _tsEndpoint :: Endpoint, - _tsBrig :: Endpoint, - _tsOpts :: Opts + { manager :: Manager, + endpoint :: Endpoint, + brig :: Endpoint, + opts :: Opts } -makeLenses ''TestSetup +makeLensesWith (lensRules & lensField .~ suffixNamer) ''TestSetup -- | Note: Apply this function last when composing (Request -> Request) functions apiVersion :: ByteString -> Request -> Request @@ -103,16 +99,8 @@ removeVersionPrefix bs = do (_, s') <- B8.readInteger s pure (B8.tail s') --- | Note: Apply this function last when composing (Request -> Request) functions -unversioned :: Request -> Request -unversioned r = - r - { HTTP.path = - maybe - (HTTP.path r) - (B8.pack "/" <>) - (removeVersionPrefix . removeSlash' $ HTTP.path r) - } +viewUnversionedCargohold :: TestM Cargohold +viewUnversionedCargohold = mkRequest <$> asks (.endpoint) viewCargohold :: TestM Cargohold viewCargohold = @@ -123,11 +111,8 @@ viewCargohold = latestVersion :: Version latestVersion = maxBound -viewUnversionedCargohold :: TestM Cargohold -viewUnversionedCargohold = mkRequest <$> view tsEndpoint - runTestM :: TestSetup -> TestM a -> IO a -runTestM ts action = runHttpT (view tsManager ts) (runReaderT action ts) +runTestM ts action = runHttpT ts.manager (runReaderT action ts) test :: IO TestSetup -> TestName -> TestM () -> TestTree test s name action = testCase name $ do @@ -154,24 +139,24 @@ createTestSetup optsPath configPath = do tlsManagerSettings { managerResponseTimeout = responseTimeoutMicro 300000000 } - let localEndpoint p = Endpoint {_host = "127.0.0.1", _port = p} + let localEndpoint p = Endpoint {host = "127.0.0.1", port = p} iConf <- handleParseError =<< decodeFileEither configPath opts <- decodeFileThrow optsPath endpoint <- optOrEnv @IntegrationConfig (.cargohold) iConf (localEndpoint . read) "CARGOHOLD_WEB_PORT" brigEndpoint <- optOrEnv @IntegrationConfig (.brig) iConf (localEndpoint . read) "BRIG_WEB_PORT" pure $ TestSetup - { _tsManager = m, - _tsEndpoint = endpoint, - _tsBrig = brigEndpoint, - _tsOpts = opts + { manager = m, + endpoint = endpoint, + brig = brigEndpoint, + opts = opts } runFederationClient :: ClientM a -> ReaderT TestSetup (ExceptT ClientError (Codensity IO)) a runFederationClient action = do - man <- view tsManager - Endpoint cHost cPort <- view tsEndpoint - domain <- view (tsOpts . settings . federationDomain) + man <- asks (.manager) + Endpoint cHost cPort <- asks (.endpoint) + domain <- asks (.opts.settings.federationDomain) let base = BaseUrl Http (T.unpack cHost) (fromIntegral cPort) "/federation" let env = (mkClientEnv man base) @@ -188,29 +173,3 @@ runFederationClient action = do catch (withClientM action env k) (k . Left) either throwError pure r - -hoistFederation :: ReaderT TestSetup (ExceptT ClientError (Codensity IO)) a -> ExceptT ClientError TestM a -hoistFederation action = do - env <- ask - hoist (liftIO . lowerCodensity) $ runReaderT action env - -withFederationClient :: ReaderT TestSetup (ExceptT ClientError (Codensity IO)) a -> TestM a -withFederationClient action = - runExceptT (hoistFederation action) >>= \case - Left err -> - liftIO - . assertFailure - $ "Unexpected federation client error: " - <> displayException err - Right x -> pure x - -withFederationError :: ReaderT TestSetup (ExceptT ClientError (Codensity IO)) a -> TestM Wai.Error -withFederationError action = - runExceptT (hoistFederation action) - >>= liftIO - . \case - Left (FailureResponse _ resp) -> case Aeson.eitherDecode (responseBody resp) of - Left err -> assertFailure $ "Error while parsing error response: " <> err - Right e -> (Wai.code e @?= responseStatusCode resp) $> e - Left err -> assertFailure $ "Unexpected federation client error: " <> displayException err - Right _ -> assertFailure "Unexpected success" diff --git a/services/federator/default.nix b/services/federator/default.nix index 9b687bbd39e..cb42cc59c93 100644 --- a/services/federator/default.nix +++ b/services/federator/default.nix @@ -25,7 +25,6 @@ , hinotify , HsOpenSSL , hspec -, hspec-core , hspec-junit-formatter , http-client , http-client-tls @@ -139,11 +138,11 @@ mkDerivation { bytestring-conversion crypton crypton-connection + data-default dns-util exceptions HsOpenSSL hspec - hspec-core hspec-junit-formatter http-client-tls http-types diff --git a/services/federator/federator.cabal b/services/federator/federator.cabal index 4c88186b527..65f9a0ef444 100644 --- a/services/federator/federator.cabal +++ b/services/federator/federator.cabal @@ -285,12 +285,12 @@ executable federator-integration , bytestring-conversion , crypton , crypton-connection + , data-default , dns-util , exceptions , federator , HsOpenSSL , hspec - , hspec-core , hspec-junit-formatter , http-client-tls , http-types diff --git a/services/federator/src/Federator/Error.hs b/services/federator/src/Federator/Error.hs index 7b6f06342d9..28d52c18a9e 100644 --- a/services/federator/src/Federator/Error.hs +++ b/services/federator/src/Federator/Error.hs @@ -17,17 +17,10 @@ module Federator.Error ( AsWai (..), - errorResponse, ) where -import Data.Aeson qualified as A -import Network.HTTP.Types.Header -import Network.Wai qualified as Wai import Network.Wai.Utilities.Error qualified as Wai class AsWai e where toWai :: e -> Wai.Error - -errorResponse :: [Header] -> Wai.Error -> Wai.Response -errorResponse hdrs e = Wai.responseLBS (Wai.code e) hdrs (A.encode e) diff --git a/services/federator/src/Federator/Interpreter.hs b/services/federator/src/Federator/Interpreter.hs index 2042ab1e043..e59f6a4cb0c 100644 --- a/services/federator/src/Federator/Interpreter.hs +++ b/services/federator/src/Federator/Interpreter.hs @@ -42,8 +42,8 @@ import Servant hiding (ServerError, respond, serve) import Servant.Client (mkClientEnv) import Servant.Client.Core import Util.Options (Endpoint (..)) +import Wire.API.Component (Component (Brig)) import Wire.API.FederationUpdate qualified as FedUp (getFederationDomainConfigs) -import Wire.API.MakesFederatedCall (Component (Brig)) import Wire.API.Routes.FederationDomainConfig qualified as FedUp (FederationDomainConfigs) import Wire.Network.DNS.Effect import Wire.Sem.Logger.TinyLog diff --git a/services/federator/src/Federator/Run.hs b/services/federator/src/Federator/Run.hs index c02d9f25f7d..83b9883b414 100644 --- a/services/federator/src/Federator/Run.hs +++ b/services/federator/src/Federator/Run.hs @@ -73,10 +73,10 @@ run opts = do void $ waitAnyCancel [internalServerThread, externalServerThread] where endpointInternal = federatorInternal opts - portInternal = fromIntegral $ endpointInternal ^. port + portInternal = fromIntegral $ endpointInternal.port endpointExternal = federatorExternal opts - portExternal = fromIntegral $ endpointExternal ^. port + portExternal = fromIntegral $ endpointExternal.port mkResolvConf :: RunSettings -> DNS.ResolvConf -> DNS.ResolvConf mkResolvConf settings conf = @@ -92,13 +92,13 @@ run opts = do newEnv :: Opts -> DNS.Resolver -> Log.Logger -> IO Env newEnv o _dnsResolver _applog = do - let _requestId = RequestId "N/A" - _runSettings = Opt.optSettings o - _service Brig = Opt.brig o - _service Galley = Opt.galley o - _service Cargohold = Opt.cargohold o - _externalPort = o.federatorExternal._port - _internalPort = o.federatorInternal._port + let _requestId = RequestId defRequestId + _runSettings = o.optSettings + _service Brig = o.brig + _service Galley = o.galley + _service Cargohold = o.cargohold + _externalPort = o.federatorExternal.port + _internalPort = o.federatorInternal.port _httpManager <- initHttpManager sslContext <- mkTLSSettingsOrThrow _runSettings _http2Manager <- newIORef =<< mkHttp2Manager o.optSettings.tcpConnectionTimeout sslContext diff --git a/services/federator/src/Federator/Validation.hs b/services/federator/src/Federator/Validation.hs index 38c315a6498..362c081d4ed 100644 --- a/services/federator/src/Federator/Validation.hs +++ b/services/federator/src/Federator/Validation.hs @@ -18,7 +18,6 @@ module Federator.Validation ( ensureCanFederateWith, parseDomain, - parseDomainText, decodeCertificate, validateDomain, validateDomainName, @@ -127,13 +126,6 @@ parseDomain domain = note (DomainParseError (Text.decodeUtf8With Text.lenientDecode domain)) $ fromByteString domain -parseDomainText :: (Member (Error ValidationError) r) => Text -> Sem r Domain -parseDomainText domain = - mapError @String (const (DomainParseError domain)) - . fromEither - . mkDomain - $ domain - -- | Validates an unknown domain string against the allow list using the -- federator startup configuration and checks that it matches the names reported -- by the client certificate diff --git a/services/federator/test/integration/Main.hs b/services/federator/test/integration/Main.hs index d63572adf78..a18f17708ed 100644 --- a/services/federator/test/integration/Main.hs +++ b/services/federator/test/integration/Main.hs @@ -14,13 +14,11 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . +{-# OPTIONS_GHC -Wno-deprecations #-} -module Main - ( main, - ) -where +module Main (main) where -import Control.Concurrent.Async +import Control.Concurrent.Async (concurrently_) import Imports import OpenSSL (withOpenSSL) import System.Environment (withArgs) @@ -28,7 +26,6 @@ import Test.Federator.IngressSpec qualified import Test.Federator.InwardSpec qualified import Test.Federator.Util (TestEnv, mkEnvFromOptions) import Test.Hspec -import Test.Hspec.Core.Format import Test.Hspec.JUnit import Test.Hspec.JUnit.Config.Env import Test.Hspec.Runner @@ -39,8 +36,14 @@ main = withOpenSSL $ do env <- withArgs wireArgs mkEnvFromOptions -- withArgs hspecArgs . hspec $ do -- beforeAll (pure env) . afterAll destroyEnv $ Hspec.mkspec - cfg <- hspecConfig - withArgs hspecArgs . hspecWith cfg $ mkspec env + + -- FUTUREWORK(mangoiv): we should remove the deprecated module and instaed move to this config, however, this + -- needs check of whether it modifies the behaviour + -- junitConfig <- envJUnitConfig + -- withArgs hspecArgs . hspec . add junitConfig $ do + + conf <- hspecConfig + withArgs hspecArgs . hspecWith conf $ mkspec env hspecConfig :: IO Config hspecConfig = do @@ -52,7 +55,6 @@ hspecConfig = do : configAvailableFormatters defaultConfig } where - checksAndJUnitFormatter :: JUnitConfig -> FormatConfig -> IO Format checksAndJUnitFormatter junitConfig config = do junit <- junitFormat junitConfig config let checksFormatter = fromJust (lookup "checks" $ configAvailableFormatters defaultConfig) diff --git a/services/federator/test/integration/Test/Federator/IngressSpec.hs b/services/federator/test/integration/Test/Federator/IngressSpec.hs index 2165edb0761..41b9f42af3c 100644 --- a/services/federator/test/integration/Test/Federator/IngressSpec.hs +++ b/services/federator/test/integration/Test/Federator/IngressSpec.hs @@ -75,6 +75,7 @@ spec env = do it "testRejectRequestsWithoutClientCertIngress" (testRejectRequestsWithoutClientCertIngress env) +-- @SF.Federation @TSFI.RESTfulAPI @S2 @S3 @S7 -- -- This test was primarily intended to test that federator is using the API right (header -- name etc.), but it is also effectively testing that federator rejects clients without @@ -110,6 +111,8 @@ testRejectRequestsWithoutClientCertIngress env = runTestFederator env $ do expectationFailure "Expected client certificate error, got remote error" Left (RemoteErrorResponse _ _ status _) -> status `shouldBe` HTTP.status400 +-- @END + liftToCodensity :: (Member (Embed (Codensity IO)) r) => Sem (Embed IO ': r) a -> Sem r a liftToCodensity = runEmbedded @IO @(Codensity IO) lift @@ -147,7 +150,7 @@ inwardBrigCallViaIngressWithSettings sslCtx requestPath payload = mgr <- liftToCodensity . liftIO $ http2ManagerWithSSLCtx sslCtx liftToCodensity . runInputConst mgr - . runInputConst (RequestId "N/A") + . runInputConst (RequestId defRequestId) . assertNoError @DiscoveryFailure . discoverConst target . interpretRemote diff --git a/services/federator/test/integration/Test/Federator/InwardSpec.hs b/services/federator/test/integration/Test/Federator/InwardSpec.hs index 85980f8948c..1daee8e77e4 100644 --- a/services/federator/test/integration/Test/Federator/InwardSpec.hs +++ b/services/federator/test/integration/Test/Federator/InwardSpec.hs @@ -112,11 +112,17 @@ spec env = it "testRejectRequestsWithoutClientCertInward" (testRejectRequestsWithoutClientCertInward env) +-- @SF.Federation @TSFI.RESTfulAPI @S2 @S3 @S7 +-- -- This test is covered by the unit tests 'validateDomainCertWrongDomain' because -- the domain matching is checked on certificate validation. testShouldRejectMissmatchingOriginDomainInward :: TestEnv -> IO () testShouldRejectMissmatchingOriginDomainInward env = runTestFederator env $ pure () +-- @END + +-- @SF.Federation @TSFI.RESTfulAPI @S2 @S3 @S7 +-- -- See related tests in unit tests (for matching client certificates against domain names) -- and "IngressSpec". testRejectRequestsWithoutClientCertInward :: TestEnv -> IO () @@ -129,6 +135,8 @@ testRejectRequestsWithoutClientCertInward env = runTestFederator env $ do (encode hdl) !!! const 400 === statusCode +-- @END + inwardCallWithHeaders :: (MonadIO m, MonadHttp m, MonadReader TestEnv m, HasCallStack) => ByteString -> diff --git a/services/federator/test/integration/Test/Federator/Util.hs b/services/federator/test/integration/Test/Federator/Util.hs index 92d80d7d752..dea547dd52b 100644 --- a/services/federator/test/integration/Test/Federator/Util.hs +++ b/services/federator/test/integration/Test/Federator/Util.hs @@ -35,6 +35,7 @@ import Data.Aeson import Data.Aeson.TH import Data.Aeson.Types qualified as Aeson import Data.ByteString.Char8 qualified as C8 +import Data.Default (def) import Data.Id import Data.Misc import Data.String.Conversions @@ -151,20 +152,17 @@ cliOptsParser = -- | Create an environment for integration tests from integration and federator config files. mkEnv :: (HasCallStack) => IntegrationConfig -> Opts -> IO TestEnv mkEnv _teTstOpts _teOpts = do - let managerSettings = mkManagerSettings (Network.Connection.TLSSettingsSimple True False False) Nothing + let managerSettings = mkManagerSettings (Network.Connection.TLSSettingsSimple True False False def) Nothing _teMgr :: Manager <- newManager managerSettings let _teBrig = endpointToReq _teTstOpts.brig _teCargohold = endpointToReq _teTstOpts.cargohold -- _teTLSSettings <- mkTLSSettingsOrThrow (optSettings _teOpts) - _teSSLContext <- mkTLSSettingsOrThrow (optSettings _teOpts) - let _teSettings = optSettings _teOpts + _teSSLContext <- mkTLSSettingsOrThrow _teOpts.optSettings + let _teSettings = _teOpts.optSettings pure TestEnv {..} -destroyEnv :: (HasCallStack) => TestEnv -> IO () -destroyEnv _ = pure () - endpointToReq :: Endpoint -> (Bilge.Request -> Bilge.Request) -endpointToReq ep = Bilge.host (ep ^. O.host . to cs) . Bilge.port (ep ^. O.port) +endpointToReq ep = Bilge.host (cs ep.host) . Bilge.port ep.port -- All the code below is copied from brig-integration tests -- FUTUREWORK: This should live in another package and shared by all the integration tests @@ -182,22 +180,15 @@ randomUser' :: m User randomUser' hasPwd brig = do n <- fromName <$> randomName - createUser' hasPwd n brig + createUser hasPwd n brig createUser :: - (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => - Text -> - BrigReq -> - m User -createUser = createUser' True - -createUser' :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Bool -> Text -> BrigReq -> m User -createUser' hasPwd name brig = do +createUser hasPwd name brig = do r <- postUser' hasPwd True name True False Nothing Nothing brig Bool -> Text -> - Maybe Email -> + Maybe EmailAddress -> Bool -> Maybe UserSSOId -> Maybe TeamId -> @@ -319,17 +310,17 @@ defCookieLabel = CookieLabel "auth" -- | Generate emails that are in the trusted whitelist of domains whose @+@ suffices count for email -- disambiguation. See also: 'Brig.Email.mkEmailKey'. -randomEmail :: (MonadIO m) => m Email +randomEmail :: (MonadIO m) => m EmailAddress randomEmail = mkSimulatorEmail "success" -mkSimulatorEmail :: (MonadIO m) => Text -> m Email +mkSimulatorEmail :: (MonadIO m) => Text -> m EmailAddress mkSimulatorEmail loc = mkEmailRandomLocalSuffix (loc <> "@simulator.amazonses.com") -mkEmailRandomLocalSuffix :: (MonadIO m) => Text -> m Email +mkEmailRandomLocalSuffix :: (MonadIO m) => Text -> m EmailAddress mkEmailRandomLocalSuffix e = do uid <- liftIO UUID.nextRandom - case parseEmail e of - Just (Email loc dom) -> pure $ Email (loc <> "+" <> UUID.toText uid) dom + case emailAddressText e of + Just mail -> pure $ unsafeEmailAddress ((localPart mail) <> "+" <> UUID.toASCIIBytes uid) (domainPart mail) Nothing -> error $ "Invalid email address: " ++ Text.unpack e zUser :: UserId -> Bilge.Request -> Bilge.Request diff --git a/services/federator/test/resources/unit/localhost-dot-key.pem b/services/federator/test/resources/unit/localhost-dot-key.pem index 28872746b1a..59b9a0128e3 100644 --- a/services/federator/test/resources/unit/localhost-dot-key.pem +++ b/services/federator/test/resources/unit/localhost-dot-key.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAvgZ8XblpKvf46pVOL0t9TFAYyXYylP65fFp/Uiim9+3YO27m -Pcv8CUMLRov08xAUUpp8ufR0wqfEZ6Q32CwXhuNEX/JESbW4HX+wMLAcOacd117T -/4iDQJCYSg/uFoVJhCeUVSiigbB2cD6F4XKnFw7xy9AKnVzLOSHo+NdtblNRkoy5 -0JJBJ2hu4rrqtq+NNE0Fky4vfrSQ+N0wQnAu3+UwZLulJhLhIlDiY5zcZLZ1Tw92 -/CpK7L7AKFReFAH+1lfhKDuOUi+XAerHycOSTYT05Qoye2ekqLwAb0n7EIIqCEIB -mAk+7klhe51BfxfOUHOmhTUPG6C5bKD1+q0ssQIDAQABAoIBAQChk0g6NUY8N/9a -D7wcMNcIhW6eFrepwvGa0CREirZ2R7G9z21MjF7wzSYQRT7xUfHFzwBQ6ZBCV36E -Fbk6QTt8AVCJOKlh93bm9kStEYHeb7/K+iHOvJfF1Wz4RJVQZuL80N6qjlOnbJE4 -naEe8msrCxUEFRCBf355ROEgfaTZ0d0AxQiD9VL0vKjE/n1SAx0MN5x/zEfwpNAV -SIoUXo/BobLKqac5c8R61P+92nUVHKqVBL4+BquJ5/GGpn5m6k4eEsL6hp9ADCgI -sWNOPX0rcSXAwg4aIhUqNRlJQLJax+vkFVV+7usW4qyghJeWKHOeaHca8AhUCSRg -rlVaaa6VAoGBAND5eNgTStHB77QNKjXEuU3OiBs9gAB3ABX2/snOFdlMaP0Xo6da -DpQs9W31VEZA4IltoeYc4P3Z3Jgi6WTKKjCMNKW42CWz1ZH38aIKYR/z9dYrVbk4 -NdbzK2kNPtTxMekNAIosdEzeifrq0ewetfM0abRkaLXwRNGkvUFlaOEfAoGBAOjJ -ZaYdMWz9JNYwPX6VCro1BPT1bZyf56wtIvyeXdWWMre1GX1TWLco1KfGHXaH3cTz -24pKQykxK1wLnEfjxBMRrZWgp/TjaURKKJNnOOa/f4p5JrCn6rlXvsNkS3Bhxy4P -DgZiSUl8a2bdAxqiYkeR1A0nF2PQNk08Op1D4SgvAoGAdMjGSPLXIEwielvVGAFd -kWb3V0nSS/A1AxFqqDrcqPhzv+VDFxZUCWjqq82rkCtXkFXNYX0IG7Vx+y+fgS0d -0M256ldXrBJJK28GAYmNZ779xvemy4DnTb3Np8K4N2anftc+Uvmu8Pp439n1AODU -zBqhbCelAPBKdWJ8RZy3tP8CgYEA0HhEKu5r7AIMMjEDcVACSl3e8Yy1vBVMAVpu -wbMFr5iSFQj/KcgxY94SC6oViqgESDRnAMKewM4C2aygKZVla/ph7OTyZRIfnIOZ -MOC5CZSnoJf8uSm3wII/GXbBRIScPrhGxBrTLRdai8UT2Y9g2l1TfO/sN3wolSSC -DYLTqgsCgYAc6H90mZfbhHUzeYl0V7Zke+3WEKKMj3JSlOSTqaOm9dPet/LEyw5h -dLjMqbFJ46W24RyNLHuZ4qBktj/00q0Q8TH0AxX2dhBgD0zY/eofqwJTViKIo2GI -ZdoCJJtsPUu5Ctc9jW8sjL2VWtkGD+cF/UoUefm/jURqh8CwDC5myQ== +MIIEpAIBAAKCAQEA1/bcWd24MFjCfqpEPdF6pDDafOqpHLBgbs1eD3TN+dDK/hMy +3poDUwtglyIJG0XLZd3xcgFKcWdr59TXlHdKptyseG9MdvRd5AbKtOSQwmkldNi6 +ljllVf/iQhd2FmU4QkaLaeP3vJKQj5f6zHJy1uKMt1f6XekC4vaUTgp+jZiQ/JpF +psZIs0Ared9SsvLSaezfUEZSZK8M0ialzBmm2wouGjecjqWcWX4VDxMih7gEaseE +lUnzzOe/Rl7AuQKj4J9Bud0c330GNRSg52hXh6ujvpLQpmx0Pu1+bklWpOtK7xM7 +5m3U9EouVB+skMcQ5lSBwR64+FLdUDXCJDzUqQIDAQABAoIBAQCqXFACjAaqDLqQ +lNv0L/Ug5HDWLX5t5SyM+3ABnVCGipA20QpF0xRgRA9T6UNpwirrFqNKHX6N6tmS +LbJJqbi33EgpURLum2IdaMYq4ErZMXycqgK2Ulx/9LSVElDS5dH1ZhS/2Vcp6Cwl +OexbrsSsglYio0IbJp5iT8U5ssmWA5rnIlOPy/M6qNu9IJf7y6MyLBmjS6+IHIaP +i8Pzv3XsDkiIvhc7avKg1sPrTFGPCTSLV2DQFYGjT/Yl6M2qKz5KOjwXsTr9dq+7 +xBT2SeaJq8lABiZIlL7yLrB/aPOqIdAItWsuBZ7uR2WZX+tKPV4/LicC8fr0mDVv +ECKodadJAoGBAPJ/6XsbPB/rZSSMVmp79IE9jZWTXq6UbpXyKhWhgsnYZQNdAsKd +76bDlhaEnDAuWqvbX2m0THcKb3Ug6CfwoFUYxPYAiGfBzxFCjYLZnHSrDmjLP8/a +bLrSQp0q8mjz37EBErPIfcCF103mohIi8aj9kltUsPHbYWBatW/bqkMDAoGBAOP8 +xMiDo5OynJy2CHDpy9LBAAaLY8xSDI5OgorhVbLkdTDOEb4xT8DR4sf9u3dHg7Ht +nuQu/WtdYAxRgAwJb83eAS+ZDuSj5UrotEtZ+KB6xjJ72WyTLSyy2MKhaEjHujE5 +um8fcvjPg1PIJrcMw7Sj4Fr3jPmPPF6gra5+XiPjAoGBANKugsDr1n682nC4ZFO7 +QaAPRDURhg8S8kjfzeRhH+oRUSFs63r+EDnIb1s89x19CFWLEAgcFtrNfCw83LXm +fsWv8V7w85GBdk8+jQeD3EQYYTp2awhAcnqVNj6qE1VPm6aCkicUJvzey0HpACHV +rjLtqvwiRmC6Ao3eAQgfxnrVAoGAEA+5qUqk1n3pzJyCYboG0vUn1E4znKxXGBtt +1OzlBbJUkzihRV3h+XqP2HkGoPOX0owj+n1Y+xxb7OI8/BwaFU3DlZ/Zzb/CIpHc +Scav3lZn6hyRh7WipBiBbszCNQZlFpyzhqqXhfHQlbFvEMxEaVB2ONJhyx8NKLl3 +IujiJvMCgYB9vdKvWVi+wzTV0EXJawdSlSvzpbLjSBcT+IrpWiyjShOvBXHEPpzm +5he2yoZxgcB/j9p0n9ux6rTy4Y2ExgW6yve+VtBd/pnzStR22Dfz9t+k7AmFyYVj +spe+FYfRrLkQsKxE0vAbXAVq52u82kU9jtT1wpQqo5ICfLI2v+dPGw== -----END RSA PRIVATE KEY----- diff --git a/services/federator/test/resources/unit/localhost-dot.pem b/services/federator/test/resources/unit/localhost-dot.pem index 9eba28b158f..5e656feded7 100644 --- a/services/federator/test/resources/unit/localhost-dot.pem +++ b/services/federator/test/resources/unit/localhost-dot.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDQjCCAiqgAwIBAgIUF3L/4L7MgKXyAs+xO0f0/zpfwSYwDQYJKoZIhvcNAQEL -BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjIwNzIyMDgwODAwWhcN -MjMwNzIyMDgwODAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA -vgZ8XblpKvf46pVOL0t9TFAYyXYylP65fFp/Uiim9+3YO27mPcv8CUMLRov08xAU -Upp8ufR0wqfEZ6Q32CwXhuNEX/JESbW4HX+wMLAcOacd117T/4iDQJCYSg/uFoVJ -hCeUVSiigbB2cD6F4XKnFw7xy9AKnVzLOSHo+NdtblNRkoy50JJBJ2hu4rrqtq+N -NE0Fky4vfrSQ+N0wQnAu3+UwZLulJhLhIlDiY5zcZLZ1Tw92/CpK7L7AKFReFAH+ -1lfhKDuOUi+XAerHycOSTYT05Qoye2ekqLwAb0n7EIIqCEIBmAk+7klhe51BfxfO -UHOmhTUPG6C5bKD1+q0ssQIDAQABo4GaMIGXMA4GA1UdDwEB/wQEAwIFoDAdBgNV +MIIDQjCCAiqgAwIBAgIUVSPGRv1KOXw3iQBP1qT56qQCDJgwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjQwODIxMDg0NjAwWhcN +MjUwODIxMDg0NjAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +1/bcWd24MFjCfqpEPdF6pDDafOqpHLBgbs1eD3TN+dDK/hMy3poDUwtglyIJG0XL +Zd3xcgFKcWdr59TXlHdKptyseG9MdvRd5AbKtOSQwmkldNi6ljllVf/iQhd2FmU4 +QkaLaeP3vJKQj5f6zHJy1uKMt1f6XekC4vaUTgp+jZiQ/JpFpsZIs0Ared9SsvLS +aezfUEZSZK8M0ialzBmm2wouGjecjqWcWX4VDxMih7gEaseElUnzzOe/Rl7AuQKj +4J9Bud0c330GNRSg52hXh6ujvpLQpmx0Pu1+bklWpOtK7xM75m3U9EouVB+skMcQ +5lSBwR64+FLdUDXCJDzUqQIDAQABo4GaMIGXMA4GA1UdDwEB/wQEAwIFoDAdBgNV HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4E -FgQUuLBJ1uWinp9T4WG27wVudIp7LAkwHwYDVR0jBBgwFoAUmUEefcwWEyqnjNBx -DNucQ7woHRwwGAYDVR0RAQH/BA4wDIIKbG9jYWxob3N0LjANBgkqhkiG9w0BAQsF -AAOCAQEABfH+r39LsPVSU5Kk7/6xk2HTthT1Fy/fPKy1elPtdakh7l6eI4op+lLT -Fwv1FPI0FzNpSYXjzCNfhuiO83+D0hcaDGT+syHrQwHO50P6mRP0pB5gWULfAvd1 -4FGMk5tEzQHpIy+QpLThJErVjmI0x1ufR6d38KLvH87AZE3Hx1k4j/qWFik/C9s8 -qfXJxntlyyDmPQsSgEYMqjffhhhWfKjhJX4AIj/akq7dnnmsqioXO1aXgBCCVprm -RRak6MY9Sb+31nFIZbXDOlCL1/d5IPfT8C7qd19aFt3vFPpI+83OldaskL3isga8 -citSwDCqZppVIFn2s4SM61bS2z6R0g== +FgQUtMGs+LCIf1q+ytJH9WaLt7c89RcwHwYDVR0jBBgwFoAUv4GMQ6kOgRxHoPPJ +oPWQGDxVuCMwGAYDVR0RAQH/BA4wDIIKbG9jYWxob3N0LjANBgkqhkiG9w0BAQsF +AAOCAQEAnprNAyy/Z2mDN4ZaXNVDjBe+T1is5fi9EsaR0CJnN6q61fwo3hBwi5KK +/cADYlvc/7AzlfP39L52oZ7eWKjeuy9BP6l3uZIfHLKsQoUY0SAtx6TBQT8ayiUe +DfBvezg6sKvDa1SPBev90rEwOwbS6xi+M+pRZgptgh2bMkGoUoE0/4rIjRDaimxQ +GdSwLeNsZNAP361fXIpmJKtOw68TRla0PvHffTBRwV85Y6BI6AuLWaFlfhkMYKxK +gm5mqhXutNGGxEGbf24+S4kFgtzYeiYnNidWGUN9LWgcNymdDgZYu8fApY/8geiv +XoSOfarydyovBeuCPyX1AAviW1/N5Q== -----END CERTIFICATE----- diff --git a/services/federator/test/resources/unit/localhost-key.pem b/services/federator/test/resources/unit/localhost-key.pem index 5630060c32e..6e869952931 100644 --- a/services/federator/test/resources/unit/localhost-key.pem +++ b/services/federator/test/resources/unit/localhost-key.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAuu8FGRe6JZ5k2avQv1DZKLuYk6wPQHXtI23NSSHCEo5NH+8G -AlkzpZNmH25jEWkgECd3See9aebfTZByG1BWJkfj+f/0X3FewFdyGnxy6pZvjRbl -LNhBE/k545w7LOsDJ/AJvYe31j8WEBehzfxRxmC2mWLMK4HKxP3LM0q4zPKW5Ax9 -WFazzJnR/mu6q6FdxZumzMwX/0NocFTLJXN90XzkrxPXhHjZJ6FkiWuzdaOaOUu4 -cWQar4Ew31mjEst8JKTpX9kOjzOZYlBd2Ez7HFV1f5H5TUthV+0mNSAJhHc+b0k/ -11EF3fgg0lb1+RV8938tVKxKPs+aLcMu6YKQFQIDAQABAoIBAQCPf46VTZ6K5EWc -xwVO5/xcBW5B5jIrFJu+t7p/6lc1sWFJI06knN9FupoJhM6t/dosLG+pHylLU3yV -6U1+5DPN3SAHuNFaNwg6dKZV6LS6mlL1pt60hyml8IrczACtIZdhoCWKBdY8tF72 -aX2/R3Nq6rEhnDMJLvB+OikzraehQ0MX8LASdnO4/UO8HR0L2QZr8GqFQXJOW/S5 -7/3/jJmjtlJU0a2jNxL8dDK+w2nLwxMITMw7IS8yihoFmDBYkXPlfMOnfvV1kecJ -QIoYHbbAtqaGI1p81sipcuSq7BoBnhyl2KLRV6E2yun6u5x4wVdADsD+LWDGzKrE -m3DroWpBAoGBAMgPmFo/J53qQae+F0W1xuS12GPluNB+TxIf6TiI/mgFUa/ykxj1 -NXWPUWnZ5qq0keOd1wKPChf5NYwJXfEe5l0VXLjcun9xgK+D4UdZYMM0pJi0muOT -vydIaAqeXrrFSdOUXYnIF2O1lx9bOR/xM9IMS6UY7UZU03G2VcCtZ0OFAoGBAO8z -xgbxUUov6MXfYOR55/TI6me9hwaYWvkL7gIFs2tAyyOVBVG3TSsQRN6y4zHsvWRW -td3gIDP15ASK/K1yC9z65NW0pKSECbs59sHRpfdrtMtycLQDb+/eS6mZ5VrzWDsr -qMxkq3oOywRGpnE60u+u9Tj6wiPQMFk1V2OvlFdRAoGAOardJK5tsgRTdpHBzZNP -SJ/uRyVxt4+hJT1TkbtTchKOcGRA6IHOLhvowNVRu0UfhHf8AT3QEAcC22Hb1WQt -zQkaWCMwEwpZqL4gEtv9m2cyRt5Qg2cUQ7OIYf8ymS3DURzENbIao+A1NpGqDr4N -TO/EYkukIZDT/kQrxcV2La0CgYEAt5e4TUnYx6Uf9wetOY+rjgDLkRYx1ckIQhB6 -/Ehd3lsbz1Kog4C6FOxmv7rzkDURZDr9Wa+VZ+w5t5bpu0JGgrR7AN+mYrMJOQ+T -Kk38IXwkhuZuRGxC9QtcbW82T2lo9flblI1L4+IIxl5nj47Dqb1ScApfCdfX1BCR -42w24hECgYAYsxps9FqDeoZAjVbQMlzKX8uCB6w+XE6vF8kDdpuEMMaBFOGOfr4t -ccyLwIry/g9jXZDx9FQVjd93B3HQl/VcvAkTpCEkjOdMdq16aZncjhwThRjEGdDB -VG46d5sTAVeY2OdEXO16aATvGcxv0yrEeA0GdwSa9B3tX9b4kU27qA== +MIIEpAIBAAKCAQEA+gITPHZQnXllPO2KYNRbHF961JKELjHIv148M8R6bdFlFtGz +Vd/4KpnRsIbruQNQEIreiF+rD7tWQuN3gWhrxn1qbfHvuvX1QcXzyF2OoW8atIU1 +c5fNeLGQFGIs2a0uyrmBg6o6Vet6LNGI3OXIXmGfQC5PkH6uBhZmEnugyRDzOOmV +g75fqIPZw1SQdlWbpVS+7poXc06v+QHfcF0NqUkZBeUnpw83Aj6HVFgAhuHEy5eJ +fXj8zRmDCqqjItE2D2HMVACxZgDEsSe6wDl+WvW1QQU9Kmm4fTOx6FOXtJjjTtDd +iaP/F/BnqJoVqkhvITGziwqjLJ2NNC0K7RoMbwIDAQABAoIBADRXpOFewAgIN3rn +HLajHyQ0lUnWFxh40dfHCgGonB1L3sdFRi+vgYyhwbYcuVN17xhXirmwleboSOoe +J5IPY6kd7t9v7MoO3rdTk3Oaqtb/pO6wiP1XhexD+K9b0poMWSSWbBg91pLQhzbE +88uTzDx+YdIVIBFXhGW/4MTz3zjZCrQPDZEZdBL7A3kElY8yPhY1WYbdljwTYPF8 +uevUSVn5WVrQnlt7SS9EsV87sIqBum/kMUZarNI5AcmXHT5IxatG3dHE8aG3Ed1L +e3AwC4ITV7Vt/hA2H+bVx5SsPT3PZHtV2ydsQddOINAPyB3WCbwWZpi3wSz5LQd+ +SGJVvUkCgYEA/astbPqcdm2an1KJtPmINFd0ceUQbAJ6qarfwopddFZPURfsDz1a +yYm7OCj86adX3lrV/6yxp4WtcFzh+9f+xtA0sOzl8HDqdP6mE7hnUYX17gRnBw1l +t0l/2PsOtpYfKGzWMf80ib0WRKW5KA3B9Qp3sft+qzyVLTkwajQfwmMCgYEA/E5J +B5ovYOj787WU6F2EcoJLstjuyqk8tjE4YeI1Axa8cpF89q2f927QNCLPPJhWJSyW +mrC+neVhSemjlYXaqdSc8EEyYqZgaIFICwD29G2VTcMITrzLlQuFKJFA42Tzq5Hp +1PSNP+P8S+eu+oeR7Acp5rXYF92hPGzinxtDZYUCgYEAyDofpw/CBLDLBctOqyzz +1+zYGzal2buzOs2HxbUVw0iFXws052qUiNRQlSm1SHEwqHCmziNwLz0TA8gtEG8T +ybZ6gNTdQwa96g8+4/4Af5bv4ipTcHuguCYp5gl1OaYRfgU6pUg+HiLEuvbcycLi +QBs9E53iBCPT1Fh54Lq3/uUCgYBvzXz9GmzeQ7/KCe/XXAFiAKzsrsZ6Fa5qibsT +XPriyINvPVsjsGKPcZJfWAF/N34M8Qo3uBRvwYJwD1FG4862rRlyOWHLZzCXfppf +Delg/OJJWCBpS63m+PjjtiIL4eM8Zuc4T7n70totBJh8OfEGp1IBAxmj0bkuHo35 +tUoTRQKBgQD0v2hVS+dIVhioth2KGlHKqSLKYXPEtwr8U/NMDv9kIUkG0Rj6tiLY +Lyx0z9y8uLrXwJMz80QmBX3buY7rURvlFpD5I0/KBdTJdHRxbvyvw2Ph1hIVdGNM +GCHpXxps+5pKcqnhC47MacQHZDSA09MLIRTED6hA6s1Da1r4uiSbNg== -----END RSA PRIVATE KEY----- diff --git a/services/federator/test/resources/unit/localhost.example.com-key.pem b/services/federator/test/resources/unit/localhost.example.com-key.pem index fafd3084540..897d4630535 100644 --- a/services/federator/test/resources/unit/localhost.example.com-key.pem +++ b/services/federator/test/resources/unit/localhost.example.com-key.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA0zOmcMLyUOSlZaEGvktbhDzr4EE0yW+EO2d7OgFxZNtGnkbm -raRjaLY+sXvkyDt7RZSoN/V1CtvJZfUioQh2h0xzPTTG1XrQaZJCUjd6mroXZlOD -XQD6TihS58dGxfATyDXM4UtIX13JvcwhHHTaV5A7InAAMu3mIK0bPwDL4Z7rj8rP -q7Ykw9aWl+QB/Rg47d9KR25MRmwG2SfrYkPPmxfnJhqeBqyptkv4ahgdsLhuVF4d -np3XBUBi9+HVmcaqhUc3FMER5NoZHeUB+V+AnfoSSSw8mYgtD/QxWakzC5ILCkNa -nqTPfFzcqJGLMsOL+6K5LMT+AwICwz7YlWriQwIDAQABAoIBAQDNJk+XGpXbo61R -QY/WSkaz9aU9KLmIrQzp0wOsfXhS+nfnCIHLy3FQZi4RooHBSZ3dIaAg8wlkqcdA -hVPEzf6sP8N0gY5eETTeR1aqm/84ymguWhKwxWFdh+e4Aiap4CCnCjNf6At9rxFm -jiDfjlYEVjJKqjZXQiSWOu/LTA++sCYP2WBRECVbg9/PQYRKjcqqAPGrXj6rQGZg -GSU2McvuWeqbavoSCJozEHEXWLqxwnYiVKSA5ztNbUJBmaw6RDR32W61AA+9IAtf -Q5TiPNS8D+UIa7RV88ERR9/0oFuiqgA9o0vQOun/DO+GEPh20oQFLvqpaICbSAy7 -ffsM/tGpAoGBANp/8KqzkZfpL5MSb1d799L68N3CxphxcMZTrK5LIexhgZyls8H5 -WYs5/WLN/3+UpbP+W7IeG8bJSZYrmwZ3KTetYOt+Qa/UajRxBHmbHV3Q3P18N3W+ -Ka4W7qJa28ND9JRVpSGsgSBaVRVIKR/B6bH5vl12PUybh9sv3/6X4vp1AoGBAPdz -EB+ULijkDr26mwlKBUbgT0gBoymBRGMfsF9cDjnxQRjmYwiCRepyFPkLXaEQPdJo -uNg2BS5jZVqz3egpKgjj/xbSLS6gGXJZUvc/aT+aZZhhY1jNzOuWWisfBVl5CbJe -bsWVyM6XutKOX/TbmpZO0WG7RUl/3EcAPV73iyLXAoGAE8s9USl9Sga84ZTs8z7u -v3UuNti7RvoX3k+cOBnkU9ateDRmqW7eVseFFdtVhwg/TqP/SI6Ds6lueiUvIRHQ -cRPK0OqaJsSWbnPClQWhTmtqaahEGe2FNxkquxiCChlw4bM0h21qMTUduhTUbOUT -N6VJQoxAl10LnKakoxq9XDUCgYBPRmmv9EUljIq8dgAdZb1zC4Hay8t+DI/gQdK7 -ej6EiaoVVBn+K95CUfIuJ1oDs7RaqHovqn7WcbmS6XT6X9W7q2+Z3BPlkB0W6U8P -Lx2E5u4Nd4XgeW5hO3X/wpxwIbrdjitm2anPpgSQWFSdmY1ZAj9KVDjKZ1Am3wjK -V/wXWQKBgQDKJdvEmaQnBSSX1NeN5155Ztj6BGRrh04VSwM10Xw6J9VhPJD4jLkc -4TylS5qb8I9di4alMa78e1O/j7mLprDsFy/uB7idQM6B22RrBrvR1iNw/D/UDKLV -YW40zxb35takn+MiNBs+orpbJCPME35ojRdj6xgt/JG0FiMwzwthlA== +MIIEpAIBAAKCAQEAzAqzoVkI3AgBlYEsJ+cOl49hHKvFt+E7cKt24ofRJP0eLyyW +EaR+tLPFs7jBDMn/5nntd2Y2q43MIGZozB0ncpcvBvSMhFI+v+xpacAXyLREks1T +y7lgbTs3tnZJ7FPFH0waYamLIIHgdp1vlaGlOH6TV0E0xibTaLA0P/ECcvXODT+M +IOczIOUXf/wpcEmY4kTq5I2oktnlYHC+n43cyfb6NUA9sOUhRREJtcmmQyZFmkpP +xH8tA4Pg0wwDwHfygVbYIY/xQsMcO1lQHRdNC8B7EK9ms9PKKmffR24Q+ce7b0Ua +6ME9jisSbnFjcekic7GH3yWd0BBUjeHGMgktjQIDAQABAoIBAF53Xe8Hj6h/NGyJ +X24h9YmJ4kYp6OJXLvPdDq+WfegIIF9q3xfP9rGmwZORqB9JrmaAcMbk1c7bWXRu +tXo6zTaqdCVeN2hTw6WLyMojG1/axhzJX1BkxRYNYp/haBw4NH1m+JfarQUh3FBO +V4kJS7s6LvEoyUwsUZiYa7hm1uFtxCGy9D8vgyH09EgKGrfa5UxCNGKMg4O/Iqmx +Sjq2Msd5sjQoW0yUrfkY/6M0cw7PlDphQRaaNZpJD20MDwru4AzW/z3piseDpBgY +WHbS59r36243j2aY1K1JusDorH+t1cs5+sbm8cQlMckzXjWRtakzg7edjrNETCWW +hCyAKqkCgYEA8/pSwJmm3loOQC3UmreIy+yT76yE1c7LPZ7bhRvSUNh6Hc+JQ/vK +hm0YQJriWAK6tWKzBYd/UIUvuCoR+YlLax6l7mPi4gKXUdY+/8beuKVlsqsxquZ+ +cMCNkuTVGMwk8dTKXTXGA9mr4kooNhD9vEGD3VuXJoIInZ51ofTIXD8CgYEA1hia +OsM1LLAXiP+4Zm0b4NyWKaI71XJYrezZWMytNTH+gkXeQ2xgOTpKSEBQKQzQLRBp +yNSapcQU3aLSaxUfTteWXnyNLZ2RFx/lx4bon59OqedHp5orJxw17Ov+L74LF43C +FeTHnj/0bmeSITzxt3jJk6xl/7FcWBpHDcmx8zMCgYB1//8gGfCEIEg+MCxgvB3U +i1Ktm+IPStovrnJ7uY4J/flqC6NXFyPHymHtdTu912wYKGlvZi5kclY3G1ngN9Ab +OhKE7xifuSMYuKd1q+iyo4RBWt1Fy/8hZ2/RuQ171diUghdx530jBZdFdhpms+cU +sxck65R6Um/6U9aPA0YYnwKBgQCEERsago5Lqbhq5yvt6wJWfFwZBJ2aNnYjm8Yb +M+7osJVJ8SWBO9pDkwR9e0a3e9Ly6XwHybY36TPh4G2iZp4weWallHlWGSuGM0QA +B+V5icDxp9yYs85Iyuss4gqjkryu4+BEtyK4KMV0UWlgHYMR5W2bLa6hwtuu8U7B +X5jo5wKBgQCU8BvbnZtu+Y/YOUVKOeQoWeajeQ8AeRkinD1eSZPQJgX1b42ndnRc +Aa+u8bxVXS4TFceqKQGbOxWjDKFhwHx4K06eVD3zbh6ICLQilwleQgS5QH2uZN6f +xQ1dYF4PK5ZL6KDF9MIyBHmNBUMglTIdiWyxeaNCcc0qJWj+8wJvpQ== -----END RSA PRIVATE KEY----- diff --git a/services/federator/test/resources/unit/localhost.example.com.pem b/services/federator/test/resources/unit/localhost.example.com.pem index 25967b394e9..92122ade2b2 100644 --- a/services/federator/test/resources/unit/localhost.example.com.pem +++ b/services/federator/test/resources/unit/localhost.example.com.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDTTCCAjWgAwIBAgIUWaYwdJg+8H7ktXbcpS9WKDDTrIAwDQYJKoZIhvcNAQEL -BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjIwNzIyMDgwODAwWhcN -MjMwNzIyMDgwODAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA -0zOmcMLyUOSlZaEGvktbhDzr4EE0yW+EO2d7OgFxZNtGnkbmraRjaLY+sXvkyDt7 -RZSoN/V1CtvJZfUioQh2h0xzPTTG1XrQaZJCUjd6mroXZlODXQD6TihS58dGxfAT -yDXM4UtIX13JvcwhHHTaV5A7InAAMu3mIK0bPwDL4Z7rj8rPq7Ykw9aWl+QB/Rg4 -7d9KR25MRmwG2SfrYkPPmxfnJhqeBqyptkv4ahgdsLhuVF4dnp3XBUBi9+HVmcaq -hUc3FMER5NoZHeUB+V+AnfoSSSw8mYgtD/QxWakzC5ILCkNanqTPfFzcqJGLMsOL -+6K5LMT+AwICwz7YlWriQwIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIFoDAdBgNV +MIIDTTCCAjWgAwIBAgIUAgPuw8grsozNwLmI2VS10QxJE1QwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjQwODIxMDg0NjAwWhcN +MjUwODIxMDg0NjAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +zAqzoVkI3AgBlYEsJ+cOl49hHKvFt+E7cKt24ofRJP0eLyyWEaR+tLPFs7jBDMn/ +5nntd2Y2q43MIGZozB0ncpcvBvSMhFI+v+xpacAXyLREks1Ty7lgbTs3tnZJ7FPF +H0waYamLIIHgdp1vlaGlOH6TV0E0xibTaLA0P/ECcvXODT+MIOczIOUXf/wpcEmY +4kTq5I2oktnlYHC+n43cyfb6NUA9sOUhRREJtcmmQyZFmkpPxH8tA4Pg0wwDwHfy +gVbYIY/xQsMcO1lQHRdNC8B7EK9ms9PKKmffR24Q+ce7b0Ua6ME9jisSbnFjceki +c7GH3yWd0BBUjeHGMgktjQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIFoDAdBgNV HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4E -FgQUXhO8Ny6KBKku8FMw6nTUtUganuMwHwYDVR0jBBgwFoAUmUEefcwWEyqnjNBx -DNucQ7woHRwwIwYDVR0RAQH/BBkwF4IVbG9jYWxob3N0LmV4YW1wbGUuY29tMA0G -CSqGSIb3DQEBCwUAA4IBAQALzyseFMpr4FOkkn1WzMAJSmePw3/AIHRT6tmpPzTI -qh6SggI/3lvvXCVSG4ghDPVL7YKcdx3aZt5IB0ZLuAt3opjX9ZazGYq1MZimbcXO -L6ooRqPM0I8oca0Sy9WM7u2kOWeVUC79qtDxtT7HdO8sTkOL/Ln2z5CqiJW2fG6T -RtVser240irqH/2rtRb8MFkvaGSX8xzvxgzQFH5kvTDu3Wa/M6aWko2O3efjoJF6 -dFa8w7Q6E656brztCuwUq7UUI9zmEZCcKUKbNrJYquhYGHRvDNM4jTc6yD3JTvpq -RrYvK/rRpb5QTz8Yi8wtu8AXeGcL/gWBIOqN6dl1Lo+A +FgQU0FeAlO0zdMqN+x1cMV6ib4Mt2xAwHwYDVR0jBBgwFoAUv4GMQ6kOgRxHoPPJ +oPWQGDxVuCMwIwYDVR0RAQH/BBkwF4IVbG9jYWxob3N0LmV4YW1wbGUuY29tMA0G +CSqGSIb3DQEBCwUAA4IBAQAIfXkrqb0KJaUdo6o2TIZMhDj5JZGgDeUigq1Z+rT0 +bZ8CorWKX3mvYWySuosb3+SPkSd+w6OxIiInQl1JIlKwzulLSY3U2GPu+uZY8F+I +q/1tXu4F5P/7A7tvfizOdVt7YBUwp2Cj+NIggiufi523CzEy8Ki12bQD3C9XxQhC +xe+9ugJokMGdflndMKxMd+S3DDGuqWRaGWavZkj6AFMG52nlYCSRWv2m3QV9wo4m +B1GLAVIiRMcOe7BkHw9Nkh6ZhGwP9SZTr5aNlnzvvgoKVH+AjeyxgMpQ562ajdPf +gsfez8zYthPQcweleBsxms1EdqZfL9HQwa1KsH8yvrtt -----END CERTIFICATE----- diff --git a/services/federator/test/resources/unit/localhost.pem b/services/federator/test/resources/unit/localhost.pem index 8927bb2a05f..950a2572e3c 100644 --- a/services/federator/test/resources/unit/localhost.pem +++ b/services/federator/test/resources/unit/localhost.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDQTCCAimgAwIBAgIUBd2V4fE6PJFOkVOjw+mpIdwZ5JwwDQYJKoZIhvcNAQEL -BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjIwNzIyMDgwODAwWhcN -MjMwNzIyMDgwODAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA -uu8FGRe6JZ5k2avQv1DZKLuYk6wPQHXtI23NSSHCEo5NH+8GAlkzpZNmH25jEWkg -ECd3See9aebfTZByG1BWJkfj+f/0X3FewFdyGnxy6pZvjRblLNhBE/k545w7LOsD -J/AJvYe31j8WEBehzfxRxmC2mWLMK4HKxP3LM0q4zPKW5Ax9WFazzJnR/mu6q6Fd -xZumzMwX/0NocFTLJXN90XzkrxPXhHjZJ6FkiWuzdaOaOUu4cWQar4Ew31mjEst8 -JKTpX9kOjzOZYlBd2Ez7HFV1f5H5TUthV+0mNSAJhHc+b0k/11EF3fgg0lb1+RV8 -938tVKxKPs+aLcMu6YKQFQIDAQABo4GZMIGWMA4GA1UdDwEB/wQEAwIFoDAdBgNV +MIIDQTCCAimgAwIBAgIUDSbc1r9KTiCOa+lKj322ZLi3PGkwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjQwODIxMDg0NjAwWhcN +MjUwODIxMDg0NjAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA ++gITPHZQnXllPO2KYNRbHF961JKELjHIv148M8R6bdFlFtGzVd/4KpnRsIbruQNQ +EIreiF+rD7tWQuN3gWhrxn1qbfHvuvX1QcXzyF2OoW8atIU1c5fNeLGQFGIs2a0u +yrmBg6o6Vet6LNGI3OXIXmGfQC5PkH6uBhZmEnugyRDzOOmVg75fqIPZw1SQdlWb +pVS+7poXc06v+QHfcF0NqUkZBeUnpw83Aj6HVFgAhuHEy5eJfXj8zRmDCqqjItE2 +D2HMVACxZgDEsSe6wDl+WvW1QQU9Kmm4fTOx6FOXtJjjTtDdiaP/F/BnqJoVqkhv +ITGziwqjLJ2NNC0K7RoMbwIDAQABo4GZMIGWMA4GA1UdDwEB/wQEAwIFoDAdBgNV HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4E -FgQUctS7mVDQHyRkH0rrqFeASEs5SFEwHwYDVR0jBBgwFoAUmUEefcwWEyqnjNBx -DNucQ7woHRwwFwYDVR0RAQH/BA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUA -A4IBAQBstDMgzQKCqUnikh8FwZu8Sq3ddNDSsT3bD3nK04JVgFe28BNwxGxkKhBw -mp5WvdtGJJPMskEozoqlqPuQ2gPIzZa8yc6jpKgDrPQIaUkQHB+044ZnkGkE1fht -RDtBKZG/nYmqXVQhHgsbMN3HGVEjbfpMk26gCQnvYO8iwXt+XgJKF8hpGMWU7+Rh -1NFHjSYair07VJIpbcB5kVeOgjDF0IJFJFvZe2142BiG7D5vnJRraTPvcH1X4htN -3mOQsV64tFCMGa33DfAj6n0GrOmHAE/eJFVvIwFrYbGQWY0pdraNTa18B9zJu3zc -vlEPMbfftb3oB38RWN6MVGpqBAfp +FgQUCdxcQDi3Q4YzwAdpbStqA3FZAy4wHwYDVR0jBBgwFoAUv4GMQ6kOgRxHoPPJ +oPWQGDxVuCMwFwYDVR0RAQH/BA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUA +A4IBAQCoqmWAPm0WZVbppL57uRY5J04W++0zuoHQzFcVlvuycTKM30AWeW+kx9Il +Cac99zqL11+GG7EDzkCwnYyWsrBOzWhfbWBRbk9VwoVXOeuWqLEv8lGg7ainL8L/ +9M/XpS9HTcljZH5X47laukb+G/WazPZ66WEVvOjmXu5G2sIpxKnNAPzlyO0hHTbD +tOYpVGC4LFRw2ptVsw2zE5+5E0PiKbCDNhnPBJXxAZ1P9BFm6U3HJxBGGz0zUEYI +EFRHK7pIzMOU5uQMf8C8YtEwnBWLQeaTsz4QkAY1MxVX9vEYXFTx+8MbPfkV5DVW +4OMKS6zgGqulkuEPfqDW9f8gUROc -----END CERTIFICATE----- diff --git a/services/federator/test/resources/unit/multidomain-federator.example.com-key.pem b/services/federator/test/resources/unit/multidomain-federator.example.com-key.pem index a87cc702421..4bd8b8fa2d4 100644 --- a/services/federator/test/resources/unit/multidomain-federator.example.com-key.pem +++ b/services/federator/test/resources/unit/multidomain-federator.example.com-key.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAvNi/pSMmtt82ImrP2ZtkSA00p0MCyUZoC0nyuWEJpOpbjPLE -5z5wJNPefH45huYgbw8e9eYp65iWYEnQACPtS+4a/mZ4YOHRRUFtRzoMvlZBK8pm -XY4+5Tc430oBOymq70LHCsOn54Vx48wb2aR5AZmJKcXaqFd0vbQmJT5tlHOgBOfB -l0124EZq4QWdKNG4iDjZGhvRdUTsBqeWV0XjqfnV5ek3jtQzscI30eG82LeXB7VN -ygMv91SuCXLH6VWd5sZ2fVhh24ciP4WPl5SsVDYNPD6nKa2eYaAJYtzBux7oKj6h -uXQ0NALbXpeyRzH23OizqoiUrEvkISnhWnPogQIDAQABAoIBAGjrV1ZtOCYjz1TB -2SbCMa3iQF2pWlPvHQEgnY0m/4+zcRfXDVSYmP+tApBSJK3xDxYE5aOis6mkRe/L -MpfRXhZwfGjZD4psC7OZjRgkhU3+aAjnU4Yo8IKy8pMD77kqBkEV7bXqTE/SERuJ -m/OIcH5WCiG+PiSHKmH3Q7Yvf/wYaE0Oor0OM4SGAoh7zMyF3UtiCQOeWTlkHtwk -8n8Y7hDZPwDiBlP0pSPI84frBzoHK9xSA0OMkAd6dkwhMY57n/n4tokkrgPa6Bve -pFcTlM/3m3lwHDj7DEBC+2yBzPQ1q1dv6h+xbQXUF7nnmstQ8Ws5nvLibMbtKybU -BfZIcK0CgYEA7RK0EVc+x7A1aa2Ufmtt36LQbYlCnVZlRUBOFGlWCAQfKTz9UsAN -LXAzK9I4FOvD5X+p9IHzZbN5gzMIVQ7Z/m0UrsRPlRnVT+DR3ZqLGeu3D6Jdd6PJ -jFX9hnWnmJGR1TqLa196xwtIxQbMpXiPjwFy96fdtsd3p+xY/MEWgPcCgYEAy+xl -Fq9K8Q/Iu/jH/b3mC7E1Zs7xOqWlOv15CU7Y2hh5z8ORcpIbxga89L+kmYpDpFha -4nMDqjRCHVAmwgXj2hnzXDx9D4u9kEKvj0Wo1dgblBT26AL9IHTdITj6O2hDBXGC -OXgIynFIzW0Uidu4fAGofvGqC+pQQGzCWP4V/EcCgYA3CtCj9g2tb0v3bUW81FR+ -R978j5HX1edNXNDEDHDdC5fwiyiFvfFRS2uWEQjUUqj479sV2dYZFsJvo7mbhgc2 -+zyCFzIi/Ax6r0gKm4cQTZoDFz98N5rj6lMilM6ErceeKimUlGZ9MDGFDT8WbBdo -rH104pSni0hMxKMki/AdoQKBgB3yIQYrx72Oq9OuvO+uK1IcO9NVIGeUW6dGAbg0 -M/QTFBBGj91bR2jVJHpsiidh/nzr3KzUZv1fnzXex1JGuycUGIC7AUJ/Kt88a7uG -Zzy/94zPZ1K63aEeiDqQu25t6SmreYwm7GOOLzq7ggCcm4LaW3wI0Qfe6NoNHp0i -8ueRAoGAeNecV2ZljXzS52X3w8WWgQ8R4leUIIlRoxTNk3dk0/Awo667IhRmVUbH -asfJbT5wmcihnJXO7cr/5JQtBtFtw4trHezBpmw9rq8ZecuxYHb0NA+gEWfzxyaH -I084S0Lae5s5V0WvJRtvBmBqHmAqNu5AWNm/brJNmw60aDHDzdU= +MIIEpQIBAAKCAQEA12bnG9nQR+OL35ojChk1hrHAyrX3xR3tHmj3zfz0B8auIBwF +1tJm7Opz3Y1W7uujmGnoU2u0k1Q/mtrhz5hew7vSTffjxqqbxx9Ibc92a69SC3o9 +XX9Tec1a4JeFyj/sXx325pF0PY9Cne+/OCtPx7h6fetjxpFZ1e+QRTp7GEIDqA7J +Sig0gVne9IIJQpoey2awsU+YSDKXbh34ZRUvE3ecP4W56zJZsp3i1GF1rQlab8Li +Fm9s8uVlVv/4eoswpWweveivl9WB+VVRO5j4dk+okkdjuY2Tdu91auJFKFNKTxdd +tJytE2EH++wKhkBYmpzeC3qQw1dZm0mfzOTKGQIDAQABAoIBAQDMlCtDxGeRH4il +YhuNZ8vylbhpztH4ISgoDcHtniWXjRer33GcSlD/Ct3oumiqmprSEyRYtp7Wntma +FfEJ3cmDVUu0SY/IDBnP0OJViQkL1YOy6vKFbny13lhFnyOup2+0Fx18dwFTxlCa ++C1BB8HKCmgsV/h3i20rR1Bar+Rhb/NBrCceWDoVRvHMZyD1nwmSH7/99gbdb/TI +qO6MASCrRZU4QDNaYsL/vOpzcIrW1yvy2g0JuxE8uYJFwnOKwFOrnx63Dtccmpex +X9lPB/PwXWCEKjRQYlYy3jxTA53TY2r3bk8ZGA7s2vn0U3DFjsLCk1NI/CMH5z6y +UDvqbz4RAoGBAOtb2jBDKeXpejFIFMMPD48lXk68S9iYiMEJlheANZa69q8yro0d +cj8E4MLqZ3cYLliBVJVwDDKR1moOVLHDJqmMwUtXFvDWfTyljXE2lXdPB6Xv9m/7 +jozpgsBF1C+AuwkKSluY5H5Uawz4cPUs3JVd0Bn7j4T7zE5yqBwaqDczAoGBAOpK +/ZCLacxwrqxtCS2rUuSZRwmKUbEpbMsv0EBbieFwBRPzPIAvZapOMIFz6LpC61lH +p6p05sLYH1Jp1QIPTzaVX67FmxWvyL9pwv6TDLzrhkI6kAZWifrVh35n5bFnbeIJ +UuVJtF2lJAyelCaaQmoU+pu0pfM73IysjxieoUmDAoGBAJDnGz1NjJUlmvqOc2Ho +et7Z1edJ3LR8rO2UVlkfsV6cu1YAMSFmeLk96pd2s77KH3aUIZxjwM69pTHkotZS +3RHUPAmDk/cxRye4kY2bWoh7Gq1aQPKPASPWfI6eL1YTvpOBR2h/iGYS2VnB9+Gq +/h/kA0SL8b+hOxctVjz/WIOrAoGBANWKS9l2d5NSEKIvLvJk+ERKP6i+XP6v1uzK +Q3Ck/eJvCvHH/BiJGoxCf4s9bZx2abMR0AtYSQrFmKawtugNtBD3zCGrEVKOyNVN +O+BzfmSh9dhfL+3W6iOogrn/UCaFMm2WOeupZa8EWPr3fehBKM8vF8rat3Yd1UKR +9EpoKnCpAoGAZLnv9bbVZZmDAvxhgykJs5KHzSCa8mYtjwPNi1aR5fiJmcm9GA9R +9JjFMvKzONY1EHcRmUgdq2oOjdG5LBoW/M1FKXfczah5ab95eUBqOO7hawVQ7iH9 +lk+JY2kOrqmBggOUl+yaho5zwvZf2XYE2wWItW57kwkYWwIFIDxsGJo= -----END RSA PRIVATE KEY----- diff --git a/services/federator/test/resources/unit/multidomain-federator.example.com.pem b/services/federator/test/resources/unit/multidomain-federator.example.com.pem index b91600d7487..ddfe8399865 100644 --- a/services/federator/test/resources/unit/multidomain-federator.example.com.pem +++ b/services/federator/test/resources/unit/multidomain-federator.example.com.pem @@ -1,22 +1,22 @@ -----BEGIN CERTIFICATE----- -MIIDqzCCApOgAwIBAgIUW3cvth/26V7+8RUmf5QoL6oeHrIwDQYJKoZIhvcNAQEL -BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjIwNzIyMDgwOTAwWhcN -MjMwNzIyMDgwOTAwWjAdMRswGQYDVQQDExJ3ZWJhcHAuZXhhbXBsZS5jb20wggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC82L+lIya23zYias/Zm2RIDTSn -QwLJRmgLSfK5YQmk6luM8sTnPnAk0958fjmG5iBvDx715inrmJZgSdAAI+1L7hr+ -Znhg4dFFQW1HOgy+VkErymZdjj7lNzjfSgE7KarvQscKw6fnhXHjzBvZpHkBmYkp -xdqoV3S9tCYlPm2Uc6AE58GXTXbgRmrhBZ0o0biIONkaG9F1ROwGp5ZXReOp+dXl -6TeO1DOxwjfR4bzYt5cHtU3KAy/3VK4JcsfpVZ3mxnZ9WGHbhyI/hY+XlKxUNg08 -PqcprZ5hoAli3MG7HugqPqG5dDQ0Attel7JHMfbc6LOqiJSsS+QhKeFac+iBAgMB +MIIDqzCCApOgAwIBAgIUVPtdqBD1xMY9qGmETqbWnH9XwbswDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjQwODIxMDg0NjAwWhcN +MjUwODIxMDg0NjAwWjAdMRswGQYDVQQDExJ3ZWJhcHAuZXhhbXBsZS5jb20wggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDXZucb2dBH44vfmiMKGTWGscDK +tffFHe0eaPfN/PQHxq4gHAXW0mbs6nPdjVbu66OYaehTa7STVD+a2uHPmF7Du9JN +9+PGqpvHH0htz3Zrr1ILej1df1N5zVrgl4XKP+xfHfbmkXQ9j0Kd7784K0/HuHp9 +62PGkVnV75BFOnsYQgOoDslKKDSBWd70gglCmh7LZrCxT5hIMpduHfhlFS8Td5w/ +hbnrMlmyneLUYXWtCVpvwuIWb2zy5WVW//h6izClbB696K+X1YH5VVE7mPh2T6iS +R2O5jZN273Vq4kUoU0pPF120nK0TYQf77AqGQFianN4LepDDV1mbSZ/M5MoZAgMB AAGjgeYwgeMwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggr -BgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBQyrh8V2XXTksoNCROdYqeC -oVqIHTAfBgNVHSMEGDAWgBSZQR59zBYTKqeM0HEM25xDvCgdHDBkBgNVHREEXTBb +BgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBQXKSV/CrluJgjjYxj0mcys +jOZvhjAfBgNVHSMEGDAWgBS/gYxDqQ6BHEeg88mg9ZAYPFW4IzBkBgNVHREEXTBb ghVzb21ldGhpbmcuZXhhbXBsZS5jb22CGXNvbWV0aGluZ2Vsc2UuZXhhbXBsZS5j b22CFWZlZGVyYXRvci5leGFtcGxlLmNvbYIQbW9yZS5leGFtcGxlLmNvbTANBgkq -hkiG9w0BAQsFAAOCAQEAuZVe1Aq/EJT8LAnPbcb12IWagdkdjlRNDzficWJP3qzc -gOwLECi7TIKYpWXCWzXWS8ZAu5HL/pX6094ZIVXRfQgBFmodZREjo+UG0XpS8cT7 -ZRh8wSuJgl3eDfMissCxzJPYn9RkLvqcpUQSwlvoTBaPrtNBV+aZYJE0U675VY+1 -NoYsJK9ORz4hEvNObcaZYyhbFUWLixcYrSY7iDdiL3bV+o60ZrlKyRF3Nj5mjDBk -mFX8STvLPi0QM8lJ4O7DvNS/jcn6u6dfONy/IVp+vPtXdKGrS9e38BLubLT92E7y -6lZNMOo2oPiv5lMJSivSGfdkUIXia/O6rEAtXvJuXg== +hkiG9w0BAQsFAAOCAQEAaROooVKP0AGgEJzD9WXh00yiH0EexnH7VgHZTKNbSPBB +uIpZiDcUJhdpT7Hw8eBEkFr7EqT6OEeU6JTqh2auNdH/HNxFIMj2X4s6mmiuQjAy +yc3lQcqWbRqRd3xOAaYAVGlLYDUjPnTuxU7rURIkhuVJPhWJFr8C9medO9MEeCrb +3/StC3ZMOsHJbwHsrKd9Pty5dANCRo0HXgjhdpeHCNjg7Qz3DV+U+nCR4s/IWoU8 +dNjP5lQWyhIHdLrfZ3Kv53Svxk4OtDf3HRyxgxohQVzToJKfnjdpmnuNxnyaahH8 +sEpjthT6OAlMzxWxShT7bN/D6+mYKiXAUgxLGDZJUg== -----END CERTIFICATE----- diff --git a/services/federator/test/resources/unit/second-federator.example.com-key.pem b/services/federator/test/resources/unit/second-federator.example.com-key.pem index f71dd93d91d..6c412fe1818 100644 --- a/services/federator/test/resources/unit/second-federator.example.com-key.pem +++ b/services/federator/test/resources/unit/second-federator.example.com-key.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAs/5e0CMvRsVGyHScUiUfIgszybuCArL5NP7N0ee8yZlgej56 -JdkeQhbvJbm2MyWBI3vRhU4S4Sevr6PtONh4aP4JGJ7t5dhrJsuThTZXfePxANHl -LhwZYeAo06v/yGxaE9wSUDa7REW5U12wb0NNpMhHselG2pZFVNqBImDN9cLDHUzn -sC8pibxuWJqTHxIpHeKcvdJfoKbwAJpP8FjhewbNy7mQ3BVMzGMJKTPC0LrW3SfM -WK+iYrNN9Y5e9rnjYj2OY6ldCxjoC/JouoRPojr/s6WLIECch0RtImb3iL9/TzPq -LLuqMYzhYacCVgfKrBoXogWDwfHgh06I8tDUrQIDAQABAoIBAA3bFgdlvfzvsMw7 -ldEJBIWFYT41TgPRLTf0KXnwIetPAEtIxfRl61thEpXP3wO+7lsB7BYb9X4ZpP+b -WeaXW2WRsLeRfHTGHTGGWFvX3BJX2rSac88B4L5VGC97PRx7os+GkG5WWEIgL+0H -+E4IW1DFDifW8lpfWQT4MRqpYxF5BKcps2mOqaJ0+r9friyRD138MT357gKoNrtH -bnx5qcY/onNeZhIEolTZwkIkKLkF0PFnlc2Fg6j/Yx6NgYbefMl8UDq8j9thLNmC -6GgYuWvim6YmbG6OQZLM8/XOjgMsbj2kLz1eann/2+sWScBN0KvgurjKTi0W9U93 -+CzxU4ECgYEA0aJvZNhdQBaGCOIzFYF00REOcTsG5XXarlQiB+lEl3DUu9DUV+/S -m1P5R+PY+F6RZSh1a0vNJ3XxicOCP7ff1b7yd/mEqASffJZXkAjTQtd+sXmv8ztw -3xs1s2KNsgRkuSk+RGicfa9lgXXGGGpcSHhF23d6mt/sVybzieN7LyECgYEA282p -EGdedg2+1bBU1xBIWi3VlEmMM3O0YzCl3FrmTZipfAiNkP38JG4bjdKAWQaCCzFy -oo31puSvsiJJAo53oPJyoYzcxhP4ayu8I2NWWGtWLtQdJMIeb64zON+jfikLfe/Y -eeVTS4rQWqWWrzs07DiHICEnWmAppUN34yfRcA0CgYEAwXY3eQiX3iorHg2qeSFU -bhBglKyVq7M80f8AvO5Qh7XwDTgmjtTbNs+jUO378RJM+d8Bpbh5pv4YnzuXezg0 -0Kx8Va4m92v0x2RzgJA0bw4ydJTJhR4JB0y0HU1JWMznK13dggJM0UzJz1SiMSwO -6C5ewbAcrMsT8EYZINqXdyECgYEA2YCqQlw3ghRZ+bSST3qCThI63ZDl1mgxYVHb -XrDoYnKli+IezBI4dogqZe2eKfohpxfYOvjE09BJ97irjIcT48TmH+x27t8GHG7b -rhz3bWlRj+c/q4cXfSXg8++BJi9Ret5i1URTZ1ZGlNx0vpOU7AeH1whXm7u6mhmo -QbS+L+UCgYAImq7sfzD5v4kRXUPRoFTtOleXvbxdwU/v3PB4aG7iXdqeWocJI76k -p9bVxs/r8xoFBTrN6yycSDI3Lhwc3et5cl440/rrYK0YGnPrjKZrZcbWnikWCvk+ -6O2yLQrl4ks2zyD8HPtq9hfnPW4XDB4Vdr1qvclDKIUYHFRV/8v1EA== +MIIEowIBAAKCAQEAuNpPEDEP0uj8Rec3suIAIj789bKUtUnE87Hl5tN6/UTryhf/ +s8cs41+/PXuCHVjQthT7xxb7Jsx4W9DYjCH611hjUx3W3rjL9fIxDiUfUrB1CPIU +6N/j9hcNnx03T6neh5TdFxAS+3CECy0H7xCokunJQxXhlwmJ/Ml0iP2N1L20ZQdp +BLvPKxBdpDKWoydpkIM2JjvVYokFxLZ0hDgSkWTVVU50biwMPTdthW/gihUrIftR +TH0V16VCAMdOdNcOxcTQfjGVeF1UHEOJXjNSAQBvP+sD5pBFy1KY/RYiRxcoEMBP +ul6Ayc6iqN6KgxckzZwSySiPeSz/DIFOLHJPHQIDAQABAoIBADAtrfehYedtk/rA +JbM41iIW2qVK8xlA7dU6I5qCugZyxSW6FYoMunVUiiqDG0l80YDzfR/JrJHTLvd3 +OkljvdYMkm6iU1NfRMGIayKtqDlMBmTbe3mqiOal4YX7/mOD+ZzKvsj5BomELNYg +2XWEmPsZdbxHYrGT6eP5uabOtv2gNpMvDyizMJesPThTMFgPpn6VvtdGeDVDNmEC +W+4vF1mMii8GGA0i+OVZnYjjP3qIMaU8xJbda4rno4uU8CeZAu6/OYWOLJ8yZWgb +Hs9ifVVVky0Ykjz0q0i4/ti5YSV0QHvQFqJn4cRjCGAbx59KXNSFtwEmElwZAjOV +AbrO9D0CgYEA1/+IOWGvZkSXOygVfdpXoiNj9/qy8z20rlNY3WLfBbJGJiBFuK9O +xaIzhR4/mcfn4roW9+cki9cc2OcxWIM5im6zDKpjndvuEB/hZ7E7WyLjCCMaOf+V +UmX83L9fQSu+4z8gFL9t1qHFQ26eZ0R2wpmUeoSK3QEkkY3qn5sBsusCgYEA2xYs +hn4ZahnxRoGQHXyZyTih7TzbAdTOLBDha6ib4tos+K6XxantNPm6YZ0d1h3MwuBi +dLVdDyU4JW2wGkuVDlzh1MgLQ1oVyBt9QpDB/7Z9UIgN5v/7DVVLT38hi/NpkLL7 +qXHusuAClhRQqGggeq3t2n/cL+GJ7fFYFvz6tBcCgYBpNmRxpv6cLx8HRsApPJjh +NqH0Yd6XE2CWZazsscN2796xpZiwnFwfcqHr3s8WJkTysLiNar7nixHXKc5kkg8O +Olvm+HxroXx1yEGwk6kY/IZgKVEWHUPsDhe8o09P3HIwGUiUMqbbHJONBC4OmU/L +/KlRgIxvmKXqbJlzwzpxnwKBgHY6AxEY31IYadFofYLMCJlDzG4flvfoBNJW0a6t +MGI85mPUo+ZxCqa51NB0XvN65VKMj9T3Qh64MRJRnOSzwN4dVWjkAt/3ryVrYC8Z +uvbpXbqlkQsFPE83pgpiSpIhauhBDfmkl/FDXWHr2JLojg5l6aMtuH7GMQ6MXMMb +BZdFAoGBAMjLsQpWb3w5werUr+bC5FvqxeeCitkLOqu4YDLvxsy4sf2hq4HJZA0j +QxrcyLlTczu2rcBBm17fv/kf3fVYjRT2RIGtKY4rrDf62vyg/PuvNjFlLLFW+dMi +PouTFE/Pl49wLCURxKX59GTUCZzo7csNYMOp3kL6YujqW+J3J1ND -----END RSA PRIVATE KEY----- diff --git a/services/federator/test/resources/unit/second-federator.example.com.pem b/services/federator/test/resources/unit/second-federator.example.com.pem index e8550b57332..44d8f0d823c 100644 --- a/services/federator/test/resources/unit/second-federator.example.com.pem +++ b/services/federator/test/resources/unit/second-federator.example.com.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDVDCCAjygAwIBAgIUY7ykU1AZbcuWpyzmllHdEpiT7MkwDQYJKoZIhvcNAQEL -BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjIwNzIyMDgwODAwWhcN -MjMwNzIyMDgwODAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA -s/5e0CMvRsVGyHScUiUfIgszybuCArL5NP7N0ee8yZlgej56JdkeQhbvJbm2MyWB -I3vRhU4S4Sevr6PtONh4aP4JGJ7t5dhrJsuThTZXfePxANHlLhwZYeAo06v/yGxa -E9wSUDa7REW5U12wb0NNpMhHselG2pZFVNqBImDN9cLDHUznsC8pibxuWJqTHxIp -HeKcvdJfoKbwAJpP8FjhewbNy7mQ3BVMzGMJKTPC0LrW3SfMWK+iYrNN9Y5e9rnj -Yj2OY6ldCxjoC/JouoRPojr/s6WLIECch0RtImb3iL9/TzPqLLuqMYzhYacCVgfK -rBoXogWDwfHgh06I8tDUrQIDAQABo4GsMIGpMA4GA1UdDwEB/wQEAwIFoDAdBgNV +MIIDVDCCAjygAwIBAgIUdVshF7UQ9dbH0rSVBhrl5yj7fI0wDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjQwODIxMDg0NjAwWhcN +MjUwODIxMDg0NjAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +uNpPEDEP0uj8Rec3suIAIj789bKUtUnE87Hl5tN6/UTryhf/s8cs41+/PXuCHVjQ +thT7xxb7Jsx4W9DYjCH611hjUx3W3rjL9fIxDiUfUrB1CPIU6N/j9hcNnx03T6ne +h5TdFxAS+3CECy0H7xCokunJQxXhlwmJ/Ml0iP2N1L20ZQdpBLvPKxBdpDKWoydp +kIM2JjvVYokFxLZ0hDgSkWTVVU50biwMPTdthW/gihUrIftRTH0V16VCAMdOdNcO +xcTQfjGVeF1UHEOJXjNSAQBvP+sD5pBFy1KY/RYiRxcoEMBPul6Ayc6iqN6Kgxck +zZwSySiPeSz/DIFOLHJPHQIDAQABo4GsMIGpMA4GA1UdDwEB/wQEAwIFoDAdBgNV HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4E -FgQUbxdwjvoFOgaWjCrluZmkyMIrhnswHwYDVR0jBBgwFoAUmUEefcwWEyqnjNBx -DNucQ7woHRwwKgYDVR0RAQH/BCAwHoIcc2Vjb25kLWZlZGVyYXRvci5leGFtcGxl -LmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAPyjWTMn0mzA/IqXeSzX/cGWcJNKwBst1 -89lyOv7je5cDGNgOQt0S+bXgU+LHz6M6QaP5OjcJsNyAXsfXSehewlEk6sAh14dY -mU9Gbk8r/vA/hW8+bZH6ON2EE4ag/bRu+NtVsQM0+Gx7mN8ChrAwoM8wQ4gevG/g -rDBa8iNuqY3X77MbiOtvtsG+qOC1+N+4aeBDlw+mD9dL6Xblo1IkhkF3m0NXGbCC -O47euGPRYEm4wjEzeF85Sm9u7uf/orn06lwKNj8jemcc0rWjpCEL4wtfNoNFu+3e -M4X2jTcqw91v73jK95lU7FoAo7lTe3Po+Bz0OrdMheAhpnudz5Xb2A== +FgQUX86PqMWqHzPznwlXw6x2wWyLMGUwHwYDVR0jBBgwFoAUv4GMQ6kOgRxHoPPJ +oPWQGDxVuCMwKgYDVR0RAQH/BCAwHoIcc2Vjb25kLWZlZGVyYXRvci5leGFtcGxl +LmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAem1j1UwbIcDUOk3Ao7OHrgpAZGwJkyBm +bmLrWLpiLAzEoZPZTye2H1BWaxCOgzD9E/Adw4qVNEmU/2mI3RVCiiX+rinm2j8M +PmL52fQ9XdWZqAMNBD9ZIpBJo1O8K8pj7RSBULuoFYfoXLg18UlKE5xz4gbzLnlB +8i1Ig2zh+eOUKvF8DhXT7N5ceDnHWFBGGvGLxC8zJqOENcxpBbzgCGjYwcvJZYoQ +dohBKPOwZZ00JzZUpdR1k1n9NkBKfOnhqVp6hpp5clt3IhJ/lihJOjltt789hpR6 +XLSnBkN95BsEUoixba+HytnAhUjYgjJGhQT3nKwV1IXWDR6Ip8u2Og== -----END CERTIFICATE----- diff --git a/services/federator/test/resources/unit/unit-ca-key.pem b/services/federator/test/resources/unit/unit-ca-key.pem index f4849cf8406..5b02eb88316 100644 --- a/services/federator/test/resources/unit/unit-ca-key.pem +++ b/services/federator/test/resources/unit/unit-ca-key.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIIEpgIBAAKCAQEA3PaXwF/To/KvrEpUpVpv1lGqYph/hxVYZF76D6y1NHZSJQhD -We1kJ0HlbBDpky3nbkRlEuc2CC4VnmGxnGjGvXMiH6kc9hikfEEn1DDrxbZ6OTYU -8wSvaTrDPjT2GT05XJkaJ9riQsP24gwwwqYCCx9bgUfZkQ5cNwu3eVTJs+ugk5jt -2CTKJhBCuoNiz+g+XPxPkTKjkK51jNR87rO20495y7LQWOf1BRFST9W3xhC3XX67 -ckd1VfIL3f8QAvQOy0SjVjph98WWjXopfij2IAMqZvUwOv9CKzQ+o8zkGtXGSrf9 -jDI9NuMwseTTOVw9+6zdd9tdV/zlRZJXMAKz0QIDAQABAoIBAQCT86GEtCVX/6XV -LSgkV7vOu3vk7MtnPcmj21yAplnmuNsj5LaEPknzZ1Ig2+oqHHB8CtvKnno1W/Yp -yKxOWHHKZI8St/+mYLWyZUYv9FnKVvbb2/SHDDoPscMNohSuran88J7s1Mjvf5uN -nDACBXoUzHhOxjdNw1nHmdbOLRGpn/udVkAa7qgTXrEeY7wt12tIxk9sG/EyJPQw -MaWua2F/FwB8viYK7AABlNQwoo22W4WUSwy+3enflvA4zyhIC/FijiBrBqBFfBEy -rX6aIZM8jBmY+qGFgsHh0u6DSY87tEsbtkTm78XmxpsA2FFjsQRfPmzEDtZMHqSR -f7r1TZphAoGBAPM43gQ+RL6g5CihejJEvKeVe+G8qN0gQwxf+3onwdHoa8pJ+BP3 -1Xz44B5Q3F8U7/Ve/vR9oM3gQ6GnusA92Z0gEHQhVOVP4TpiSd5PZHcUeuaMzGlv -8NhZ06tvjv6rRNgQ6eA47jmDUMSeNjo7J5t9nkKfllJ71DKXMVoDRIXlAoGBAOiS -XL8B2lVpSzR+0czJpCfsfZG9pmNnUWPZGSYPSjIM9gVV4+4E0NU9V9kDxtjJJ/gB -Ocbf3fdW37wzuhcX1lrbCEvy8dvXBAWmEX7KeKv/8z6zUPr4WTfdgcGAAtfb5DKo -FGuVuH/aj/MuCw/6y9jfsvGXS3sb4dkUHnsNEtd9AoGBANoUIS1Yz7bE+A5eSvyL -mUQBymPrDtXGOZf877TltOmLJirpC+CLlQZK+Fj2U8GFOmqd1KhPhEFUVg+6Hr6E -Nnyl5VrbEA3UA7SfsG/+a6xB1rilghr/3cl/MUVD78j0s/OvpqP/J8q1rVO+MA0i -QS8wARjnkpc+pFMAMaXtzCDpAoGBAIZh681CWNH5YUcX8EoQSRX4hCXuG3JchJ7Z -FrrzIsAA1TcIBgfGKJTOOTBgCknBEaMvsh7DxBHi1Kx+hwxI7cbZwNWBr1aDywMo -jfthKpDX98lG+4fIcGTjTNJuETaBDD5o+EOh41WdCIhPFn1JQPgzacZG0Yp09e7k -vSgEydpRAoGBAJk61dDlXcuVSFkOX+L/7sGnxDO3BSnvZG+WbC6Hs5nRl/3dgIV+ -qpm44cSTiDifUdcnSclyqFhzTwgmTk5SWQr8Hbx+9/bB6l8xeLPXzvJWzqSkSFT4 -CRgBxDfDoQ9h9fnJKm30SMyUiRYhvaplWLp3lTIa8mxBYNcxxIWoqq8Y +MIIEowIBAAKCAQEAqyah8JD4swzUX3YyfKZDUi8c0bmLNHRibVWp6ezQtO598DpN +I1YlGuoh1Hjx86CCUIRkwIvrQ6k314PilCMY0mfH46tYxBPUlbCtksLzGKr/Gp4u +7igqf2E/D0opeGa4etmUjp9+Lfszj+hlHcp4klk5WoFnxbGdlN5xBHdtWOMFG8J9 +aurcJ4uK7pSnOTg9PXdDN9GjKba33RHF6Cz4+5jR+5ii92AqaXF1xp0kfHM3yqTO +6AzaPaYdoBLlvCtkvt5gl6lIrN3yMg+cR4Wxwq+Nd9DDHyQpL189xcQh41Z1j778 +9Lu3zWvDBegz1kZke7yRU1XymefNs705VNRVXQIDAQABAoIBAQCck3dLUhV0GKSh +wMxnZOPp59gBtjE4B9GUHO6ZZ2F4ZUcp4ux3C3RerxvL+em/7HPLnZNe46KT/9dq +fulmhMVMmE0yZ6uJlmrBlyT0Qw2K38UKYPSmRHC8oAbEwiA2WiMjoLJb2GXjQxDk +8sKvAnBT0vS5a/G31spHS0kxwOB0k5xN1GIC91dhz/cWzCbx/nLnSgNEElrf2uXs +H8FHCjeoop8S/OUwNEWFZsz+zoY3p/Crewbw1QS9ksAG0axRh0pm19UB+loSORvG +ibihkDEoRZLDTAEbeNdN+DL5VLN4m/URoVflY0XULaQ1S3UlaC5EOaNTmwfqVvPg +7wj8tJexAoGBAMNk4Gd805wAGggh2dqsxW33dBfr8CAWyytRD998ZtOLK2XNX5C0 ++7c60J6coXmLpqD2pwRh+0UBImxyxRM3oJ8sIYQY9FxSSouncM93bn+inFB7OqqI +hm7pGVDyVSIvTkl4espZg9qYH4+j12X+MbgKhY3mthx+wzsxHVnxcN6XAoGBAOA8 +vzXsEdfy8dtyLdZGTFxOV9Y7rtNR0LFkEbE91UiyDZNNV2LZ8mehJJRCCFprhWJ3 +PE7Eu39C8Dk5HZPKDlmkAyjvBbDHdYmfp83osr3QY5mWGW7tB0yhlKOZ061gPmz3 +y9WHt6MJ75QfyGC9DkmRrs8JgPWPXyCcD1/b6N4rAoGAVvovnkGFnNq6u8X9yQbB +d7sAVam0IUSkOesCqtQgnahEsDNkh/DYX+7xcLl+c7GDggFpxVysUkI4BZhtO6m2 +eURWwwussu/6uQHXCLM8X9qNxJGmQsU1OmfO2iaVWPs/2RqlZi/Eruiqm/Et7/vg +O/GLE+iQRkzzMQUi8Ke2O48CgYA4V6snzlgxbY7Nt+PCiklXjCvP3ZEw8cbBO8ai +RUoZCPwWPgGuCds5pKi/Q9Q7e/a45gLWO+JsuJIJnstKviNa7LYKi/xfDc7/tIkC +WOzIetr71VYwpAgIfjUN4nHoh11IHf8uePpwUefLzSyY+gfj/mHxDY9EpDCfzfai +/GuHnwKBgDaS9im5UWL09XEyD5mIeIkqQ77RKdY8+emNwMZ5A0duyPYRKUNL40E0 +2IvwK/ERENSlM+lSXLCw50mo+1qxrUvpXhdpbG81f1EF8ihonGbGRspK5Yip1ixs +QhpbHeqWZmQZlCIruN06Bn62At3puYjDpaeKNbi8rOTW/0m916W5 -----END RSA PRIVATE KEY----- diff --git a/services/federator/test/resources/unit/unit-ca.pem b/services/federator/test/resources/unit/unit-ca.pem index b6e3b1b5311..70528966ff4 100644 --- a/services/federator/test/resources/unit/unit-ca.pem +++ b/services/federator/test/resources/unit/unit-ca.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- -MIIDAjCCAeqgAwIBAgIUXYv9W35DKGoq2Hpp1nCpVb+f96kwDQYJKoZIhvcNAQEL -BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjIwNzIyMDgwODAwWhcN -MjcwNzIxMDgwODAwWjAZMRcwFQYDVQQDEw5jYS5leGFtcGxlLmNvbTCCASIwDQYJ -KoZIhvcNAQEBBQADggEPADCCAQoCggEBANz2l8Bf06Pyr6xKVKVab9ZRqmKYf4cV -WGRe+g+stTR2UiUIQ1ntZCdB5WwQ6ZMt525EZRLnNgguFZ5hsZxoxr1zIh+pHPYY -pHxBJ9Qw68W2ejk2FPMEr2k6wz409hk9OVyZGifa4kLD9uIMMMKmAgsfW4FH2ZEO -XDcLt3lUybProJOY7dgkyiYQQrqDYs/oPlz8T5Eyo5CudYzUfO6zttOPecuy0Fjn -9QURUk/Vt8YQt11+u3JHdVXyC93/EAL0DstEo1Y6YffFlo16KX4o9iADKmb1MDr/ -Qis0PqPM5BrVxkq3/YwyPTbjMLHk0zlcPfus3XfbXVf85UWSVzACs9ECAwEAAaNC -MEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJlB -Hn3MFhMqp4zQcQzbnEO8KB0cMA0GCSqGSIb3DQEBCwUAA4IBAQBhtvb/c5aM5F6M -y5qY9xJPDAbdPBKuQhQrAtUJn41+qKts0qsV4KNPuqv9zM+UIUTa2czt1MU9az4l -7S+fiL0EctBgifKT5bDBtbuiwztOxgFw5SwKyxRhoIx8k0g25O5opavhXwbHPjRx -4AjVbcEzzL9qnGaFfpps5BsznUFGntlBcw1peZQerUq5vbUYwN1qbsd/DFkU/5Z+ -V/6URgKXMqpU0OMuz20e9QNQcIgQKiYD5X7k3Ouscf5aKkZ9vaAC23NVcHAIq/Le -5mGZptEzuLq7Mq60d3vKC79L+cLm6ivSlg90PkMcmBPbQqwlv2mh094NeU5FibTh -SVvltELN +MIIDAjCCAeqgAwIBAgIUWLkk5j79RpKaTsLgjbFh8xteceQwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjQwODIxMDg0NjAwWhcN +MjkwODIwMDg0NjAwWjAZMRcwFQYDVQQDEw5jYS5leGFtcGxlLmNvbTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKsmofCQ+LMM1F92MnymQ1IvHNG5izR0 +Ym1Vqens0LTuffA6TSNWJRrqIdR48fOgglCEZMCL60OpN9eD4pQjGNJnx+OrWMQT +1JWwrZLC8xiq/xqeLu4oKn9hPw9KKXhmuHrZlI6ffi37M4/oZR3KeJJZOVqBZ8Wx +nZTecQR3bVjjBRvCfWrq3CeLiu6Upzk4PT13QzfRoym2t90Rxegs+PuY0fuYovdg +KmlxdcadJHxzN8qkzugM2j2mHaAS5bwrZL7eYJepSKzd8jIPnEeFscKvjXfQwx8k +KS9fPcXEIeNWdY++/PS7t81rwwXoM9ZGZHu8kVNV8pnnzbO9OVTUVV0CAwEAAaNC +MEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFL+B +jEOpDoEcR6DzyaD1kBg8VbgjMA0GCSqGSIb3DQEBCwUAA4IBAQCD8JDLG7F2VvMf +WONxbmw3nDlKzMyuOSfRXZr/RdpLJFlpz1uwUYV5fyT9wG9LFhw4aN+tXWi4+LLs +Vi947pqm78jqvI6rpqAcy/EjRf2oA9aGkZn1siM3eVga2ZDw9hstawB8ioW7Fj8C +rdHsGGsJHEkzXL6ntydaVphWTxvITvyHHwyI809Q3TotFn1zleXhFgiHwjHIjMnZ +vqd4juNUlaGyYQzg/QbOOCt65KcJ5mZTELNRXuNjf+QPmpc5lZXyEMrnTirleXza +S3Gb660QldEf0x8UoV/vF3cQZXGevqs7yNWUAMpHTnNjuYiXO646iHVDHUNMgZdZ +CxJC5jBz -----END CERTIFICATE----- diff --git a/services/federator/test/unit/Test/Federator/Client.hs b/services/federator/test/unit/Test/Federator/Client.hs index a816f7710c9..4801240ece8 100644 --- a/services/federator/test/unit/Test/Federator/Client.hs +++ b/services/federator/test/unit/Test/Federator/Client.hs @@ -54,8 +54,6 @@ import Wire.API.Federation.Client import Wire.API.Federation.Error import Wire.API.User (UserProfile) -instance AddAnnotation loc comp name x - targetDomain :: Domain targetDomain = Domain "target.example.com" @@ -97,7 +95,7 @@ withMockFederatorClient mock action = withTempMockFederator mock $ \port -> do ceTargetDomain = targetDomain, ceFederator = Endpoint "127.0.0.1" (fromIntegral port), ceHttp2Manager = mgr, - ceOriginRequestId = RequestId "N/A" + ceOriginRequestId = RequestId defRequestId } a <- runFederatorClient env action case a of @@ -137,7 +135,7 @@ testClientStreaming = withInfiniteMockServer $ \port -> do ceTargetDomain = targetDomain, ceFederator = Endpoint "127.0.0.1" (fromIntegral port), ceHttp2Manager = mgr, - ceOriginRequestId = RequestId "N/A" + ceOriginRequestId = RequestId defRequestId } venv = FederatorClientVersionedEnv env Nothing let c = clientIn (Proxy @StreamingAPI) (Proxy @(FederatorClient 'Brig)) @@ -202,7 +200,7 @@ testClientConnectionError = do ceTargetDomain = targetDomain, ceFederator = Endpoint "127.0.0.1" 1, ceHttp2Manager = mgr, - ceOriginRequestId = RequestId "N/A" + ceOriginRequestId = RequestId defRequestId } result <- runFederatorClient env (fedClient @'Brig @"get-user-by-handle" handle) case result of diff --git a/services/federator/test/unit/Test/Federator/ExternalServer.hs b/services/federator/test/unit/Test/Federator/ExternalServer.hs index ac45f5aae2d..93af2e8ba9c 100644 --- a/services/federator/test/unit/Test/Federator/ExternalServer.hs +++ b/services/federator/test/unit/Test/Federator/ExternalServer.hs @@ -256,6 +256,8 @@ requestNoCertificate = assertEqual "no calls to any service should be made" [] serviceCalls pure Wai.ResponseReceived +-- @SF.Federation @TSFI.Federate @TSFI.DNS @S2 @S3 @S7 +-- Reject request if the client certificate for federator is invalid requestInvalidCertificate :: TestTree requestInvalidCertificate = testCase "testRequestInvalidCertificate - should fail with a 404 when an invalid certificate is given" $ do @@ -274,6 +276,8 @@ requestInvalidCertificate = assertEqual "no calls to any service should be made" [] serviceCalls pure Wai.ResponseReceived +-- @END + testInvalidPaths :: TestTree testInvalidPaths = do let invalidPaths = diff --git a/services/federator/test/unit/Test/Federator/InternalServer.hs b/services/federator/test/unit/Test/Federator/InternalServer.hs index 66706b74f68..db91bf3dfe9 100644 --- a/services/federator/test/unit/Test/Federator/InternalServer.hs +++ b/services/federator/test/unit/Test/Federator/InternalServer.hs @@ -112,6 +112,7 @@ federatedRequestSuccess = body <- Wai.lazyResponseBody res body @?= "\"bar\"" +-- @SF.Federation @TSFI.Federate @TSFI.DNS @S2 @S3 @S7 -- -- Refuse to send outgoing request to non-included domain when AllowDynamic is configured. federatedRequestFailureAllowList :: TestTree @@ -154,3 +155,5 @@ federatedRequestFailureAllowList = . interpretMetricsEmpty $ callOutward targetDomain Brig (RPC "get-user-by-handle") request undefined eith @?= Left (FederationDenied targetDomain) + +-- @END diff --git a/services/federator/test/unit/Test/Federator/Options.hs b/services/federator/test/unit/Test/Federator/Options.hs index bece8365ab0..137b6d411e5 100644 --- a/services/federator/test/unit/Test/Federator/Options.hs +++ b/services/federator/test/unit/Test/Federator/Options.hs @@ -163,6 +163,8 @@ testSettings = assertFailure "expected failure for invalid private key, got success" ] +-- @SF.Federation @TSFI.Federate @S3 @S7 +-- failToStartWithInvalidServerCredentials :: IO () failToStartWithInvalidServerCredentials = do let settings = @@ -186,6 +188,8 @@ failToStartWithInvalidServerCredentials = do Right _ -> assertFailure "expected failure for invalid client certificate, got success" +-- @END + assertParsesAs :: (HasCallStack, Eq a, FromJSON a, Show a) => a -> ByteString -> Assertion assertParsesAs v bs = assertEqual "YAML parsing" (Right v) $ diff --git a/services/federator/test/unit/Test/Federator/Remote.hs b/services/federator/test/unit/Test/Federator/Remote.hs index 0a8b92e432a..9409a0c9f45 100644 --- a/services/federator/test/unit/Test/Federator/Remote.hs +++ b/services/federator/test/unit/Test/Federator/Remote.hs @@ -130,6 +130,7 @@ testValidatesCertificateSuccess = Right _ -> assertFailure "Congratulations, you fixed a known issue!" ] +-- @SF.Federation @TSFI.Federate @TSFI.DNS @S2 -- -- This is a group of test cases where refusing to connect with the server is -- checked. The second test case refuses to connect with a server when the @@ -155,6 +156,8 @@ testValidatesCertificateWrongHostname = Right _ -> assertFailure "Expected connection with the server to fail" ] +-- @END + testConnectionError :: TestTree testConnectionError = testCase "connection failures are reported correctly" $ do tlsSettings <- mkTLSSettingsOrThrow settings diff --git a/services/federator/test/unit/Test/Federator/Validation.hs b/services/federator/test/unit/Test/Federator/Validation.hs index bd2c882c0e7..1a36f2f6644 100644 --- a/services/federator/test/unit/Test/Federator/Validation.hs +++ b/services/federator/test/unit/Test/Federator/Validation.hs @@ -115,6 +115,7 @@ federateWithAllowListFail = $ ensureCanFederateWith (Domain "hello.world") assertBool "federating should not be allowed" (isLeft eith) +-- @SF.Federation @TSFI.Federate @TSFI.DNS @S2 @S3 @S7 -- -- Refuse to send outgoing request to non-included domain when AllowDynamic is configured. validateDomainAllowListFail :: TestTree @@ -132,6 +133,8 @@ validateDomainAllowListFail = $ validateDomain exampleCert (Domain "localhost.example.com") res @?= Left (FederationDenied (Domain "localhost.example.com")) +-- @END + validateDomainAllowListSuccess :: TestTree validateDomainAllowListSuccess = testCase "should give parsed domain if in the allow list" $ do @@ -148,6 +151,7 @@ validateDomainAllowListSuccess = $ validateDomain exampleCert domain assertEqual "validateDomain should give 'localhost.example.com' as domain" domain res +-- @SF.Federation @TSFI.Federate @TSFI.DNS @S3 @S7 -- -- Reject request if the infrastructure domain in the client cert does not match the backend -- domain in the `Wire-origin-domain` header. @@ -165,6 +169,8 @@ validateDomainCertWrongDomain = $ validateDomain exampleCert (Domain "foo.example.com") res @?= Left (AuthenticationFailure (pure [X509.NameMismatch "foo.example.com"])) +-- @END + validateDomainCertCN :: TestTree validateDomainCertCN = testCase "should succeed if the certificate has subject CN but no SAN" $ do @@ -247,9 +253,12 @@ validateDomainNonIdentitySRV = $ validateDomain exampleCert domain res @?= domain +-- @SF.Federation @TSFI.Federate @TSFI.DNS @S2 @S3 @S7 -- Reject request if the client certificate for federator is invalid validateDomainCertInvalid :: TestTree validateDomainCertInvalid = testCase "validateDomainCertInvalid - should fail if the client certificate is invalid" $ do let res = decodeCertificate "not a certificate" res @?= Left "no certificate found" + +-- @END diff --git a/services/galley/default.nix b/services/galley/default.nix index b414e5b0551..7cec256aa46 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -19,7 +19,6 @@ , bytestring , bytestring-conversion , call-stack -, case-insensitive , cassandra-util , cassava , cereal @@ -27,13 +26,11 @@ , conduit , containers , cookie -, cql , crypton , crypton-x509 , currency-codes , data-default , data-timeout -, either , enclosed-exceptions , errors , exceptions @@ -42,9 +39,11 @@ , federator , filepath , galley-types +, generics-sop , gitignoreSource -, gundeck-types , hex +, hs-opentelemetry-instrumentation-wai +, hs-opentelemetry-sdk , HsOpenSSL , http-api-data , http-client @@ -79,13 +78,12 @@ , resourcet , retry , safe-exceptions -, saml2-web-sso -, schema-profunctor , servant , servant-client , servant-client-core , servant-server , singletons +, singletons-base , sop-core , split , ssl-util @@ -98,13 +96,13 @@ , tasty-cannon , tasty-hunit , tasty-quickcheck +, template-haskell , temporary , text , time , tinylog , tls , transformers -, transitive-anns , types-common , types-common-aws , types-common-journal @@ -115,7 +113,6 @@ , utf8-string , uuid , uuid-types -, vector , wai , wai-extra , wai-middleware-gunzip @@ -124,6 +121,7 @@ , warp-tls , wire-api , wire-api-federation +, wire-otel , wire-subsystems , yaml }: @@ -147,25 +145,25 @@ mkDerivation { brig-types bytestring bytestring-conversion - case-insensitive cassandra-util cassava comonad containers - cql crypton crypton-x509 currency-codes + data-default data-timeout - either enclosed-exceptions errors exceptions extended extra galley-types - gundeck-types + generics-sop hex + hs-opentelemetry-instrumentation-wai + hs-opentelemetry-sdk HsOpenSSL http-client http-client-openssl @@ -187,22 +185,22 @@ mkDerivation { resourcet retry safe-exceptions - saml2-web-sso - schema-profunctor servant servant-client servant-server singletons + singletons-base + sop-core split ssl-util stm tagged + template-haskell text time tinylog tls transformers - transitive-anns types-common types-common-aws types-common-journal @@ -216,6 +214,7 @@ mkDerivation { wai-utilities wire-api wire-api-federation + wire-otel wire-subsystems ]; executableHaskellDepends = [ @@ -230,9 +229,7 @@ mkDerivation { bytestring bytestring-conversion call-stack - case-insensitive cassandra-util - cassava cereal conduit containers @@ -271,8 +268,6 @@ mkDerivation { quickcheck-instances random retry - saml2-web-sso - schema-profunctor servant-client servant-client-core servant-server @@ -298,9 +293,7 @@ mkDerivation { unliftio unordered-containers uuid - vector wai - wai-extra wai-utilities warp warp-tls diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 47474894165..0d7a3da6931 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -25,6 +25,7 @@ common common-all default-extensions: AllowAmbiguousTypes BangPatterns + BlockArguments ConstraintKinds DataKinds DefaultSignatures @@ -81,6 +82,7 @@ library Galley.API.Internal Galley.API.LegalHold Galley.API.LegalHold.Conflicts + Galley.API.LegalHold.Get Galley.API.LegalHold.Team Galley.API.Mapping Galley.API.Message @@ -120,6 +122,7 @@ library Galley.API.Push Galley.API.Query Galley.API.Teams + Galley.API.Teams.Export Galley.API.Teams.Features Galley.API.Teams.Features.Get Galley.API.Teams.Notifications @@ -136,9 +139,12 @@ library Galley.Cassandra.Conversation.MLS Galley.Cassandra.ConversationList Galley.Cassandra.CustomBackend - Galley.Cassandra.GetAllTeamFeatureConfigs + Galley.Cassandra.FeatureTH + Galley.Cassandra.GetAllTeamFeatures Galley.Cassandra.Instances Galley.Cassandra.LegalHold + Galley.Cassandra.MakeFeature + Galley.Cassandra.Orphans Galley.Cassandra.Proposal Galley.Cassandra.Queries Galley.Cassandra.SearchVisibility @@ -272,97 +278,99 @@ library Galley.Schema.V90_EnforceFileDownloadLocationConfig Galley.Schema.V91_TeamMemberDeletedLimitedEventFanout Galley.Schema.V92_MlsE2EIdConfig + Galley.Schema.V93_ConferenceCallingSftForOneToOne Galley.Types.Clients Galley.Types.ToUserRole Galley.Types.UserList Galley.Validation - ghc-options: -fplugin=TransitiveAnns.Plugin + ghc-options: other-modules: Paths_galley hs-source-dirs: src build-depends: - , aeson >=2.0.1.0 - , amazonka >=1.4.5 - , amazonka-sqs >=1.4.5 + , aeson >=2.0.1.0 + , amazonka >=1.4.5 + , amazonka-sqs >=1.4.5 , amqp , asn1-encoding , asn1-types - , async >=2.0 - , base >=4.6 && <5 - , base64-bytestring >=1.0 - , bilge >=0.21.1 - , brig-types >=0.73.1 - , bytestring >=0.9 - , bytestring-conversion >=0.2 - , case-insensitive - , cassandra-util >=0.16.2 - , cassava >=0.5.2 + , async >=2.0 + , base >=4.6 && <5 + , base64-bytestring >=1.0 + , bilge >=0.21.1 + , brig-types >=0.73.1 + , bytestring >=0.9 + , bytestring-conversion >=0.2 + , cassandra-util >=0.16.2 + , cassava >=0.5.2 , comonad - , containers >=0.5 - , cql + , containers >=0.5 , crypton , crypton-x509 - , currency-codes >=2.0 + , currency-codes >=2.0 + , data-default , data-timeout - , either - , enclosed-exceptions >=1.0 - , errors >=2.0 - , exceptions >=0.4 + , enclosed-exceptions >=1.0 + , errors >=2.0 + , exceptions >=0.4 , extended - , extra >=1.3 - , galley-types >=0.65.0 - , gundeck-types >=1.35.2 + , extra >=1.3 + , galley-types >=0.65.0 + , generics-sop , hex - , HsOpenSSL >=0.11 - , http-client >=0.7 - , http-client-openssl >=0.2 + , hs-opentelemetry-instrumentation-wai + , hs-opentelemetry-sdk + , HsOpenSSL >=0.11 + , http-client >=0.7 + , http-client-openssl >=0.2 , http-media - , http-types >=0.8 + , http-types >=0.8 , http2-manager , imports , kan-extensions - , lens >=4.4 + , lens >=4.4 , metrics-core - , metrics-wai >=0.4 + , metrics-wai >=0.4 , optparse-applicative , pem , polysemy , polysemy-wire-zoo , prometheus-client - , proto-lens >=0.2 - , raw-strings-qq >=1.0 - , resourcet >=1.1 - , retry >=0.5 - , safe-exceptions >=0.1 - , saml2-web-sso >=0.20 - , schema-profunctor + , proto-lens >=0.2 + , raw-strings-qq >=1.0 + , resourcet >=1.1 + , retry >=0.5 + , safe-exceptions >=0.1 , servant , servant-client , servant-server , singletons - , split >=0.2 - , ssl-util >=0.1 - , stm >=2.4 + , singletons-base + , sop-core + , split >=0.2 + , ssl-util >=0.1 + , stm >=2.4 , tagged - , text >=0.11 - , time >=1.4 - , tinylog >=0.10 - , tls >=1.7.0 + , template-haskell + , text >=0.11 + , time >=1.4 + , tinylog >=0.10 + , tls >=1.7.0 , transformers - , transitive-anns - , types-common >=0.16 + , types-common >=0.16 , types-common-aws - , types-common-journal >=0.1 - , unliftio >=0.2 - , uri-bytestring >=0.2 + , types-common-journal >=0.1 + , unliftio >=0.2 + , uri-bytestring >=0.2 , utf8-string - , uuid >=1.3 - , wai >=3.0 - , wai-extra >=3.0 - , wai-middleware-gunzip >=0.0.2 - , wai-utilities >=0.16 + , uuid >=1.3 + , wai >=3.0 + , wai-extra >=3.0 + , wai-middleware-gunzip >=0.0.2 + , wai-utilities >=0.16 , wire-api , wire-api-federation + , wire-otel , wire-subsystems executable galley @@ -472,9 +480,7 @@ executable galley-integration , bytestring , bytestring-conversion , call-stack - , case-insensitive , cassandra-util - , cassava , cereal , containers , cookie @@ -512,8 +518,6 @@ executable galley-integration , quickcheck-instances , random , retry - , saml2-web-sso >=0.20 - , schema-profunctor , servant-client , servant-client-core , servant-server @@ -539,9 +543,7 @@ executable galley-integration , unliftio , unordered-containers , uuid - , vector , wai - , wai-extra , wai-utilities , warp , warp-tls >=3.2 @@ -572,7 +574,6 @@ executable galley-migrate-data , exceptions , extended , imports - , lens , optparse-applicative , text , time diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index 465d807cec3..3f1e069313f 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -50,13 +50,20 @@ settings: # Once set, DO NOT change it: if you do, existing users may have a broken experience and/or stop working # Remember to keep it the same in Brig federationDomain: example.com + federationProtocols: ["mls", "proteus"] mlsPrivateKeyPaths: removal: - ed25519: test/resources/ed25519.pem - ecdsa_secp256r1_sha256: test/resources/ecdsa_secp256r1_sha256.pem - ecdsa_secp384r1_sha384: test/resources/ecdsa_secp384r1_sha384.pem - ecdsa_secp521r1_sha512: test/resources/ecdsa_secp521r1_sha512.pem + ed25519: test/resources/backendA/ed25519.pem + ecdsa_secp256r1_sha256: test/resources/backendA/ecdsa_secp256r1_sha256.pem + ecdsa_secp384r1_sha384: test/resources/backendA/ecdsa_secp384r1_sha384.pem + ecdsa_secp521r1_sha512: test/resources/backendA/ecdsa_secp521r1_sha512.pem guestLinkTTLSeconds: 604800 + passwordHashingOptions: # in testing, we want these settings to be faster, not secure against attacks. + algorithm: argon2id + iterations: 1 + memory: 128 + parallelism: 1 + # We explicitly do not disable any API version. Please make sure the configuration value is the same in all these configs: # brig, cannon, cargohold, galley, gundeck, proxy, spar. disabledAPIVersions: [] @@ -75,13 +82,20 @@ settings: status: enabled config: domains: ["example.com"] + enforceFileDownloadLocation: + defaults: + status: disabled + lockStatus: unlocked + config: + enforcedDownloadLocation: "downloads" fileSharing: defaults: status: enabled lockStatus: unlocked conferenceCalling: defaults: - status: enabled + status: disabled + lockStatus: locked outlookCalIntegration: defaults: status: disabled @@ -92,6 +106,7 @@ settings: config: verificationExpiration: 86400 acmeDiscoveryUrl: null + crlProxy: https://crlproxy.example.com lockStatus: unlocked mlsMigration: defaults: diff --git a/services/galley/migrate-data/src/Galley/DataMigration.hs b/services/galley/migrate-data/src/Galley/DataMigration.hs index e4c27464ee0..5393d52a4d8 100644 --- a/services/galley/migrate-data/src/Galley/DataMigration.hs +++ b/services/galley/migrate-data/src/Galley/DataMigration.hs @@ -40,10 +40,10 @@ data CassandraSettings = CassandraSettings toCassandraOpts :: CassandraSettings -> CassandraOpts toCassandraOpts cas = CassandraOpts - { _endpoint = Endpoint (Text.pack (cas.cHost)) (cas.cPort), - _keyspace = C.unKeyspace (cas.cKeyspace), - _filterNodesByDatacentre = Nothing, - _tlsCa = cas.cTlsCa + { endpoint = Endpoint (Text.pack (cas.cHost)) (cas.cPort), + keyspace = C.unKeyspace (cas.cKeyspace), + filterNodesByDatacentre = Nothing, + tlsCa = cas.cTlsCa } cassandraSettingsParser :: Parser CassandraSettings diff --git a/services/galley/migrate-data/src/V1_BackfillBillingTeamMembers.hs b/services/galley/migrate-data/src/V1_BackfillBillingTeamMembers.hs index 7d46e3f8f13..6903c066cc1 100644 --- a/services/galley/migrate-data/src/V1_BackfillBillingTeamMembers.hs +++ b/services/galley/migrate-data/src/V1_BackfillBillingTeamMembers.hs @@ -19,7 +19,6 @@ module V1_BackfillBillingTeamMembers where import Cassandra import Conduit -import Control.Lens (view) import Data.Conduit.Internal (zipSources) import Data.Conduit.List qualified as C import Data.Id @@ -70,5 +69,5 @@ createBillingTeamMembers pair = cql = "INSERT INTO billing_team_member (team, user) values (?, ?)" isOwner :: (TeamId, UserId, Maybe Permissions) -> Bool -isOwner (_, _, Just p) = SetBilling `Set.member` view self p +isOwner (_, _, Just p) = SetBilling `Set.member` p.self isOwner _ = False diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 23a3ecb106c..e5e7ff6a79d 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -26,7 +26,6 @@ module Galley.API.Action -- * Performing actions updateLocalConversation, updateLocalConversationUnchecked, - updateLocalConversationUserUnchecked, NoChanges (..), LocalConversationUpdate (..), notifyTypingIndicator, @@ -40,6 +39,7 @@ module Galley.API.Action addLocalUsersToRemoteConv, ConversationUpdate, getFederationStatus, + enforceFederationProtocol, checkFederationStatus, firstConflictOrFullyConnected, ) @@ -92,7 +92,6 @@ import Galley.Options import Galley.Types.Conversations.Members import Galley.Types.UserList import Galley.Validation -import Gundeck.Types.Push.V2 qualified as PushV2 import Imports hiding ((\\)) import Network.AMQP qualified as Q import Polysemy @@ -117,12 +116,13 @@ import Wire.API.Federation.API.Galley import Wire.API.Federation.API.Galley qualified as F import Wire.API.Federation.Error import Wire.API.FederationStatus +import Wire.API.Push.V2 qualified as PushV2 import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Team.Feature import Wire.API.Team.LegalHold import Wire.API.Team.Member import Wire.API.Team.Permission (Perm (AddRemoveConvMember, ModifyConvName)) -import Wire.API.User qualified as User +import Wire.API.User as User import Wire.NotificationSubsystem data NoChanges = NoChanges @@ -244,11 +244,8 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con HasConversationActionEffects 'ConversationUpdateProtocolTag r = ( Member ConversationStore r, Member (ErrorS 'ConvInvalidProtocolTransition) r, - Member (ErrorS OperationDenied) r, Member (ErrorS 'MLSMigrationCriteriaNotSatisfied) r, Member (Error NoChanges) r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'TeamNotFound) r, Member BrigAccess r, Member ExternalAccess r, Member FederatorAccess r, @@ -261,7 +258,6 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Member Random r, Member SubConversationStore r, Member TeamFeatureStore r, - Member TeamStore r, Member TinyLog r ) @@ -332,6 +328,19 @@ type family HasConversationActionGalleyErrors (tag :: ConversationActionTag) :: ErrorS 'TeamNotFound ] +enforceFederationProtocol :: + ( Member (Error FederationError) r, + Member (Input Opts) r + ) => + ProtocolTag -> + [Remote ()] -> + Sem r () +enforceFederationProtocol proto domains = do + unless (null domains) $ do + mAllowedProtos <- view (settings . federationProtocols) <$> input + unless (maybe True (elem proto) mAllowedProtos) $ + throw FederationDisabledForProtocol + checkFederationStatus :: ( Member (Error UnreachableBackends) r, Member (Error NonFederatingBackends) r, @@ -494,7 +503,7 @@ performAction tag origUser lconv action = do E.updateToMixedProtocol lcnv (convType (tUnqualified lconv)) pure (mempty, action) (ProtocolMixedTag, ProtocolMLSTag, Just tid) -> do - mig <- getFeatureStatus @MlsMigrationConfig DontDoAuth tid + mig <- getFeatureForTeam @MlsMigrationConfig tid now <- input mlsConv <- mkMLSConversation conv >>= noteS @'ConvInvalidProtocolTransition ok <- checkMigrationCriteria now mlsConv mig @@ -526,6 +535,7 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do ensureMemberLimit (convProtocolTag conv) (toList (convLocalMembers conv)) newMembers ensureAccess conv InviteAccess checkLocals lusr (convTeam conv) (ulLocals newMembers) + enforceFederationProtocol (protocolTag conv.convProtocol) (fmap void (ulRemotes newMembers)) checkRemotes lusr (ulRemotes newMembers) checkLHPolicyConflictsLocal (ulLocals newMembers) checkLHPolicyConflictsRemote (FutureWork (ulRemotes newMembers)) @@ -792,28 +802,6 @@ updateLocalConversationUnchecked lconv qusr con action = do (convBotsAndMembers conv <> extraTargets) action' --- | Similar to 'updateLocalConversationUnchecked', but skips performing --- user authorisation checks. This is written for use in de-federation code --- where conversations for many users will be torn down at once and must work. --- --- Additionally, this function doesn't make notification calls to clients. -updateLocalConversationUserUnchecked :: - forall tag r. - ( SingI tag, - HasConversationActionEffects tag r, - Member BackendNotificationQueueAccess r, - Member (Error FederationError) r - ) => - Local Conversation -> - Qualified UserId -> - ConversationAction tag -> - Sem r () -updateLocalConversationUserUnchecked lconv qusr action = do - let tag = sing @tag - - -- perform action - void $ performAction tag qusr lconv action - -- -------------------------------------------------------------------------------- -- -- Utilities diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index fa65410616d..9ab84a07469 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -63,7 +63,6 @@ import Galley.Types.Teams (notTeamMember) import Galley.Types.ToUserRole import Galley.Types.UserList import Galley.Validation -import Gundeck.Types.Push.V2 qualified as PushV2 import Imports hiding ((\\)) import Polysemy import Polysemy.Error @@ -76,6 +75,7 @@ import Wire.API.Error.Galley import Wire.API.Event.Conversation import Wire.API.Federation.Error import Wire.API.FederationStatus +import Wire.API.Push.V2 qualified as PushV2 import Wire.API.Routes.Public.Galley.Conversation import Wire.API.Routes.Public.Util import Wire.API.Team @@ -160,6 +160,7 @@ createGroupConversation :: Sem r CreateGroupConversationResponse createGroupConversation lusr conn newConv = do let remoteDomains = void <$> snd (partitionQualified lusr $ newConv.newConvQualifiedUsers) + enforceFederationProtocol (baseProtocolToProtocol newConv.newConvProtocol) remoteDomains checkFederationStatus (RemoteDomains $ Set.fromList remoteDomains) cnv <- createGroupConversationGeneric diff --git a/services/galley/src/Galley/API/Error.hs b/services/galley/src/Galley/API/Error.hs index 04423558a2f..a8241afa1c4 100644 --- a/services/galley/src/Galley/API/Error.hs +++ b/services/galley/src/Galley/API/Error.hs @@ -25,9 +25,6 @@ module Galley.API.Error internalErrorWithDescription, internalErrorDescription, legalHoldServiceUnavailable, - - -- * Errors thrown by wai-routing handlers - invalidTeamNotificationId, ) where @@ -65,12 +62,14 @@ data InvalidInput | InvalidRange LText | InvalidUUID4 | InvalidPayload LText + | FederationFunctionNotSupported LText instance APIError InvalidInput where toResponse CustomRolesNotSupported = toResponse $ badRequest "Custom roles not supported" toResponse (InvalidRange t) = toResponse $ invalidRange t toResponse InvalidUUID4 = toResponse invalidUUID4 toResponse (InvalidPayload t) = toResponse $ invalidPayload t + toResponse (FederationFunctionNotSupported t) = toResponse $ federationFunctionNotSupported t ---------------------------------------------------------------------------- -- Other errors @@ -87,6 +86,9 @@ invalidPayload = Wai.mkError status400 "invalid-payload" badRequest :: LText -> Wai.Error badRequest = Wai.mkError status400 "bad-request" +federationFunctionNotSupported :: LText -> Wai.Error +federationFunctionNotSupported = Wai.mkError status400 "federation-function-not-supported" + invalidUUID4 :: Wai.Error invalidUUID4 = Wai.mkError status400 "client-error" "Invalid UUID v4 format" @@ -101,6 +103,3 @@ badConvState cid = legalHoldServiceUnavailable :: (Show a) => a -> Wai.Error legalHoldServiceUnavailable e = Wai.mkError status412 "legalhold-unavailable" ("legal hold service unavailable with underlying error: " <> (LT.pack . show $ e)) - -invalidTeamNotificationId :: Wai.Error -invalidTeamNotificationId = Wai.mkError status400 "invalid-notification-id" "Could not parse notification id (must be UUIDv1)." diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index aab4029df73..d46f818b856 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -39,6 +39,7 @@ import Data.Text.Lazy qualified as LT import Data.Time.Clock import Galley.API.Action import Galley.API.Error +import Galley.API.MLS import Galley.API.MLS.Enabled import Galley.API.MLS.GroupInfo import Galley.API.MLS.Message @@ -62,7 +63,6 @@ import Galley.Options import Galley.Types.Conversations.Members import Galley.Types.Conversations.One2One import Galley.Types.UserList (UserList (UserList)) -import Gundeck.Types.Push.V2 (RecipientClients (..)) import Imports import Polysemy import Polysemy.Error @@ -89,9 +89,11 @@ 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.ServantProto import Wire.API.User (BaseProtocolTag (..)) @@ -104,19 +106,21 @@ federationSitemap :: ServerT FederationAPI (Sem GalleyEffects) federationSitemap = Named @"on-conversation-created" onConversationCreated + :<|> Named @"get-conversations@v1" getConversationsV1 :<|> Named @"get-conversations" getConversations - :<|> Named @"leave-conversation" (callsFed (exposeAnnotations leaveConversation)) - :<|> Named @"send-message" (callsFed (exposeAnnotations sendMessage)) - :<|> Named @"update-conversation" (callsFed (exposeAnnotations updateConversation)) + :<|> Named @"leave-conversation" leaveConversation + :<|> Named @"send-message" sendMessage + :<|> Named @"update-conversation" updateConversation :<|> Named @"mls-welcome" mlsSendWelcome - :<|> Named @"send-mls-message" (callsFed (exposeAnnotations sendMLSMessage)) - :<|> Named @"send-mls-commit-bundle" (callsFed (exposeAnnotations sendMLSCommitBundle)) + :<|> Named @"send-mls-message" sendMLSMessage + :<|> Named @"send-mls-commit-bundle" sendMLSCommitBundle :<|> Named @"query-group-info" queryGroupInfo - :<|> Named @"update-typing-indicator" (callsFed (exposeAnnotations updateTypingIndicator)) + :<|> Named @"update-typing-indicator" updateTypingIndicator :<|> Named @"on-typing-indicator-updated" onTypingIndicatorUpdated :<|> Named @"get-sub-conversation" getSubConversationForRemoteUser - :<|> Named @"delete-sub-conversation" (callsFed deleteSubConversationForRemoteUser) - :<|> Named @"leave-sub-conversation" (callsFed leaveSubConversation) + :<|> Named @"delete-sub-conversation" deleteSubConversationForRemoteUser + :<|> Named @"leave-sub-conversation" leaveSubConversation + :<|> Named @"get-one2one-conversation@v1" getOne2OneConversationV1 :<|> Named @"get-one2one-conversation" getOne2OneConversation :<|> Named @"on-client-removed" onClientRemoved :<|> Named @"on-message-sent" onMessageSent @@ -198,17 +202,27 @@ onConversationCreated domain rc = do pushConversationEvent Nothing event (qualifyAs loc [qUnqualified . Public.memId $ mem]) [] pure EmptyResponse -getConversations :: +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 () - GetConversationsResponse + GetConversationsResponseV2 . mapMaybe (Mapping.conversationToRemote (tDomain loc) ruid) <$> E.getConversations cids @@ -612,15 +626,25 @@ sendMLSCommitBundle remoteDomain msr = handleMLSMessageErrors $ do ibundle <- noteS @'MLSUnsupportedMessage $ mkIncomingBundle bundle (ctype, qConvOrSub) <- getConvFromGroupId ibundle.groupId when (qUnqualified qConvOrSub /= msr.convOrSubId) $ throwS @'MLSGroupConversationMismatch - MLSMessageResponseUpdates . map lcuUpdate - <$> postMLSCommitBundle - loc - (tUntagged sender) - msr.senderClient - ctype - qConvOrSub - Nothing - ibundle + -- this cannot throw the error since we always pass the sender which is qualified to be remote + 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, @@ -735,34 +759,62 @@ deleteSubConversationForRemoteUser domain DeleteSubConversationFedRequest {..} = lconv <- qualifyLocal dscreqConv deleteLocalSubConversation qusr lconv dscreqSubConv dsc +getOne2OneConversationV1 :: + ( Member (Input (Local ())) r, + Member BrigAccess 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 BrigAccess r + Member BrigAccess r, + Member (Input Env) r ) => Domain -> GetOne2OneConversationRequest -> - Sem r GetOne2OneConversationResponse + Sem r GetOne2OneConversationResponseV2 getOne2OneConversation domain (GetOne2OneConversationRequest self other) = - fmap (Imports.fromRight GetOne2OneConversationNotConnected) + fmap (Imports.fromRight GetOne2OneConversationV2MLSNotEnabled) + . runError @(Tagged 'MLSNotEnabled ()) + . fmap (Imports.fromRight GetOne2OneConversationV2NotConnected) . runError @(Tagged 'NotConnected ()) $ do lother <- qualifyLocal other let rself = toRemoteUnsafe domain self - ensureConnectedToRemotes lother [rself] let getLocal lconv = do mconv <- E.getConversation (tUnqualified lconv) - fmap GetOne2OneConversationOk $ case mconv of + 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 GetOne2OneConversationBackendMismatch)) + (const (pure GetOne2OneConversationV2BackendMismatch)) (one2OneConvId BaseProtocolMLSTag (tUntagged lother) (tUntagged rself)) -------------------------------------------------------------------------------- @@ -824,7 +876,7 @@ onMLSMessageSent domain rmm = ByteString ) let recipients = - filter (\r -> Set.member (_recipientUserId r) members) + filter (\r -> Set.member (recipientUserId r) members) . map (\(u, clts) -> Recipient u (RecipientClientsSome (List1 clts))) . Map.assocs $ rmm.recipients diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 3cf4708fa8b..411ad8fe295 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -1,3 +1,6 @@ +{-# LANGUAGE PartialTypeSignatures #-} +{-# OPTIONS_GHC -Wno-partial-type-signatures #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -42,13 +45,13 @@ import Galley.API.MLS.Removal import Galley.API.One2One import Galley.API.Public.Servant import Galley.API.Query qualified as Query -import Galley.API.Teams (uncheckedDeleteTeamMember) +import Galley.API.Teams import Galley.API.Teams qualified as Teams import Galley.API.Teams.Features +import Galley.API.Teams.Features.Get import Galley.API.Update qualified as Update import Galley.API.Util import Galley.App -import Galley.Cassandra.TeamFeatures (getAllFeatureConfigsForServer) import Galley.Data.Conversation qualified as Data import Galley.Effects import Galley.Effects.BackendNotificationQueueAccess @@ -65,14 +68,13 @@ import Galley.Options hiding (brig) import Galley.Queue qualified as Q import Galley.Types.Conversations.Members (RemoteMember (rmId)) import Galley.Types.UserList -import Gundeck.Types.Push.V2 qualified as PushV2 import Imports hiding (head) import Network.AMQP qualified as Q import Polysemy import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog qualified as P -import Servant hiding (JSON, WithStatus) +import Servant import System.Logger.Class hiding (Path, name) import System.Logger.Class qualified as Log import Wire.API.Conversation hiding (Member) @@ -84,13 +86,14 @@ import Wire.API.Event.LeaveReason import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error +import Wire.API.Push.V2 qualified as PushV2 import Wire.API.Routes.API import Wire.API.Routes.Internal.Brig.EJPD import Wire.API.Routes.Internal.Galley import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Routes.MultiTablePaging (mtpHasMore, mtpPagingState, mtpResults) import Wire.API.Routes.MultiTablePaging qualified as MTP -import Wire.API.Team.Feature hiding (setStatus) +import Wire.API.Team.Feature import Wire.API.User.Client import Wire.NotificationSubsystem import Wire.Sem.Paging @@ -100,8 +103,8 @@ internalAPI :: API InternalAPI GalleyEffects internalAPI = hoistAPI @InternalAPIBase Imports.id $ mkNamedAPI @"status" (pure ()) - <@> mkNamedAPI @"delete-user" (callsFed (exposeAnnotations rmUser)) - <@> mkNamedAPI @"connect" (callsFed (exposeAnnotations Create.createConnectConversation)) + <@> mkNamedAPI @"delete-user" rmUser + <@> mkNamedAPI @"connect" Create.createConnectConversation <@> mkNamedAPI @"get-conversation-clients" iGetMLSClientListForConv <@> mkNamedAPI @"guard-legalhold-policy-conflicts" guardLegalholdPolicyConflictsH <@> legalholdWhitelistedTeamsAPI @@ -114,7 +117,7 @@ internalAPI = <@> iEJPDAPI iEJPDAPI :: API IEJPDAPI GalleyEffects -iEJPDAPI = mkNamedAPI @"get-conversations-by-user" (callsFed (exposeAnnotations ejpdGetConvInfo)) +iEJPDAPI = mkNamedAPI @"get-conversations-by-user" ejpdGetConvInfo -- | An unpaginated, internal http interface to `Query.conversationIdsPageFrom`. Used for -- EJPD reports. Called locally with very little data for each conv, so we don't expect @@ -171,12 +174,10 @@ conversationAPI :: API IConversationAPI GalleyEffects conversationAPI = mkNamedAPI @"conversation-get-member" Query.internalGetMember <@> mkNamedAPI @"conversation-accept-v2" Update.acceptConv - <@> mkNamedAPI @"conversation-block-unqualified" Update.blockConvUnqualified <@> mkNamedAPI @"conversation-block" Update.blockConv - <@> mkNamedAPI @"conversation-unblock-unqualified" Update.unblockConvUnqualified <@> mkNamedAPI @"conversation-unblock" Update.unblockConv <@> mkNamedAPI @"conversation-meta" Query.getConversationMeta - <@> mkNamedAPI @"conversation-mls-one-to-one" Query.getMLSOne2OneConversation + <@> mkNamedAPI @"conversation-mls-one-to-one" Query.getMLSOne2OneConversationInternal <@> mkNamedAPI @"conversation-mls-one-to-one-established" Query.isMLSOne2OneEstablished legalholdWhitelistedTeamsAPI :: API ILegalholdWhitelistedTeamsAPI GalleyEffects @@ -226,85 +227,67 @@ miscAPI = <@> mkNamedAPI @"test-delete-client" Clients.rmClient <@> mkNamedAPI @"add-service" createService <@> mkNamedAPI @"delete-service" deleteService - <@> mkNamedAPI @"add-bot" Update.addBot + <@> mkNamedAPI @"i-add-bot" Update.addBot <@> mkNamedAPI @"delete-bot" Update.rmBot <@> mkNamedAPI @"put-custom-backend" setCustomBackend <@> mkNamedAPI @"delete-custom-backend" deleteCustomBackend +featureAPI1Full :: + forall cfg r. + (_) => + API (IFeatureAPI1Full cfg) r +featureAPI1Full = + mkNamedAPI @'("iget", cfg) getFeatureInternal + <@> mkNamedAPI @'("iput", cfg) setFeatureInternal + <@> mkNamedAPI @'("ipatch", cfg) patchFeatureInternal + +featureAPI1Get :: + forall cfg r. + (_) => + API (IFeatureStatusGet cfg) r +featureAPI1Get = mkNamedAPI @'("iget", cfg) getFeatureInternal + +allFeaturesAPI :: API (IAllFeaturesAPI Features) GalleyEffects +allFeaturesAPI = + featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Get + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + featureAPI :: API IFeatureAPI GalleyEffects featureAPI = - mkNamedAPI @'("iget", SSOConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", SSOConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", SSOConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", LegalholdConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", LegalholdConfig) (callsFed (exposeAnnotations setFeatureStatusInternal)) - <@> mkNamedAPI @'("ipatch", LegalholdConfig) (callsFed (exposeAnnotations patchFeatureStatusInternal)) - <@> mkNamedAPI @'("iget", SearchVisibilityAvailableConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", SearchVisibilityAvailableConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", SearchVisibilityAvailableConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", ValidateSAMLEmailsConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", ValidateSAMLEmailsConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", ValidateSAMLEmailsConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", DigitalSignaturesConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", DigitalSignaturesConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", DigitalSignaturesConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", AppLockConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", AppLockConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", AppLockConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", FileSharingConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", FileSharingConfig) setFeatureStatusInternal + allFeaturesAPI + -- legacy endpoints <@> mkNamedAPI @'("ilock", FileSharingConfig) (updateLockStatus @FileSharingConfig) - <@> mkNamedAPI @'("ipatch", FileSharingConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", ConferenceCallingConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", ConferenceCallingConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", ConferenceCallingConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", SelfDeletingMessagesConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", SelfDeletingMessagesConfig) setFeatureStatusInternal + <@> mkNamedAPI @'("ilock", ConferenceCallingConfig) (updateLockStatus @ConferenceCallingConfig) <@> mkNamedAPI @'("ilock", SelfDeletingMessagesConfig) (updateLockStatus @SelfDeletingMessagesConfig) - <@> mkNamedAPI @'("ipatch", SelfDeletingMessagesConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", GuestLinksConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", GuestLinksConfig) setFeatureStatusInternal <@> mkNamedAPI @'("ilock", GuestLinksConfig) (updateLockStatus @GuestLinksConfig) - <@> mkNamedAPI @'("ipatch", GuestLinksConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", SndFactorPasswordChallengeConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", SndFactorPasswordChallengeConfig) setFeatureStatusInternal <@> mkNamedAPI @'("ilock", SndFactorPasswordChallengeConfig) (updateLockStatus @SndFactorPasswordChallengeConfig) - <@> mkNamedAPI @'("ipatch", SndFactorPasswordChallengeConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", SearchVisibilityInboundConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", SearchVisibilityInboundConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", SearchVisibilityInboundConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("igetmulti", SearchVisibilityInboundConfig) getFeatureStatusMulti - <@> mkNamedAPI @'("iget", ClassifiedDomainsConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iget", MLSConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", MLSConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", MLSConfig) patchFeatureStatusInternal <@> mkNamedAPI @'("ilock", MLSConfig) (updateLockStatus @MLSConfig) - <@> mkNamedAPI @'("iget", ExposeInvitationURLsToTeamAdminConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", ExposeInvitationURLsToTeamAdminConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", ExposeInvitationURLsToTeamAdminConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", SearchVisibilityInboundConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", SearchVisibilityInboundConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", SearchVisibilityInboundConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", OutlookCalIntegrationConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", OutlookCalIntegrationConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", OutlookCalIntegrationConfig) patchFeatureStatusInternal <@> mkNamedAPI @'("ilock", OutlookCalIntegrationConfig) (updateLockStatus @OutlookCalIntegrationConfig) - <@> mkNamedAPI @'("iget", MlsE2EIdConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", MlsE2EIdConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", MlsE2EIdConfig) patchFeatureStatusInternal <@> mkNamedAPI @'("ilock", MlsE2EIdConfig) (updateLockStatus @MlsE2EIdConfig) - <@> mkNamedAPI @'("iget", MlsMigrationConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", MlsMigrationConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", MlsMigrationConfig) patchFeatureStatusInternal <@> mkNamedAPI @'("ilock", MlsMigrationConfig) (updateLockStatus @MlsMigrationConfig) - <@> mkNamedAPI @'("iget", EnforceFileDownloadLocationConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", EnforceFileDownloadLocationConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", EnforceFileDownloadLocationConfig) patchFeatureStatusInternal <@> mkNamedAPI @'("ilock", EnforceFileDownloadLocationConfig) (updateLockStatus @EnforceFileDownloadLocationConfig) - <@> mkNamedAPI @'("iget", LimitedEventFanoutConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", LimitedEventFanoutConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", LimitedEventFanoutConfig) patchFeatureStatusInternal - <@> mkNamedAPI @"feature-configs-internal" (maybe getAllFeatureConfigsForServer getAllFeatureConfigsForUser) + -- special endpoints + <@> mkNamedAPI @'("igetmulti", SearchVisibilityInboundConfig) getFeatureMulti + -- all features + <@> mkNamedAPI @"feature-configs-internal" (maybe getAllTeamFeaturesForServer getAllTeamFeaturesForUser) rmUser :: forall p1 p2 r. @@ -358,12 +341,12 @@ rmUser lusr conn = do leaveTeams page = for_ (pageItems page) $ \tid -> do toNotify <- handleImpossibleErrors $ - getFeatureStatus @LimitedEventFanoutConfig DontDoAuth tid + getFeatureForTeam @LimitedEventFanoutConfig tid >>= ( \case FeatureStatusEnabled -> Left <$> E.getTeamAdmins tid FeatureStatusDisabled -> Right <$> getTeamMembersForFanout tid ) - . wsStatus + . (.status) uncheckedDeleteTeamMember lusr conn tid (tUnqualified lusr) toNotify page' <- listTeams @p2 (tUnqualified lusr) (Just (pageState page)) maxBound leaveTeams page' diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index 75eceeec319..0f3b88bb7a9 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -34,7 +34,7 @@ import Brig.Types.Connection (UpdateConnectionsInternal (..)) import Brig.Types.Team.LegalHold (legalHoldService, viewLegalHoldService) import Control.Exception (assert) import Control.Lens (view, (^.)) -import Data.ByteString.Conversion (toByteString, toByteString') +import Data.ByteString.Conversion (toByteString) import Data.Id import Data.LegalHold (UserLegalHoldStatus (..), defUserLegalHoldStatus) import Data.List.Split (chunksOf) @@ -44,6 +44,7 @@ import Data.Qualified import Data.Range (toRange) import Data.Time.Clock import Galley.API.Error +import Galley.API.LegalHold.Get import Galley.API.LegalHold.Team import Galley.API.Query (iterateConversations) import Galley.API.Update (removeMemberFromLocalConv) @@ -67,6 +68,7 @@ import Polysemy.Input import Polysemy.TinyLog qualified as P import System.Logger.Class qualified as Log import Wire.API.Conversation (ConvType (..)) +import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley @@ -109,9 +111,9 @@ createSettings lzusr tid newService = do -- . Log.field "action" (Log.val "LegalHold.createSettings") void $ permissionCheck ChangeLegalHoldTeamSettings zusrMembership (key :: ServiceKey, fpr :: Fingerprint Rsa) <- - LegalHoldData.validateServiceKey (newLegalHoldServiceKey newService) + LegalHoldData.validateServiceKey newService.newLegalHoldServiceKey >>= noteS @'LegalHoldServiceInvalidKey - LHService.checkLegalHoldServiceStatus fpr (newLegalHoldServiceUrl newService) + LHService.checkLegalHoldServiceStatus fpr newService.newLegalHoldServiceUrl let service = legalHoldService tid fpr newService key LegalHoldData.createSettings service pure . viewLegalHoldService $ service @@ -169,7 +171,8 @@ removeSettingsInternalPaging :: Member SubConversationStore r, Member TeamFeatureStore r, Member (TeamMemberStore InternalPaging) r, - Member TeamStore r + Member TeamStore r, + Member (Embed IO) r ) => Local UserId -> TeamId -> @@ -211,7 +214,8 @@ removeSettings :: Member ProposalStore r, Member P.TinyLog r, Member Random r, - Member SubConversationStore r + Member SubConversationStore r, + Member (Embed IO) r ) => UserId -> TeamId -> @@ -266,7 +270,8 @@ removeSettings' :: Member ProposalStore r, Member Random r, Member P.TinyLog r, - Member SubConversationStore r + Member SubConversationStore r, + Member (Embed IO) r ) => TeamId -> Sem r () @@ -287,47 +292,9 @@ removeSettings' tid = removeLHForUser member = do luid <- qualifyLocal (member ^. userId) removeLegalHoldClientFromUser (tUnqualified luid) - LHService.removeLegalHold tid (tUnqualified luid) + LHService.removeLegalHold tid luid changeLegalholdStatusAndHandlePolicyConflicts tid luid (member ^. legalHoldStatus) UserLegalHoldDisabled -- (support for withdrawing consent is not planned yet.) --- | Learn whether a user has LH enabled and fetch pre-keys. --- Note that this is accessible to ANY authenticated user, even ones outside the team -getUserStatus :: - forall r. - ( Member (Error InternalError) r, - Member (ErrorS 'TeamMemberNotFound) r, - Member LegalHoldStore r, - Member TeamStore r, - Member P.TinyLog r - ) => - Local UserId -> - TeamId -> - UserId -> - Sem r Public.UserLegalHoldStatusResponse -getUserStatus _lzusr tid uid = do - teamMember <- noteS @'TeamMemberNotFound =<< getTeamMember tid uid - let status = view legalHoldStatus teamMember - (mlk, lcid) <- case status of - UserLegalHoldNoConsent -> pure (Nothing, Nothing) - UserLegalHoldDisabled -> pure (Nothing, Nothing) - UserLegalHoldPending -> makeResponseDetails - UserLegalHoldEnabled -> makeResponseDetails - pure $ UserLegalHoldStatusResponse status mlk lcid - where - makeResponseDetails :: Sem r (Maybe LastPrekey, Maybe ClientId) - makeResponseDetails = do - mLastKey <- fmap snd <$> LegalHoldData.selectPendingPrekeys uid - lastKey <- case mLastKey of - Nothing -> do - P.err . Log.msg $ - "expected to find a prekey for user: " - <> toByteString' uid - <> " but none was found" - throw NoPrekeyForUser - Just lstKey -> pure lstKey - let clientId = clientIdFromPrekey . unpackLastPrekey $ lastKey - pure (Just lastKey, Just clientId) - -- | Change 'UserLegalHoldStatus' from no consent to disabled. FUTUREWORK: -- @withdrawExplicitConsentH@ (lots of corner cases we'd have to implement for that to pan -- out). @@ -382,6 +349,7 @@ requestDevice :: Member (ErrorS 'LegalHoldNotEnabled) r, Member (ErrorS 'LegalHoldServiceBadResponse) r, Member (ErrorS 'LegalHoldServiceNotRegistered) r, + Member (ErrorS 'MLSLegalholdIncompatible) r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'NoUserLegalHoldConsent) r, Member (ErrorS OperationDenied) r, @@ -402,7 +370,8 @@ requestDevice :: Member Random r, Member SubConversationStore r, Member TeamFeatureStore r, - Member TeamStore r + Member TeamStore r, + Member (Embed IO) r ) => Local UserId -> TeamId -> @@ -429,6 +398,12 @@ requestDevice lzusr tid uid = do lhs@UserLegalHoldDisabled -> RequestDeviceSuccess <$ provisionLHDevice zusr luid lhs UserLegalHoldNoConsent -> throwS @'NoUserLegalHoldConsent where + disallowIfMLSUser :: Local UserId -> Sem r () + disallowIfMLSUser luid = do + void $ iterateConversations luid (toRange (Proxy @500)) $ \convs -> do + when (any (\c -> c.convProtocol /= ProtocolProteus) convs) $ do + throwS @'MLSLegalholdIncompatible + -- Wire's LH service that galley is usually calling here is idempotent in device creation, -- ie. it returns the existing device on multiple calls to `/init`, like here: -- https://github.com/wireapp/legalhold/blob/e0a241162b9dbc841f12fbc57c8a1e1093c7e83a/src/main/java/com/wire/bots/hold/resource/InitiateResource.java#L42 @@ -438,6 +413,7 @@ requestDevice lzusr tid uid = do -- device at (almost) the same time. provisionLHDevice :: UserId -> Local UserId -> UserLegalHoldStatus -> Sem r () provisionLHDevice zusr luid userLHStatus = do + disallowIfMLSUser luid (lastPrekey', prekeys) <- requestDeviceFromService luid -- We don't distinguish the last key here; brig will do so when the device is added LegalHoldData.insertPendingPrekeys (tUnqualified luid) (unpackLastPrekey lastPrekey' : prekeys) @@ -447,7 +423,7 @@ requestDevice lzusr tid uid = do requestDeviceFromService :: Local UserId -> Sem r (LastPrekey, [Prekey]) requestDeviceFromService luid = do LegalHoldData.dropPendingPrekeys (tUnqualified luid) - lhDevice <- LHService.requestNewDevice tid (tUnqualified luid) + lhDevice <- LHService.requestNewDevice tid luid let NewLegalHoldClient prekeys lastKey = lhDevice pure (lastKey, prekeys) @@ -488,7 +464,8 @@ approveDevice :: Member Random r, Member SubConversationStore r, Member TeamFeatureStore r, - Member TeamStore r + Member TeamStore r, + Member (Embed IO) r ) => Local UserId -> ConnId -> @@ -522,7 +499,7 @@ approveDevice lzusr connId tid uid (Public.ApproveLegalHoldForUserRequest mPassw -- checks that the user is part of a binding team -- FUTUREWORK: reduce double checks legalHoldAuthToken <- getLegalHoldAuthToken (tUnqualified luid) mPassword - LHService.confirmLegalHold clientId tid (tUnqualified luid) legalHoldAuthToken + LHService.confirmLegalHold clientId tid luid legalHoldAuthToken -- TODO: send event at this point (see also: -- https://github.com/wireapp/wire-server/pull/802#pullrequestreview-262280386) changeLegalholdStatusAndHandlePolicyConflicts tid luid userLHStatus UserLegalHoldEnabled @@ -564,7 +541,8 @@ disableForUser :: Member P.TinyLog r, Member Random r, Member SubConversationStore r, - Member TeamStore r + Member TeamStore r, + Member (Embed IO) r ) => Local UserId -> TeamId -> @@ -598,7 +576,7 @@ disableForUser lzusr tid uid (Public.DisableLegalHoldForUserRequest mPassword) = disableLH zusr luid userLHStatus = do ensureReAuthorised zusr mPassword Nothing Nothing removeLegalHoldClientFromUser uid - LHService.removeLegalHold tid uid + LHService.removeLegalHold tid luid -- TODO: send event at this point (see also: related TODO in this module in -- 'approveDevice' and -- https://github.com/wireapp/wire-server/pull/802#pullrequestreview-262280386) diff --git a/services/galley/src/Galley/API/LegalHold/Conflicts.hs b/services/galley/src/Galley/API/LegalHold/Conflicts.hs index e70ffff0f3d..1503e390fe4 100644 --- a/services/galley/src/Galley/API/LegalHold/Conflicts.hs +++ b/services/galley/src/Galley/API/LegalHold/Conflicts.hs @@ -24,7 +24,7 @@ module Galley.API.LegalHold.Conflicts ) where -import Control.Lens (view, (^.)) +import Control.Lens (to, view, (^.)) import Data.ByteString.Conversion (toByteString') import Data.Id import Data.LegalHold (UserLegalHoldStatus (..)) @@ -44,6 +44,7 @@ import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog qualified as P import System.Logger.Class qualified as Log +import Wire.API.Team.Feature import Wire.API.Team.LegalHold import Wire.API.Team.Member import Wire.API.User @@ -92,7 +93,7 @@ guardLegalholdPolicyConflicts LegalholdPlusFederationNotImplemented _otherClient guardLegalholdPolicyConflicts UnprotectedBot _otherClients = pure () guardLegalholdPolicyConflicts (ProtectedUser self) otherClients = do opts <- input - case view (settings . featureFlags . flagLegalHold) opts of + case view (settings . featureFlags . to npProject) opts of FeatureLegalHoldDisabledPermanently -> case FutureWork @'LegalholdPlusFederationNotImplemented () of FutureWork () -> -- FUTUREWORK: if federation is enabled, we still need to run the guard! @@ -127,7 +128,7 @@ guardLegalholdPolicyConflictsUid self (Map.keys . userClients -> otherUids) = do checkAnyConsentMissing :: Sem r Bool checkAnyConsentMissing = do - users :: [User] <- accountUser <$$> getUsers (self : otherUids) + users <- getUsers (self : otherUids) -- NB: `users` can't be empty! let checkUserConsentMissing :: User -> Sem r Bool checkUserConsentMissing user = diff --git a/services/galley/src/Galley/API/LegalHold/Get.hs b/services/galley/src/Galley/API/LegalHold/Get.hs new file mode 100644 index 00000000000..3607c040060 --- /dev/null +++ b/services/galley/src/Galley/API/LegalHold/Get.hs @@ -0,0 +1,78 @@ +-- 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.LegalHold.Get (getUserStatus) where + +import Control.Lens (view) +import Data.ByteString.Conversion (toByteString') +import Data.Id +import Data.LegalHold (UserLegalHoldStatus (..)) +import Data.Qualified +import Galley.API.Error +import Galley.Effects +import Galley.Effects.LegalHoldStore qualified as LegalHoldData +import Galley.Effects.TeamStore +import Imports +import Polysemy +import Polysemy.Error +import Polysemy.TinyLog qualified as P +import System.Logger.Class qualified as Log +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.Team.LegalHold +import Wire.API.Team.LegalHold qualified as Public +import Wire.API.Team.Member +import Wire.API.User.Client.Prekey + +-- | Learn whether a user has LH enabled and fetch pre-keys. +-- Note that this is accessible to ANY authenticated user, even ones outside the team +getUserStatus :: + forall r. + ( Member (Error InternalError) r, + Member (ErrorS 'TeamMemberNotFound) r, + Member LegalHoldStore r, + Member TeamStore r, + Member P.TinyLog r + ) => + Local UserId -> + TeamId -> + UserId -> + Sem r Public.UserLegalHoldStatusResponse +getUserStatus _lzusr tid uid = do + teamMember <- noteS @'TeamMemberNotFound =<< getTeamMember tid uid + let status = view legalHoldStatus teamMember + (mlk, lcid) <- case status of + UserLegalHoldNoConsent -> pure (Nothing, Nothing) + UserLegalHoldDisabled -> pure (Nothing, Nothing) + UserLegalHoldPending -> makeResponseDetails + UserLegalHoldEnabled -> makeResponseDetails + pure $ UserLegalHoldStatusResponse status mlk lcid + where + makeResponseDetails :: Sem r (Maybe LastPrekey, Maybe ClientId) + makeResponseDetails = do + mLastKey <- fmap snd <$> LegalHoldData.selectPendingPrekeys uid + lastKey <- case mLastKey of + Nothing -> do + P.err + . Log.msg + $ "expected to find a prekey for user: " + <> toByteString' uid + <> " but none was found" + throw NoPrekeyForUser + Just lstKey -> pure lstKey + let clientId = clientIdFromPrekey . unpackLastPrekey $ lastKey + pure (Just lastKey, Just clientId) diff --git a/services/galley/src/Galley/API/LegalHold/Team.hs b/services/galley/src/Galley/API/LegalHold/Team.hs index 6fbc8f3bfd6..a8851f2f83f 100644 --- a/services/galley/src/Galley/API/LegalHold/Team.hs +++ b/services/galley/src/Galley/API/LegalHold/Team.hs @@ -17,25 +17,27 @@ module Galley.API.LegalHold.Team ( isLegalHoldEnabledForTeam, + computeLegalHoldFeatureStatus, assertLegalHoldEnabledForTeam, ensureNotTooLargeToActivateLegalHold, teamSizeBelowLimit, ) where +import Data.Default import Data.Id import Data.Range import Galley.Effects import Galley.Effects.BrigAccess import Galley.Effects.LegalHoldStore qualified as LegalHoldData -import Galley.Effects.TeamFeatureStore qualified as TeamFeatures +import Galley.Effects.TeamFeatureStore import Galley.Effects.TeamStore import Galley.Types.Teams as Team import Imports import Polysemy import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.Team.Feature qualified as Public +import Wire.API.Team.Feature import Wire.API.Team.Size assertLegalHoldEnabledForTeam :: @@ -51,6 +53,22 @@ assertLegalHoldEnabledForTeam tid = unlessM (isLegalHoldEnabledForTeam tid) $ throwS @'LegalHoldNotEnabled +computeLegalHoldFeatureStatus :: + ( Member TeamStore r, + Member LegalHoldStore r + ) => + TeamId -> + DbFeature LegalholdConfig -> + Sem r FeatureStatus +computeLegalHoldFeatureStatus tid dbFeature = + getLegalHoldFlag >>= \case + FeatureLegalHoldDisabledPermanently -> pure FeatureStatusDisabled + FeatureLegalHoldDisabledByDefault -> + pure (applyDbFeature dbFeature def).status + FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> do + wl <- LegalHoldData.isTeamLegalholdWhitelisted tid + pure $ if wl then FeatureStatusEnabled else FeatureStatusDisabled + isLegalHoldEnabledForTeam :: forall r. ( Member LegalHoldStore r, @@ -60,18 +78,9 @@ isLegalHoldEnabledForTeam :: TeamId -> Sem r Bool isLegalHoldEnabledForTeam tid = do - getLegalHoldFlag >>= \case - FeatureLegalHoldDisabledPermanently -> do - pure False - FeatureLegalHoldDisabledByDefault -> do - statusValue <- - Public.wssStatus <$$> TeamFeatures.getFeatureConfig Public.FeatureSingletonLegalholdConfig tid - pure $ case statusValue of - Just Public.FeatureStatusEnabled -> True - Just Public.FeatureStatusDisabled -> False - Nothing -> False - FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> - LegalHoldData.isTeamLegalholdWhitelisted tid + dbFeature <- getDbFeature tid + status <- computeLegalHoldFeatureStatus tid dbFeature + pure $ status == FeatureStatusEnabled ensureNotTooLargeToActivateLegalHold :: ( Member BrigAccess r, diff --git a/services/galley/src/Galley/API/MLS.hs b/services/galley/src/Galley/API/MLS.hs index ed2b2afbd17..7a83ac92146 100644 --- a/services/galley/src/Galley/API/MLS.hs +++ b/services/galley/src/Galley/API/MLS.hs @@ -22,12 +22,11 @@ module Galley.API.MLS postMLSCommitBundleFromLocalUser, postMLSMessageFromLocalUser, getMLSPublicKeys, - getMLSPublicKeysJWK, + formatPublicKeys, ) where -import Data.Id -import Data.Qualified +import Data.Default import Galley.API.Error import Galley.API.MLS.Enabled import Galley.API.MLS.Message @@ -42,17 +41,28 @@ import Wire.API.MLS.Keys getMLSPublicKeys :: ( Member (Input Env) r, - Member (ErrorS 'MLSNotEnabled) r + Member (ErrorS 'MLSNotEnabled) r, + Member (Error InternalError) r ) => - Local UserId -> - Sem r (MLSKeysByPurpose MLSPublicKeys) -getMLSPublicKeys _ = mlsKeysToPublic <$$> getMLSPrivateKeys + Maybe MLSPublicKeyFormat -> + Sem r (MLSKeysByPurpose (MLSKeys SomeKey)) +getMLSPublicKeys fmt = do + publicKeys <- mlsKeysToPublic <$$> getMLSPrivateKeys + formatPublicKeys fmt publicKeys -getMLSPublicKeysJWK :: - ( Member (Input Env) r, - Member (Error InternalError) r, - Member (ErrorS 'MLSNotEnabled) r - ) => - Local UserId -> - Sem r (MLSKeysByPurpose MLSPublicKeysJWK) -getMLSPublicKeysJWK _ = mapM (note (InternalErrorWithDescription "malformed MLS removal keys") . mlsKeysToPublicJWK) =<< getMLSPrivateKeys +formatPublicKeys :: + (Member (Error InternalError) r) => + Maybe MLSPublicKeyFormat -> + MLSKeysByPurpose MLSPublicKeys -> + Sem r (MLSKeysByPurpose (MLSKeys SomeKey)) +formatPublicKeys fmt publicKeys = + case fromMaybe def fmt of + MLSPublicKeyFormatRaw -> pure (fmap (fmap mkSomeKey) publicKeys) + MLSPublicKeyFormatJWK -> do + jwks <- + traverse + ( note (InternalErrorWithDescription "malformed MLS removal keys") + . mlsPublicKeysToJWK + ) + publicKeys + pure $ fmap (fmap mkSomeKey) jwks diff --git a/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs index 7a3a815b950..8e67a0d9e4d 100644 --- a/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs @@ -32,7 +32,9 @@ import Galley.API.MLS.Removal import Galley.API.MLS.Types import Galley.API.MLS.Util import Galley.Effects +import Galley.Effects.ConversationStore import Galley.Effects.MemberStore +import Galley.Effects.SubConversationStore import Imports import Polysemy import Polysemy.Error @@ -135,11 +137,12 @@ processExternalCommit :: ClientIdentity -> Local ConvOrSubConv -> CipherSuiteTag -> + Bool -> Epoch -> ExternalCommitAction -> Maybe UpdatePath -> Sem r () -processExternalCommit senderIdentity lConvOrSub ciphersuite epoch action updatePath = do +processExternalCommit senderIdentity lConvOrSub ciphersuite ciphersuiteUpdate epoch action updatePath = do let convOrSub = tUnqualified lConvOrSub -- only members can join a subconversation @@ -173,6 +176,11 @@ processExternalCommit senderIdentity lConvOrSub ciphersuite epoch action updateP -- skip proposals for clients already removed by the external commit let indices = maybe id Set.delete action.remove indices0 + -- set cipher suite + when ciphersuiteUpdate $ case convOrSub.id of + Conv cid -> setConversationCipherSuite cid ciphersuite + SubConv cid sub -> setSubConversationCipherSuite cid sub ciphersuite + -- requeue backend remove proposals for the current epoch createAndSendRemoveProposals lConvOrSub' diff --git a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs index f0b71cb216f..6b466db99ff 100644 --- a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs @@ -39,6 +39,7 @@ import Galley.API.Util import Galley.Data.Conversation.Types hiding (Conversation) import Galley.Data.Conversation.Types qualified as Data import Galley.Effects +import Galley.Effects.ConversationStore import Galley.Effects.MemberStore import Galley.Effects.ProposalStore import Galley.Effects.SubConversationStore @@ -78,11 +79,12 @@ processInternalCommit :: Maybe ConnId -> Local ConvOrSubConv -> CipherSuiteTag -> + Bool -> Epoch -> ProposalAction -> Commit -> Sem r [LocalConversationUpdate] -processInternalCommit senderIdentity con lConvOrSub ciphersuite epoch action commit = do +processInternalCommit senderIdentity con lConvOrSub ciphersuite ciphersuiteUpdate epoch action commit = do let convOrSub = tUnqualified lConvOrSub qusr = cidQualifiedUser senderIdentity cm = convOrSub.members @@ -261,6 +263,11 @@ processInternalCommit senderIdentity con lConvOrSub ciphersuite epoch action com for_ newUserClients $ \(qtarget, newClients) -> do addMLSClients (cnvmlsGroupId convOrSub.mlsMeta) qtarget (Set.fromList (Map.assocs newClients)) + -- set cipher suite + when ciphersuiteUpdate $ case convOrSub.id of + Conv cid -> setConversationCipherSuite cid ciphersuite + SubConv cid sub -> setSubConversationCipherSuite cid sub ciphersuite + -- increment epoch number for_ lConvOrSub incrementEpoch diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 8451e05019c..0478b06ad83 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -29,16 +29,18 @@ module Galley.API.MLS.Message ) where -import Control.Comonad import Data.Domain import Data.Id import Data.Json.Util +import Data.LegalHold import Data.Qualified import Data.Set qualified as Set +import Data.Tagged import Data.Text.Lazy qualified as LT import Data.Tuple.Extra import Galley.API.Action import Galley.API.Error +import Galley.API.LegalHold.Get (getUserStatus) import Galley.API.MLS.Commit.Core (getCommitData) import Galley.API.MLS.Commit.ExternalCommit import Galley.API.MLS.Commit.InternalCommit @@ -58,6 +60,7 @@ import Galley.Effects.ConversationStore import Galley.Effects.FederatorAccess import Galley.Effects.MemberStore import Galley.Effects.SubConversationStore +import Galley.Effects.TeamStore qualified as TeamStore import Imports import Polysemy import Polysemy.Error @@ -81,6 +84,7 @@ import Wire.API.MLS.GroupInfo import Wire.API.MLS.Message import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation +import Wire.API.Team.LegalHold import Wire.NotificationSubsystem -- FUTUREWORK @@ -146,11 +150,12 @@ postMLSMessageFromLocalUser lusr c conn smsg = do pure $ MLSMessageSendingStatus events t postMLSCommitBundle :: - ( HasProposalEffects r, - Members MLSBundleStaticErrors r, + ( Member (ErrorS MLSLegalholdIncompatible) r, Member Random r, Member Resource r, - Member SubConversationStore r + Member SubConversationStore r, + Members MLSBundleStaticErrors r, + HasProposalEffects r ) => Local x -> Qualified UserId -> @@ -168,11 +173,12 @@ postMLSCommitBundle loc qusr c ctype qConvOrSub conn bundle = qConvOrSub postMLSCommitBundleFromLocalUser :: - ( HasProposalEffects r, - Members MLSBundleStaticErrors r, + ( Member (ErrorS MLSLegalholdIncompatible) r, Member Random r, Member Resource r, - Member SubConversationStore r + Member SubConversationStore r, + Members MLSBundleStaticErrors r, + HasProposalEffects r ) => Local UserId -> ClientId -> @@ -190,11 +196,12 @@ postMLSCommitBundleFromLocalUser lusr c conn bundle = do pure $ MLSMessageSendingStatus events t postMLSCommitBundleToLocalConv :: - ( HasProposalEffects r, - Members MLSBundleStaticErrors r, + ( Member (ErrorS MLSLegalholdIncompatible) r, + Member Random r, Member Resource r, Member SubConversationStore r, - Member Random r + Members MLSBundleStaticErrors r, + HasProposalEffects r ) => Qualified UserId -> ClientId -> @@ -211,18 +218,38 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do note (mlsProtocolError "Unsupported ciphersuite") $ cipherSuiteTag bundle.groupInfo.value.groupContext.cipherSuite - case convOrSub.mlsMeta.cnvmlsActiveData of + -- when a user tries to join any mls conversation while being under legalhold + -- they receive a 409 stating that mls and legalhold are incompatible + case qusr `relativeTo` lConvOrSubId of + Local luid -> + when (isNothing convOrSub.mlsMeta.cnvmlsActiveData) do + usrTeams <- TeamStore.getUserTeams (tUnqualified luid) + for_ usrTeams \tid -> do + -- this would only return 'Left' if the team member did vanish directly in the process of this + -- request or if the legalhold state was somehow inconsistent. We can safely assume that this + -- should be a server error + resp <- runError @(Tagged TeamMemberNotFound ()) $ getUserStatus luid tid (tUnqualified luid) + case resp of + Left _ -> throw $ InternalErrorWithDescription "Server error. Team member must have vanished with the legal hold check" + Right r -> case r.ulhsrStatus of + UserLegalHoldPending -> throwS @MLSLegalholdIncompatible + UserLegalHoldEnabled -> throwS @MLSLegalholdIncompatible + UserLegalHoldDisabled -> pure () + UserLegalHoldNoConsent -> pure () + + -- we can skip the remote case because we currently to not support creating conversations on the remote backend + Remote _ -> pure () + + ciphersuiteUpdate <- case convOrSub.mlsMeta.cnvmlsActiveData of -- if this is the first commit of the conversation, update ciphersuite - Nothing -> do - case convOrSub.id of - Conv cid -> setConversationCipherSuite cid ciphersuite - SubConv cid sub -> setSubConversationCipherSuite cid sub ciphersuite + Nothing -> pure True -- otherwise, make sure the ciphersuite matches Just activeData -> do unless (ciphersuite == activeData.ciphersuite) $ throw $ mlsProtocolError "GroupInfo ciphersuite does not match conversation" unless (bundle.epoch == activeData.epoch) $ throwS @'MLSStaleMessage + pure False senderIdentity <- getSenderIdentity qusr c bundle.sender lConvOrSub @@ -238,6 +265,7 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do conn lConvOrSub ciphersuite + ciphersuiteUpdate bundle.epoch action bundle.commit.value @@ -253,6 +281,7 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do senderIdentity lConvOrSub ciphersuite + ciphersuiteUpdate bundle.epoch action bundle.commit.value.path diff --git a/services/galley/src/Galley/API/MLS/Migration.hs b/services/galley/src/Galley/API/MLS/Migration.hs index 747de458cd4..2ecf68ed54f 100644 --- a/services/galley/src/Galley/API/MLS/Migration.hs +++ b/services/galley/src/Galley/API/MLS/Migration.hs @@ -52,22 +52,20 @@ checkMigrationCriteria :: ) => UTCTime -> MLSConversation -> - WithStatus MlsMigrationConfig -> + LockableFeature MlsMigrationConfig -> Sem r Bool checkMigrationCriteria now conv ws - | wsStatus ws == FeatureStatusDisabled = pure False + | ws.status == FeatureStatusDisabled = pure False | afterDeadline = pure True | otherwise = unApAll $ mconcat [localUsersMigrated, remoteUsersMigrated] where - mig = wsConfig ws - afterDeadline = maybe False (now >=) mig.finaliseRegardlessAfter + afterDeadline = maybe False (now >=) ws.config.finaliseRegardlessAfter containsMLS = Set.member BaseProtocolMLSTag localUsersMigrated = ApAll $ do localProfiles <- - map accountUser - <$> getUsers (map lmId conv.mcLocalMembers) + getUsers (map lmId conv.mcLocalMembers) pure $ all (containsMLS . userSupportedProtocols) localProfiles remoteUsersMigrated = ApAll $ do diff --git a/services/galley/src/Galley/API/MLS/One2One.hs b/services/galley/src/Galley/API/MLS/One2One.hs index 00dd1a534de..6aae0c69bb2 100644 --- a/services/galley/src/Galley/API/MLS/One2One.hs +++ b/services/galley/src/Galley/API/MLS/One2One.hs @@ -37,6 +37,7 @@ import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Federation.API.Galley import Wire.API.MLS.Group.Serialisation +import Wire.API.MLS.Keys import Wire.API.MLS.SubConversation import Wire.API.User @@ -64,7 +65,7 @@ localMLSOne2OneConversation lself (tUntagged -> convId) = -- conversation to be returned to a remote backend. localMLSOne2OneConversationAsRemote :: Local ConvId -> - RemoteConversation + RemoteConversationV2 localMLSOne2OneConversationAsRemote lcnv = let members = RemoteConvMembers @@ -72,7 +73,7 @@ localMLSOne2OneConversationAsRemote lcnv = others = [] } (metadata, mlsData) = localMLSOne2OneConversationMetadata (tUntagged lcnv) - in RemoteConversation + in RemoteConversationV2 { id = tUnqualified lcnv, metadata = metadata, members = members, @@ -100,19 +101,24 @@ localMLSOne2OneConversationMetadata convId = remoteMLSOne2OneConversation :: Local UserId -> Remote UserId -> - RemoteConversation -> - Conversation + RemoteMLSOne2OneConversation -> + (MLSOne2OneConversation MLSPublicKey) remoteMLSOne2OneConversation lself rother rc = let members = ConvMembers { cmSelf = defMember (tUntagged lself), - cmOthers = rc.members.others + cmOthers = rc.conversation.members.others } - in Conversation - { cnvQualifiedId = tUntagged (qualifyAs rother rc.id), - cnvMetadata = rc.metadata, - cnvMembers = members, - cnvProtocol = rc.protocol + conv = + Conversation + { cnvQualifiedId = tUntagged (qualifyAs rother rc.conversation.id), + cnvMetadata = rc.conversation.metadata, + cnvMembers = members, + cnvProtocol = rc.conversation.protocol + } + in MLSOne2OneConversation + { conversation = conv, + publicKeys = rc.publicKeys } -- | Create a new record for an MLS 1-1 conversation in the database and add diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index b0fe16e6c8c..d256ab23cec 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -31,7 +31,6 @@ import Galley.Data.Services import Galley.Effects import Galley.Effects.BackendNotificationQueueAccess import Galley.Types.Conversations.Members -import Gundeck.Types.Push.V2 (RecipientClients (..)) import Imports import Network.AMQP qualified as Q import Polysemy @@ -47,6 +46,7 @@ import Wire.API.MLS.Message import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.API.Message +import Wire.API.Push.V2 (RecipientClients (..)) import Wire.NotificationSubsystem -- | Propagate a message. diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index c5c57889e1a..781cacc7769 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -115,7 +115,7 @@ getLocalSubConversation :: getLocalSubConversation qusr lconv sconv = do c <- getConversationAndCheckMembership qusr lconv - unless (Data.convType c == RegularConv) $ + unless (Data.convType c == RegularConv || Data.convType c == One2OneConv) $ throwS @'MLSSubConvUnsupportedConvType msub <- Eff.getSubConversation (tUnqualified lconv) sconv diff --git a/services/galley/src/Galley/API/MLS/Types.hs b/services/galley/src/Galley/API/MLS/Types.hs index 3274956f1bd..76745094c1f 100644 --- a/services/galley/src/Galley/API/MLS/Types.hs +++ b/services/galley/src/Galley/API/MLS/Types.hs @@ -207,9 +207,3 @@ instance HasField "id" ConvOrSubConv ConvOrSubConvId where instance HasField "migrationState" ConvOrSubConv MLSMigrationState where getField (Conv c) = c.mcMigrationState getField (SubConv _ _) = MLSMigrationMLS - -convOrSubConvActivate :: ActiveMLSConversationData -> ConvOrSubConv -> ConvOrSubConv -convOrSubConvActivate activeData (Conv c) = - Conv $ c {mcMLSData = (mcMLSData c) {cnvmlsActiveData = Just activeData}} -convOrSubConvActivate activeData (SubConv c s) = - SubConv c $ s {scMLSData = (scMLSData s) {cnvmlsActiveData = Just activeData}} diff --git a/services/galley/src/Galley/API/MLS/Welcome.hs b/services/galley/src/Galley/API/MLS/Welcome.hs index c9a182d890b..c661a9dc5f5 100644 --- a/services/galley/src/Galley/API/MLS/Welcome.hs +++ b/services/galley/src/Galley/API/MLS/Welcome.hs @@ -33,7 +33,6 @@ import Data.Time import Galley.API.Push import Galley.Effects.ExternalAccess import Galley.Effects.FederatorAccess -import Gundeck.Types.Push.V2 (RecipientClients (..)) import Imports import Network.Wai.Utilities.JSONResponse import Polysemy @@ -52,6 +51,7 @@ import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.API.MLS.Welcome import Wire.API.Message +import Wire.API.Push.V2 (RecipientClients (..)) import Wire.NotificationSubsystem sendWelcomes :: diff --git a/services/galley/src/Galley/API/Mapping.hs b/services/galley/src/Galley/API/Mapping.hs index ec6f0993e12..5a9d7615a6b 100644 --- a/services/galley/src/Galley/API/Mapping.hs +++ b/services/galley/src/Galley/API/Mapping.hs @@ -97,7 +97,7 @@ conversationViewMaybe luid remoteOthers localOthers conv = do remoteConversationView :: Local UserId -> MemberStatus -> - Remote RemoteConversation -> + Remote RemoteConversationV2 -> Conversation remoteConversationView uid status (tUntagged -> Qualified rconv rDomain) = let mems = rconv.members @@ -125,7 +125,7 @@ conversationToRemote :: Domain -> Remote UserId -> Data.Conversation -> - Maybe RemoteConversation + Maybe RemoteConversationV2 conversationToRemote localDomain ruid conv = do let (selfs, rothers) = partition ((== ruid) . rmId) (Data.convRemoteMembers conv) lothers = Data.convLocalMembers conv @@ -134,7 +134,7 @@ conversationToRemote localDomain ruid conv = do map (localMemberToOther localDomain) lothers <> map remoteMemberToOther rothers pure $ - RemoteConversation + RemoteConversationV2 { id = Data.convId conv, metadata = Data.convMetadata conv, members = diff --git a/services/galley/src/Galley/API/Public/Bot.hs b/services/galley/src/Galley/API/Public/Bot.hs index 6a7dc0bd138..ad839a6f24f 100644 --- a/services/galley/src/Galley/API/Public/Bot.hs +++ b/services/galley/src/Galley/API/Public/Bot.hs @@ -26,20 +26,18 @@ import Galley.App import Galley.Effects import Galley.Effects qualified as E import Galley.Options -import Imports hiding (head) import Polysemy import Polysemy.Input import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Event.Team qualified as Public () -import Wire.API.Federation.API import Wire.API.Provider.Bot import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.Bot botAPI :: API BotAPI GalleyEffects botAPI = - mkNamedAPI @"post-bot-message-unqualified" (callsFed (exposeAnnotations postBotMessageUnqualified)) + mkNamedAPI @"post-bot-message-unqualified" postBotMessageUnqualified <@> mkNamedAPI @"get-bot-conversation" getBotConversation getBotConversation :: @@ -50,14 +48,11 @@ getBotConversation :: Member TeamFeatureStore r, Member (ErrorS 'AccessDenied) r, Member (ErrorS 'ConvNotFound) r, - Member (ErrorS OperationDenied) r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'TeamNotFound) r, Member TeamStore r ) => BotId -> ConvId -> Sem r BotConvView -getBotConversation bid cnv = - Features.guardSecondFactorDisabled (botUserId bid) cnv $ - Query.getBotConversation bid cnv +getBotConversation bid cnv = do + Features.guardSecondFactorDisabled (botUserId bid) cnv + Query.getBotConversation bid cnv diff --git a/services/galley/src/Galley/API/Public/Conversation.hs b/services/galley/src/Galley/API/Public/Conversation.hs index 487e6893c85..532d4f4dffa 100644 --- a/services/galley/src/Galley/API/Public/Conversation.hs +++ b/services/galley/src/Galley/API/Public/Conversation.hs @@ -25,7 +25,6 @@ import Galley.API.Query import Galley.API.Update import Galley.App import Imports -import Wire.API.Federation.API import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.Conversation @@ -33,64 +32,66 @@ conversationAPI :: API ConversationAPI GalleyEffects conversationAPI = mkNamedAPI @"get-unqualified-conversation" getUnqualifiedConversation <@> mkNamedAPI @"get-unqualified-conversation-legalhold-alias" getUnqualifiedConversation - <@> mkNamedAPI @"get-conversation@v2" (callsFed (exposeAnnotations getConversation)) - <@> mkNamedAPI @"get-conversation@v5" (callsFed (exposeAnnotations getConversation)) - <@> mkNamedAPI @"get-conversation" (callsFed (exposeAnnotations getConversation)) + <@> mkNamedAPI @"get-conversation@v2" getConversation + <@> mkNamedAPI @"get-conversation@v5" getConversation + <@> mkNamedAPI @"get-conversation" getConversation <@> mkNamedAPI @"get-conversation-roles" getConversationRoles - <@> mkNamedAPI @"get-group-info" (callsFed (exposeAnnotations getGroupInfo)) + <@> mkNamedAPI @"get-group-info" getGroupInfo <@> mkNamedAPI @"list-conversation-ids-unqualified" conversationIdsPageFromUnqualified <@> mkNamedAPI @"list-conversation-ids-v2" (conversationIdsPageFromV2 DoNotListGlobalSelf) <@> mkNamedAPI @"list-conversation-ids" conversationIdsPageFrom <@> mkNamedAPI @"get-conversations" getConversations - <@> mkNamedAPI @"list-conversations@v1" (callsFed (exposeAnnotations listConversations)) - <@> mkNamedAPI @"list-conversations@v2" (callsFed (exposeAnnotations listConversations)) - <@> mkNamedAPI @"list-conversations@v5" (callsFed (exposeAnnotations listConversations)) - <@> mkNamedAPI @"list-conversations" (callsFed (exposeAnnotations listConversations)) + <@> mkNamedAPI @"list-conversations@v1" listConversations + <@> mkNamedAPI @"list-conversations@v2" listConversations + <@> mkNamedAPI @"list-conversations@v5" listConversations + <@> mkNamedAPI @"list-conversations" listConversations <@> mkNamedAPI @"get-conversation-by-reusable-code" getConversationByReusableCode - <@> mkNamedAPI @"create-group-conversation@v2" (callsFed (exposeAnnotations createGroupConversationUpToV3)) - <@> mkNamedAPI @"create-group-conversation@v3" (callsFed (exposeAnnotations createGroupConversationUpToV3)) - <@> mkNamedAPI @"create-group-conversation@v5" (callsFed (exposeAnnotations createGroupConversation)) - <@> mkNamedAPI @"create-group-conversation" (callsFed (exposeAnnotations createGroupConversation)) + <@> mkNamedAPI @"create-group-conversation@v2" createGroupConversationUpToV3 + <@> mkNamedAPI @"create-group-conversation@v3" createGroupConversationUpToV3 + <@> mkNamedAPI @"create-group-conversation@v5" createGroupConversation + <@> mkNamedAPI @"create-group-conversation" createGroupConversation <@> mkNamedAPI @"create-self-conversation@v2" createProteusSelfConversation <@> mkNamedAPI @"create-self-conversation@v5" createProteusSelfConversation <@> mkNamedAPI @"create-self-conversation" createProteusSelfConversation <@> mkNamedAPI @"get-mls-self-conversation@v5" getMLSSelfConversationWithError <@> mkNamedAPI @"get-mls-self-conversation" getMLSSelfConversationWithError - <@> mkNamedAPI @"get-subconversation" (callsFed getSubConversation) - <@> mkNamedAPI @"leave-subconversation" (callsFed leaveSubConversation) - <@> mkNamedAPI @"delete-subconversation" (callsFed deleteSubConversation) - <@> mkNamedAPI @"get-subconversation-group-info" (callsFed getSubConversationGroupInfo) - <@> mkNamedAPI @"create-one-to-one-conversation@v2" (callsFed createOne2OneConversation) - <@> mkNamedAPI @"create-one-to-one-conversation" (callsFed createOne2OneConversation) - <@> mkNamedAPI @"get-one-to-one-mls-conversation@v5" getMLSOne2OneConversation + <@> mkNamedAPI @"get-subconversation" getSubConversation + <@> mkNamedAPI @"leave-subconversation" leaveSubConversation + <@> mkNamedAPI @"delete-subconversation" deleteSubConversation + <@> mkNamedAPI @"get-subconversation-group-info" getSubConversationGroupInfo + <@> mkNamedAPI @"create-one-to-one-conversation@v2" createOne2OneConversation + <@> mkNamedAPI @"create-one-to-one-conversation@v6" createOne2OneConversation + <@> mkNamedAPI @"create-one-to-one-conversation" createOne2OneConversation + <@> mkNamedAPI @"get-one-to-one-mls-conversation@v5" getMLSOne2OneConversationV5 + <@> mkNamedAPI @"get-one-to-one-mls-conversation@v6" getMLSOne2OneConversationV6 <@> mkNamedAPI @"get-one-to-one-mls-conversation" getMLSOne2OneConversation - <@> mkNamedAPI @"add-members-to-conversation-unqualified" (callsFed addMembersUnqualified) - <@> mkNamedAPI @"add-members-to-conversation-unqualified2" (callsFed addMembersUnqualifiedV2) - <@> mkNamedAPI @"add-members-to-conversation" (callsFed addMembers) - <@> mkNamedAPI @"join-conversation-by-id-unqualified" (callsFed joinConversationById) - <@> mkNamedAPI @"join-conversation-by-code-unqualified" (callsFed joinConversationByReusableCode) + <@> mkNamedAPI @"add-members-to-conversation-unqualified" addMembersUnqualified + <@> mkNamedAPI @"add-members-to-conversation-unqualified2" addMembersUnqualifiedV2 + <@> mkNamedAPI @"add-members-to-conversation" addMembers + <@> mkNamedAPI @"join-conversation-by-id-unqualified" joinConversationById + <@> mkNamedAPI @"join-conversation-by-code-unqualified" joinConversationByReusableCode <@> mkNamedAPI @"code-check" checkReusableCode <@> mkNamedAPI @"create-conversation-code-unqualified@v3" (addCodeUnqualified Nothing) <@> mkNamedAPI @"create-conversation-code-unqualified" addCodeUnqualifiedWithReqBody <@> mkNamedAPI @"get-conversation-guest-links-status" getConversationGuestLinksStatus <@> mkNamedAPI @"remove-code-unqualified" rmCodeUnqualified <@> mkNamedAPI @"get-code" getCode - <@> mkNamedAPI @"member-typing-unqualified" (callsFed (exposeAnnotations memberTypingUnqualified)) - <@> mkNamedAPI @"member-typing-qualified" (callsFed (exposeAnnotations memberTyping)) - <@> mkNamedAPI @"remove-member-unqualified" (callsFed (exposeAnnotations removeMemberUnqualified)) - <@> mkNamedAPI @"remove-member" (callsFed (exposeAnnotations removeMemberQualified)) - <@> mkNamedAPI @"update-other-member-unqualified" (callsFed (exposeAnnotations updateOtherMemberUnqualified)) - <@> mkNamedAPI @"update-other-member" (callsFed (exposeAnnotations updateOtherMember)) - <@> mkNamedAPI @"update-conversation-name-deprecated" (callsFed (exposeAnnotations updateUnqualifiedConversationName)) - <@> mkNamedAPI @"update-conversation-name-unqualified" (callsFed (exposeAnnotations updateUnqualifiedConversationName)) - <@> mkNamedAPI @"update-conversation-name" (callsFed (exposeAnnotations updateConversationName)) - <@> mkNamedAPI @"update-conversation-message-timer-unqualified" (callsFed (exposeAnnotations updateConversationMessageTimerUnqualified)) - <@> mkNamedAPI @"update-conversation-message-timer" (callsFed (exposeAnnotations updateConversationMessageTimer)) - <@> mkNamedAPI @"update-conversation-receipt-mode-unqualified" (callsFed (exposeAnnotations updateConversationReceiptModeUnqualified)) - <@> mkNamedAPI @"update-conversation-receipt-mode" (callsFed (exposeAnnotations updateConversationReceiptMode)) - <@> mkNamedAPI @"update-conversation-access-unqualified" (callsFed (exposeAnnotations updateConversationAccessUnqualified)) - <@> mkNamedAPI @"update-conversation-access@v2" (callsFed (exposeAnnotations updateConversationAccess)) - <@> mkNamedAPI @"update-conversation-access" (callsFed (exposeAnnotations updateConversationAccess)) + <@> mkNamedAPI @"member-typing-unqualified" memberTypingUnqualified + <@> mkNamedAPI @"member-typing-qualified" memberTyping + <@> mkNamedAPI @"remove-member-unqualified" removeMemberUnqualified + <@> mkNamedAPI @"remove-member" removeMemberQualified + <@> mkNamedAPI @"update-other-member-unqualified" updateOtherMemberUnqualified + <@> mkNamedAPI @"update-other-member" updateOtherMember + <@> mkNamedAPI @"update-conversation-name-deprecated" updateUnqualifiedConversationName + <@> mkNamedAPI @"update-conversation-name-unqualified" updateUnqualifiedConversationName + <@> mkNamedAPI @"update-conversation-name" updateConversationName + <@> mkNamedAPI @"update-conversation-message-timer-unqualified" updateConversationMessageTimerUnqualified + <@> mkNamedAPI @"update-conversation-message-timer" updateConversationMessageTimer + <@> mkNamedAPI @"update-conversation-receipt-mode-unqualified" updateConversationReceiptModeUnqualified + <@> mkNamedAPI @"update-conversation-receipt-mode" updateConversationReceiptMode + <@> mkNamedAPI @"update-conversation-access-unqualified" updateConversationAccessUnqualified + <@> mkNamedAPI @"update-conversation-access@v2" updateConversationAccess + <@> mkNamedAPI @"update-conversation-access" updateConversationAccess <@> mkNamedAPI @"get-conversation-self-unqualified" getLocalSelf <@> mkNamedAPI @"update-conversation-self-unqualified" updateUnqualifiedSelfMember <@> mkNamedAPI @"update-conversation-self" updateSelfMember diff --git a/services/galley/src/Galley/API/Public/Feature.hs b/services/galley/src/Galley/API/Public/Feature.hs index 3e9d3f68a54..6f182884dde 100644 --- a/services/galley/src/Galley/API/Public/Feature.hs +++ b/services/galley/src/Galley/API/Public/Feature.hs @@ -1,3 +1,6 @@ +{-# LANGUAGE PartialTypeSignatures #-} +{-# OPTIONS_GHC -Wno-partial-type-signatures #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -19,68 +22,68 @@ module Galley.API.Public.Feature where import Galley.API.Teams import Galley.API.Teams.Features +import Galley.API.Teams.Features.Get import Galley.App import Imports -import Wire.API.Federation.API import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.Feature import Wire.API.Team.Feature +featureAPIGetPut :: forall cfg r. (_) => API (FeatureAPIGetPut cfg) r +featureAPIGetPut = + mkNamedAPI @'("get", cfg) getFeature + <@> mkNamedAPI @'("put", cfg) setFeature + featureAPI :: API FeatureAPI GalleyEffects featureAPI = - mkNamedAPI @'("get", SSOConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", LegalholdConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", LegalholdConfig) (callsFed (exposeAnnotations (setFeatureStatus . DoAuth))) - <@> mkNamedAPI @'("get", SearchVisibilityAvailableConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", SearchVisibilityAvailableConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get-deprecated", SearchVisibilityAvailableConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put-deprecated", SearchVisibilityAvailableConfig) (setFeatureStatus . DoAuth) + mkNamedAPI @'("get", SSOConfig) getFeature + <@> featureAPIGetPut + <@> featureAPIGetPut <@> mkNamedAPI @"get-search-visibility" getSearchVisibility <@> mkNamedAPI @"set-search-visibility" (setSearchVisibility (featureEnabledForTeam @SearchVisibilityAvailableConfig)) - <@> mkNamedAPI @'("get", ValidateSAMLEmailsConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get-deprecated", ValidateSAMLEmailsConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", DigitalSignaturesConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get-deprecated", DigitalSignaturesConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", AppLockConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", AppLockConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", FileSharingConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", FileSharingConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", ClassifiedDomainsConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", ConferenceCallingConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", SelfDeletingMessagesConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", SelfDeletingMessagesConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", GuestLinksConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", GuestLinksConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", SndFactorPasswordChallengeConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", SndFactorPasswordChallengeConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", MLSConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", MLSConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", ExposeInvitationURLsToTeamAdminConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", ExposeInvitationURLsToTeamAdminConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", SearchVisibilityInboundConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", SearchVisibilityInboundConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", OutlookCalIntegrationConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", OutlookCalIntegrationConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", MlsE2EIdConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @"put-MlsE2EIdConfig@v5" (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", MlsE2EIdConfig) (guardMlsE2EIdConfig (setFeatureStatus . DoAuth)) - <@> mkNamedAPI @'("get", MlsMigrationConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", MlsMigrationConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", EnforceFileDownloadLocationConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", EnforceFileDownloadLocationConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", LimitedEventFanoutConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @"get-all-feature-configs-for-user" getAllFeatureConfigsForUser - <@> mkNamedAPI @"get-all-feature-configs-for-team" getAllFeatureConfigsForTeam - <@> mkNamedAPI @'("get-config", LegalholdConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", SSOConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", SearchVisibilityAvailableConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", ValidateSAMLEmailsConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", DigitalSignaturesConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", AppLockConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", FileSharingConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", ClassifiedDomainsConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", ConferenceCallingConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", SelfDeletingMessagesConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", GuestLinksConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", SndFactorPasswordChallengeConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", MLSConfig) getFeatureStatusForUser + <@> mkNamedAPI @'("get", ValidateSAMLEmailsConfig) getFeature + <@> mkNamedAPI @'("get", DigitalSignaturesConfig) getFeature + <@> featureAPIGetPut + <@> featureAPIGetPut + <@> mkNamedAPI @'("get", ClassifiedDomainsConfig) getFeature + <@> featureAPIGetPut + <@> featureAPIGetPut + <@> featureAPIGetPut + <@> featureAPIGetPut + <@> hoistAPI id featureAPIGetPut + <@> featureAPIGetPut + <@> featureAPIGetPut + <@> featureAPIGetPut + <@> mkNamedAPI @'("get", MlsE2EIdConfig) getFeature + <@> mkNamedAPI @"put-MlsE2EIdConfig@v5" setFeature + <@> mkNamedAPI @'("put", MlsE2EIdConfig) (guardMlsE2EIdConfig setFeature) + <@> hoistAPI id featureAPIGetPut + <@> hoistAPI id featureAPIGetPut + <@> mkNamedAPI @'("get", LimitedEventFanoutConfig) getFeature + <@> mkNamedAPI @"get-all-feature-configs-for-user" getAllTeamFeaturesForUser + <@> mkNamedAPI @"get-all-feature-configs-for-team" getAllTeamFeaturesForTeam + <@> deprecatedFeatureConfigAPI + <@> deprecatedFeatureAPI + +deprecatedFeatureConfigAPI :: API DeprecatedFeatureAPI GalleyEffects +deprecatedFeatureConfigAPI = + mkNamedAPI @'("get-deprecated", SearchVisibilityAvailableConfig) getFeature + <@> mkNamedAPI @'("put-deprecated", SearchVisibilityAvailableConfig) setFeature + <@> mkNamedAPI @'("get-deprecated", ValidateSAMLEmailsConfig) getFeature + <@> mkNamedAPI @'("get-deprecated", DigitalSignaturesConfig) getFeature + +deprecatedFeatureAPI :: API (AllDeprecatedFeatureConfigAPI DeprecatedFeatureConfigs) GalleyEffects +deprecatedFeatureAPI = + mkNamedAPI @'("get-config", LegalholdConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", SSOConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", SearchVisibilityAvailableConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", ValidateSAMLEmailsConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", DigitalSignaturesConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", AppLockConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", FileSharingConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", ClassifiedDomainsConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", ConferenceCallingConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", SelfDeletingMessagesConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", GuestLinksConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", SndFactorPasswordChallengeConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", MLSConfig) getSingleFeatureForUser diff --git a/services/galley/src/Galley/API/Public/LegalHold.hs b/services/galley/src/Galley/API/Public/LegalHold.hs index b313b84e972..04afca327fb 100644 --- a/services/galley/src/Galley/API/Public/LegalHold.hs +++ b/services/galley/src/Galley/API/Public/LegalHold.hs @@ -19,7 +19,6 @@ module Galley.API.Public.LegalHold where import Galley.API.LegalHold import Galley.App -import Wire.API.Federation.API import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.LegalHold @@ -27,9 +26,9 @@ legalHoldAPI :: API LegalHoldAPI GalleyEffects legalHoldAPI = mkNamedAPI @"create-legal-hold-settings" createSettings <@> mkNamedAPI @"get-legal-hold-settings" getSettings - <@> mkNamedAPI @"delete-legal-hold-settings" (callsFed (exposeAnnotations removeSettingsInternalPaging)) + <@> mkNamedAPI @"delete-legal-hold-settings" removeSettingsInternalPaging <@> mkNamedAPI @"get-legal-hold" getUserStatus - <@> mkNamedAPI @"consent-to-legal-hold" (callsFed (exposeAnnotations grantConsent)) - <@> mkNamedAPI @"request-legal-hold-device" (callsFed (exposeAnnotations requestDevice)) - <@> mkNamedAPI @"disable-legal-hold-for-user" (callsFed (exposeAnnotations disableForUser)) - <@> mkNamedAPI @"approve-legal-hold-device" (callsFed (exposeAnnotations approveDevice)) + <@> mkNamedAPI @"consent-to-legal-hold" grantConsent + <@> mkNamedAPI @"request-legal-hold-device" requestDevice + <@> mkNamedAPI @"disable-legal-hold-for-user" disableForUser + <@> mkNamedAPI @"approve-legal-hold-device" approveDevice diff --git a/services/galley/src/Galley/API/Public/MLS.hs b/services/galley/src/Galley/API/Public/MLS.hs index 2391e44c081..d99270c8485 100644 --- a/services/galley/src/Galley/API/Public/MLS.hs +++ b/services/galley/src/Galley/API/Public/MLS.hs @@ -19,13 +19,12 @@ module Galley.API.Public.MLS where import Galley.API.MLS import Galley.App -import Wire.API.MakesFederatedCall +import Imports import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.MLS mlsAPI :: API MLSAPI GalleyEffects mlsAPI = - mkNamedAPI @"mls-message" (callsFed (exposeAnnotations postMLSMessageFromLocalUser)) - <@> mkNamedAPI @"mls-commit-bundle" (callsFed (exposeAnnotations postMLSCommitBundleFromLocalUser)) - <@> mkNamedAPI @"mls-public-keys-v5" getMLSPublicKeys - <@> mkNamedAPI @"mls-public-keys" getMLSPublicKeysJWK + mkNamedAPI @"mls-message" postMLSMessageFromLocalUser + <@> mkNamedAPI @"mls-commit-bundle" postMLSCommitBundleFromLocalUser + <@> mkNamedAPI @"mls-public-keys" (const getMLSPublicKeys) diff --git a/services/galley/src/Galley/API/Public/Messaging.hs b/services/galley/src/Galley/API/Public/Messaging.hs index efbbd7482f7..806484ae908 100644 --- a/services/galley/src/Galley/API/Public/Messaging.hs +++ b/services/galley/src/Galley/API/Public/Messaging.hs @@ -19,13 +19,12 @@ module Galley.API.Public.Messaging where import Galley.API.Update import Galley.App -import Wire.API.Federation.API import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.Messaging messagingAPI :: API MessagingAPI GalleyEffects messagingAPI = - mkNamedAPI @"post-otr-message-unqualified" (callsFed (exposeAnnotations postOtrMessageUnqualified)) + mkNamedAPI @"post-otr-message-unqualified" postOtrMessageUnqualified <@> mkNamedAPI @"post-otr-broadcast-unqualified" postOtrBroadcastUnqualified - <@> mkNamedAPI @"post-proteus-message" (callsFed (exposeAnnotations postProteusMessage)) + <@> mkNamedAPI @"post-proteus-message" postProteusMessage <@> mkNamedAPI @"post-proteus-broadcast" postProteusBroadcast diff --git a/services/galley/src/Galley/API/Public/TeamConversation.hs b/services/galley/src/Galley/API/Public/TeamConversation.hs index 173d7aba61a..359c69f1db2 100644 --- a/services/galley/src/Galley/API/Public/TeamConversation.hs +++ b/services/galley/src/Galley/API/Public/TeamConversation.hs @@ -19,7 +19,6 @@ module Galley.API.Public.TeamConversation where import Galley.API.Teams import Galley.App -import Wire.API.Federation.API import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.TeamConversation @@ -28,4 +27,4 @@ teamConversationAPI = mkNamedAPI @"get-team-conversation-roles" getTeamConversationRoles <@> mkNamedAPI @"get-team-conversations" getTeamConversations <@> mkNamedAPI @"get-team-conversation" getTeamConversation - <@> mkNamedAPI @"delete-team-conversation" (callsFed (exposeAnnotations deleteTeamConversation)) + <@> mkNamedAPI @"delete-team-conversation" deleteTeamConversation diff --git a/services/galley/src/Galley/API/Public/TeamMember.hs b/services/galley/src/Galley/API/Public/TeamMember.hs index 91956a21712..c6b7d5cd059 100644 --- a/services/galley/src/Galley/API/Public/TeamMember.hs +++ b/services/galley/src/Galley/API/Public/TeamMember.hs @@ -18,6 +18,7 @@ module Galley.API.Public.TeamMember where import Galley.API.Teams +import Galley.API.Teams.Export qualified as Export import Galley.App import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.TeamMember @@ -31,4 +32,4 @@ teamMemberAPI = <@> mkNamedAPI @"delete-team-member" deleteTeamMember <@> mkNamedAPI @"delete-non-binding-team-member" deleteNonBindingTeamMember <@> mkNamedAPI @"update-team-member" updateTeamMember - <@> mkNamedAPI @"get-team-members-csv" getTeamMembersCSV + <@> mkNamedAPI @"get-team-members-csv" Export.getTeamMembersCSV diff --git a/services/galley/src/Galley/API/Push.hs b/services/galley/src/Galley/API/Push.hs index c66a8ae73a4..79a70c56281 100644 --- a/services/galley/src/Galley/API/Push.hs +++ b/services/galley/src/Galley/API/Push.hs @@ -37,13 +37,13 @@ import Data.Map qualified as Map import Data.Qualified import Galley.Data.Services import Galley.Effects.ExternalAccess -import Gundeck.Types.Push (RecipientClients (RecipientClientsSome), Route (..)) 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.NotificationSubsystem data MessagePush @@ -69,7 +69,7 @@ newMessagePush :: Event -> MessagePush newMessagePush botMap mconn mm userOrBots event = - let toPair r = case Map.lookup (_recipientUserId r) botMap of + let toPair r = case Map.lookup (recipientUserId r) botMap of Just botMember -> ([], [botMember]) Nothing -> ([r], []) (recipients, botMembers) = foldMap (toPair . toRecipient) userOrBots diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index 8facb7f7b76..47fb3365508 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -1,4 +1,5 @@ {-# LANGUAGE RecordWildCards #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} -- This file is part of the Wire Server implementation. -- @@ -37,6 +38,9 @@ module Galley.API.Query ensureConvAdmin, getMLSSelfConversation, getMLSSelfConversationWithError, + getMLSOne2OneConversationV5, + getMLSOne2OneConversationV6, + getMLSOne2OneConversationInternal, getMLSOne2OneConversation, isMLSOne2OneEstablished, ) @@ -45,6 +49,7 @@ where import Cassandra qualified as C import Control.Lens import Control.Monad.Extra +import Data.ByteString.Conversion import Data.ByteString.Lazy qualified as LBS import Data.Code import Data.CommaSeparatedList @@ -58,11 +63,13 @@ import Data.Range import Data.Set qualified as Set import Galley.API.Error import Galley.API.MLS +import Galley.API.MLS.Enabled import Galley.API.MLS.One2One import Galley.API.MLS.Types import Galley.API.Mapping import Galley.API.Mapping qualified as Mapping import Galley.API.One2One +import Galley.API.Teams.Features.Get import Galley.API.Util import Galley.Data.Conversation qualified as Data import Galley.Data.Conversation.Types qualified as Data @@ -73,15 +80,14 @@ import Galley.Effects.ConversationStore qualified as E import Galley.Effects.FederatorAccess qualified as E import Galley.Effects.ListItems qualified as E import Galley.Effects.MemberStore qualified as E -import Galley.Effects.TeamFeatureStore qualified as TeamFeatures import Galley.Env import Galley.Options import Galley.Types.Conversations.Members -import Galley.Types.Teams import Imports import Polysemy import Polysemy.Error import Polysemy.Input +import Polysemy.TinyLog (TinyLog) import Polysemy.TinyLog qualified as P import System.Logger.Class qualified as Logger import Wire.API.Conversation hiding (Member) @@ -94,11 +100,13 @@ import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Federation.API import Wire.API.Federation.API.Galley -import Wire.API.Federation.Client (FederatorClient) +import Wire.API.Federation.Client (FederatorClient, getNegotiatedVersion) import Wire.API.Federation.Error +import Wire.API.Federation.Version qualified as Federation +import Wire.API.MLS.Keys import Wire.API.Provider.Bot qualified as Public import Wire.API.Routes.MultiTablePaging qualified as Public -import Wire.API.Team.Feature as Public hiding (setStatus) +import Wire.API.Team.Feature as Public import Wire.API.User import Wire.Sem.Paging.Cassandra @@ -237,7 +245,7 @@ getRemoteConversationsWithFailures :: getRemoteConversationsWithFailures lusr convs = do -- get self member statuses from the database statusMap <- E.getRemoteConversationStatus (tUnqualified lusr) convs - let remoteView :: Remote RemoteConversation -> Conversation + let remoteView :: Remote RemoteConversationV2 -> Conversation remoteView rconv = Mapping.remoteConversationView lusr @@ -253,8 +261,15 @@ getRemoteConversationsWithFailures lusr convs = do | otherwise = [failedGetConversationLocally (map tUntagged locallyNotFound)] -- request conversations from remote backends - let rpc :: GetConversationsRequest -> FederatorClient 'Galley GetConversationsResponse - rpc = fedClient @'Galley @"get-conversations" + let rpc :: GetConversationsRequest -> FederatorClient 'Galley GetConversationsResponseV2 + rpc req = do + mFedVersion <- getNegotiatedVersion + case mFedVersion of + Nothing -> error "impossible" + Just fedVersion -> + if fedVersion < Federation.V2 + then getConversationsResponseToV2 <$> fedClient @'Galley @"get-conversations@v1" req + else fedClient @'Galley @"get-conversations" req resp <- E.runFederatedConcurrentlyEither locallyFound $ \someConvs -> rpc $ GetConversationsRequest (tUnqualified lusr) (tUnqualified someConvs) @@ -264,8 +279,8 @@ getRemoteConversationsWithFailures lusr convs = do where handleFailure :: (Member P.TinyLog r) => - Either (Remote [ConvId], FederationError) (Remote GetConversationsResponse) -> - Sem r (Either FailedGetConversation [Remote RemoteConversation]) + Either (Remote [ConvId], FederationError) (Remote GetConversationsResponseV2) -> + Sem r (Either FailedGetConversation [Remote RemoteConversationV2]) handleFailure (Left (rcids, e)) = do P.warn $ Logger.msg ("Error occurred while fetching remote conversations" :: ByteString) @@ -575,15 +590,17 @@ iterateConversations luid pageSize handleConvs = go Nothing internalGetMember :: ( Member ConversationStore r, + Member (Error FederationError) r, Member (Input (Local ())) r, Member MemberStore r ) => - ConvId -> + Qualified ConvId -> UserId -> Sem r (Maybe Public.Member) -internalGetMember cnv usr = do +internalGetMember qcnv usr = do lusr <- qualifyLocal usr - getLocalSelf lusr cnv + lcnv <- ensureLocal lusr qcnv + getLocalSelf lusr (tUnqualified lcnv) getLocalSelf :: ( Member ConversationStore r, @@ -654,7 +671,7 @@ ensureGuestLinksEnabled :: Maybe TeamId -> Sem r () ensureGuestLinksEnabled mbTid = - getConversationGuestLinksFeatureStatus mbTid >>= \ws -> case wsStatus ws of + getConversationGuestLinksFeatureStatus mbTid >>= \ws -> case ws.status of FeatureStatusEnabled -> pure () FeatureStatusDisabled -> throwS @'GuestLinksDisabled @@ -668,7 +685,7 @@ getConversationGuestLinksStatus :: ) => UserId -> ConvId -> - Sem r (WithStatus GuestLinksConfig) + Sem r (LockableFeature GuestLinksConfig) getConversationGuestLinksStatus uid convId = do conv <- E.getConversation convId >>= noteS @'ConvNotFound ensureConvAdmin (Data.convLocalMembers conv) uid @@ -680,15 +697,9 @@ getConversationGuestLinksFeatureStatus :: Member (Input Opts) r ) => Maybe TeamId -> - Sem r (WithStatus GuestLinksConfig) -getConversationGuestLinksFeatureStatus mbTid = do - defaultStatus :: WithStatus GuestLinksConfig <- input <&> view (settings . featureFlags . flagConversationGuestLinks . unDefaults) - case mbTid of - Nothing -> pure defaultStatus - Just tid -> do - mbConfigNoLock <- TeamFeatures.getFeatureConfig FeatureSingletonGuestLinksConfig tid - mbLockStatus <- TeamFeatures.getFeatureLockStatus FeatureSingletonGuestLinksConfig tid - pure $ computeFeatureConfigForTeamUser mbConfigNoLock mbLockStatus defaultStatus + Sem r (LockableFeature GuestLinksConfig) +getConversationGuestLinksFeatureStatus Nothing = getFeatureForServer @GuestLinksConfig +getConversationGuestLinksFeatureStatus (Just tid) = getFeatureForTeam @GuestLinksConfig tid -- | The same as 'getMLSSelfConversation', but it throws an error in case the -- backend is not configured for MLS (the proxy for it being the existance of @@ -735,7 +746,7 @@ getMLSSelfConversation lusr = do -- uses the same function to calculate the conversation ID and corresponding -- group ID, however we /do/ assume that the two backends agree on which of the -- two is responsible for hosting the conversation. -getMLSOne2OneConversation :: +getMLSOne2OneConversationV5 :: ( Member BrigAccess r, Member ConversationStore r, Member (Input Env) r, @@ -743,6 +754,7 @@ getMLSOne2OneConversation :: Member (Error InternalError) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'NotConnected) r, + Member (ErrorS 'MLSFederatedOne2OneNotSupported) r, Member FederatorAccess r, Member TeamStore r, Member P.TinyLog r @@ -750,7 +762,45 @@ getMLSOne2OneConversation :: Local UserId -> Qualified UserId -> Sem r Conversation -getMLSOne2OneConversation lself qother = do +getMLSOne2OneConversationV5 lself qother = do + if isLocal lself qother + then getMLSOne2OneConversationInternal lself qother + else throwS @MLSFederatedOne2OneNotSupported + +getMLSOne2OneConversationInternal :: + ( Member BrigAccess r, + Member ConversationStore r, + Member (Input Env) r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member (ErrorS 'MLSNotEnabled) r, + Member (ErrorS 'NotConnected) r, + Member FederatorAccess r, + Member TeamStore r, + Member P.TinyLog r + ) => + Local UserId -> + Qualified UserId -> + Sem r Conversation +getMLSOne2OneConversationInternal lself qother = + (.conversation) <$> getMLSOne2OneConversation lself qother Nothing + +getMLSOne2OneConversationV6 :: + ( Member BrigAccess r, + Member ConversationStore r, + Member (Input Env) r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member (ErrorS 'MLSNotEnabled) r, + Member (ErrorS 'NotConnected) r, + Member FederatorAccess r, + Member TeamStore r, + Member P.TinyLog r + ) => + Local UserId -> + Qualified UserId -> + Sem r (MLSOne2OneConversation MLSPublicKey) +getMLSOne2OneConversationV6 lself qother = do assertMLSEnabled ensureConnectedOrSameTeam lself [qother] let convId = one2OneConvId BaseProtocolMLSTag (tUntagged lself) qother @@ -760,30 +810,61 @@ getMLSOne2OneConversation lself qother = do (getRemoteMLSOne2OneConversation lself qother) convId +getMLSOne2OneConversation :: + ( Member BrigAccess r, + Member ConversationStore r, + Member (Input Env) r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member (ErrorS 'MLSNotEnabled) r, + Member (ErrorS 'NotConnected) r, + Member FederatorAccess r, + Member TeamStore r, + Member P.TinyLog r + ) => + Local UserId -> + Qualified UserId -> + Maybe MLSPublicKeyFormat -> + Sem r (MLSOne2OneConversation SomeKey) +getMLSOne2OneConversation lself qother fmt = do + convWithUnformattedKeys <- getMLSOne2OneConversationV6 lself qother + MLSOne2OneConversation convWithUnformattedKeys.conversation + <$> formatPublicKeys fmt convWithUnformattedKeys.publicKeys + getLocalMLSOne2OneConversation :: ( Member ConversationStore r, Member (Error InternalError) r, - Member P.TinyLog r + Member P.TinyLog r, + Member (Input Env) r, + Member (ErrorS MLSNotEnabled) r ) => Local UserId -> Local ConvId -> - Sem r Conversation + Sem r (MLSOne2OneConversation MLSPublicKey) getLocalMLSOne2OneConversation lself lconv = do mconv <- E.getConversation (tUnqualified lconv) - case mconv of + keys <- mlsKeysToPublic <$$> getMLSPrivateKeys + conv <- case mconv of Nothing -> pure (localMLSOne2OneConversation lself lconv) Just conv -> conversationView lself conv + pure $ + MLSOne2OneConversation + { conversation = conv, + publicKeys = keys + } getRemoteMLSOne2OneConversation :: ( Member (Error InternalError) r, Member (Error FederationError) r, Member (ErrorS 'NotConnected) r, - Member FederatorAccess r + Member FederatorAccess r, + Member (ErrorS MLSNotEnabled) r, + Member TinyLog r ) => Local UserId -> Qualified UserId -> Remote conv -> - Sem r Conversation + Sem r (MLSOne2OneConversation MLSPublicKey) getRemoteMLSOne2OneConversation lself qother rconv = do -- a conversation can only be remote if it is hosted on the other user's domain rother <- @@ -792,15 +873,32 @@ getRemoteMLSOne2OneConversation lself qother rconv = do else throw (InternalErrorWithDescription "Unexpected 1-1 conversation domain") resp <- - E.runFederated rconv $ - fedClient @'Galley @"get-one2one-conversation" $ - GetOne2OneConversationRequest (tUnqualified lself) (tUnqualified rother) + E.runFederated rconv $ do + negotiatedVersion <- getNegotiatedVersion + case negotiatedVersion of + Nothing -> error "impossible" + Just Federation.V0 -> pure . Left . FederationCallFailure $ FederatorClientVersionNegotiationError RemoteTooOld + Just Federation.V1 -> pure . Left . FederationCallFailure $ FederatorClientVersionNegotiationError RemoteTooOld + Just _ -> + fmap Right . fedClient @'Galley @"get-one2one-conversation" $ + GetOne2OneConversationRequest (tUnqualified lself) (tUnqualified rother) case resp of - GetOne2OneConversationOk rc -> + Right (GetOne2OneConversationV2Ok rc) -> pure (remoteMLSOne2OneConversation lself rother rc) - GetOne2OneConversationBackendMismatch -> + Right GetOne2OneConversationV2BackendMismatch -> throw (FederationUnexpectedBody "Backend mismatch when retrieving a remote 1-1 conversation") - GetOne2OneConversationNotConnected -> throwS @'NotConnected + Right GetOne2OneConversationV2NotConnected -> throwS @'NotConnected + Right GetOne2OneConversationV2MLSNotEnabled -> do + -- This is confusing to clients because we do not tell them which backend + -- doesn't have MLS enabled, which would nice information for fixing + -- problems in real world. We do the same thing when sending Welcome + -- messages, so for now, let's do the same thing. + P.warn $ + Logger.field "domain" (toByteString' (tDomain rother)) + . Logger.msg + ("Cannot get remote MLSOne2OneConversation because MLS is not enabled on remote" :: ByteString) + throwS @'MLSNotEnabled + Left e -> throw e -- | Check if an MLS 1-1 conversation has been established, namely if its epoch -- is non-zero. The conversation will only be stored in the database when its @@ -817,7 +915,8 @@ isMLSOne2OneEstablished :: Member (Error InternalError) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'NotConnected) r, - Member FederatorAccess r + Member FederatorAccess r, + Member TinyLog r ) => Local UserId -> Qualified UserId -> @@ -847,14 +946,16 @@ isRemoteMLSOne2OneEstablished :: ( Member (ErrorS 'NotConnected) r, Member (Error FederationError) r, Member (Error InternalError) r, - Member FederatorAccess r + Member FederatorAccess r, + Member (ErrorS MLSNotEnabled) r, + Member TinyLog r ) => Local UserId -> Qualified UserId -> Remote conv -> Sem r Bool isRemoteMLSOne2OneEstablished lself qother rconv = do - conv <- getRemoteMLSOne2OneConversation lself qother rconv + conv <- (.conversation) <$> getRemoteMLSOne2OneConversation lself qother rconv pure . (> 0) $ case cnvProtocol conv of ProtocolProteus -> 0 ProtocolMLS meta -> ep meta diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index e3aed8fbd4c..e51070d5f5a 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -31,7 +31,6 @@ module Galley.API.Teams addTeamMember, getTeamConversationRoles, getTeamMembers, - getTeamMembersCSV, bulkGetTeamMembers, getTeamMember, deleteTeamMember, @@ -57,18 +56,13 @@ module Galley.API.Teams ) where -import Brig.Types.Intra (accountUser) import Brig.Types.Team (TeamSize (..)) import Cassandra (PageWithState (pwsResults), pwsHasMore) import Cassandra qualified as C import Control.Lens -import Data.ByteString.Builder (lazyByteString) import Data.ByteString.Conversion (List, toByteString) import Data.ByteString.Conversion qualified import Data.ByteString.Lazy qualified as LBS -import Data.CaseInsensitive qualified as CI -import Data.Csv (EncodeOptions (..), Quoting (QuoteAll), encodeDefaultOrderedByNameWith) -import Data.Handle qualified as Handle import Data.Id import Data.Json.Util import Data.LegalHold qualified as LH @@ -76,8 +70,6 @@ import Data.List.Extra qualified as List import Data.List.NonEmpty (NonEmpty (..)) import Data.List1 (list1) import Data.Map qualified as Map -import Data.Map.Strict qualified as M -import Data.Misc (HttpsUrl, mkHttpsUrl) import Data.Proxy import Data.Qualified import Data.Range as Range @@ -87,6 +79,7 @@ import Data.Time.Clock (UTCTime) import Galley.API.Action import Galley.API.Error as Galley import Galley.API.LegalHold.Team +import Galley.API.Teams.Features import Galley.API.Teams.Features.Get import Galley.API.Teams.Notifications qualified as APITeamQueue import Galley.API.Update qualified as API @@ -112,14 +105,10 @@ import Galley.Types.Conversations.Members qualified as Conv import Galley.Types.Teams import Galley.Types.UserList import Imports hiding (forkIO) -import Network.Wai import Polysemy import Polysemy.Error -import Polysemy.Final import Polysemy.Input -import Polysemy.Output import Polysemy.TinyLog qualified as P -import SAML2.WebSSO qualified as SAML import System.Logger qualified as Log import Wire.API.Conversation (ConversationRemoveMembers (..)) import Wire.API.Conversation.Role (wireConvRoles) @@ -130,7 +119,6 @@ import Wire.API.Event.Conversation qualified as Conv import Wire.API.Event.LeaveReason import Wire.API.Event.Team import Wire.API.Federation.Error -import Wire.API.Message qualified as Conv import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Routes.MultiTablePaging (MultiTablePage (MultiTablePage), MultiTablePagingState (mtpsState)) import Wire.API.Routes.Public.Galley.TeamMember @@ -138,7 +126,6 @@ import Wire.API.Team import Wire.API.Team qualified as Public import Wire.API.Team.Conversation import Wire.API.Team.Conversation qualified as Public -import Wire.API.Team.Export (TeamExportUser (..)) import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.API.Team.Member qualified as M @@ -147,12 +134,8 @@ import Wire.API.Team.Permission (Perm (..), Permissions (..), SPerm (..), copy, import Wire.API.Team.Role import Wire.API.Team.SearchVisibility import Wire.API.Team.SearchVisibility qualified as Public -import Wire.API.User (ScimUserInfo (..), User, UserIdList, UserSSOId (UserScimExternalId), userSCIMExternalId, userSSOId) import Wire.API.User qualified as U -import Wire.API.User.Identity (UserSSOId (UserSSOId)) -import Wire.API.User.RichInfo (RichInfo) import Wire.NotificationSubsystem -import Wire.Sem.Paging qualified as E import Wire.Sem.Paging.Cassandra getTeamH :: @@ -228,56 +211,37 @@ lookupTeam zusr tid = do else pure Nothing createNonBindingTeamH :: - forall r. - ( Member BrigAccess r, - Member (ErrorS 'UserBindingExists) r, - Member (ErrorS 'NotConnected) r, - Member NotificationSubsystem r, - Member (Input UTCTime) r, - Member P.TinyLog r, - Member TeamStore r - ) => + (Member (ErrorS InvalidAction) r) => UserId -> ConnId -> - Public.NonBindingNewTeam -> + a -> Sem r TeamId -createNonBindingTeamH zusr zcon (Public.NonBindingNewTeam body) = do - let owner = Public.mkTeamMember zusr fullPermissions Nothing LH.defUserLegalHoldStatus - let others = - filter ((zusr /=) . view userId) - . maybe [] fromRange - $ body ^. newTeamMembers - let zothers = map (view userId) others - ensureUnboundUsers (zusr : zothers) - ensureConnectedToLocals zusr zothers - P.debug $ - Log.field "targets" (toByteString . show $ toByteString <$> zothers) - . Log.field "action" (Log.val "Teams.createNonBindingTeam") - team <- - E.createTeam - Nothing - zusr - (body ^. newTeamName) - (body ^. newTeamIcon) - (body ^. newTeamIconKey) - NonBinding - finishCreateTeam team owner others (Just zcon) - pure (team ^. teamId) +createNonBindingTeamH _ _ _ = do + -- non-binding teams are not supported anymore + throwS @InvalidAction createBindingTeam :: ( Member NotificationSubsystem r, Member (Input UTCTime) r, + Member (Input Opts) r, + Member TeamFeatureStore r, Member TeamStore r ) => TeamId -> UserId -> - BindingNewTeam -> + NewTeam -> Sem r TeamId -createBindingTeam tid zusr (BindingNewTeam body) = do +createBindingTeam tid zusr body = do let owner = Public.mkTeamMember zusr fullPermissions Nothing LH.defUserLegalHoldStatus team <- - E.createTeam (Just tid) zusr (body ^. newTeamName) (body ^. newTeamIcon) (body ^. newTeamIconKey) Binding - finishCreateTeam team owner [] Nothing + E.createTeam (Just tid) zusr body.newTeamName body.newTeamIcon body.newTeamIconKey Binding + initialiseTeamFeatures tid + + E.createTeamMember tid owner + now <- input + let e = newEvent tid now (EdTeamCreate team) + pushNotifications + [newPushLocal1 zusr (toJSONObject e) (userRecipient zusr :| [])] pure tid updateTeamStatus :: @@ -519,130 +483,6 @@ getTeamMembers lzusr tid mbMaxResults mbPagingState = do (pwsHasMore p) (teamMemberPagingState p) -outputToStreamingBody :: (Member (Final IO) r) => Sem (Output LByteString ': r) () -> Sem r StreamingBody -outputToStreamingBody action = withWeavingToFinal @IO $ \state weave _inspect -> - pure . (<$ state) $ \write flush -> do - let writeChunk c = embedFinal $ do - write (lazyByteString c) - flush - void . weave . (<$ state) $ runOutputSem writeChunk action - -getTeamMembersCSV :: - ( Member BrigAccess r, - Member (ErrorS 'AccessDenied) r, - Member (TeamMemberStore InternalPaging) r, - Member TeamStore r, - Member (Final IO) r, - Member SparAccess r - ) => - Local UserId -> - TeamId -> - Sem r StreamingBody -getTeamMembersCSV lusr tid = do - E.getTeamMember tid (tUnqualified lusr) >>= \case - Nothing -> throwS @'AccessDenied - Just member -> unless (member `hasPermission` DownloadTeamMembersCsv) $ throwS @'AccessDenied - - -- In case an exception is thrown inside the StreamingBody of responseStream - -- the response will not contain a correct error message, but rather be an - -- http error such as 'InvalidChunkHeaders'. The exception however still - -- reaches the middleware and is being tracked in logging and metrics. - outputToStreamingBody $ do - output headerLine - E.withChunks (\mps -> E.listTeamMembers @InternalPaging tid mps maxBound) $ - \members -> do - let uids = fmap (view userId) members - teamExportUser <- - mkTeamExportUser - <$> (lookupUser <$> E.lookupActivatedUsers uids) - <*> lookupInviterHandle members - <*> (lookupRichInfo <$> E.getRichInfoMultiUser uids) - <*> (lookupClients <$> E.lookupClients uids) - <*> (lookupScimUserInfo <$> Spar.lookupScimUserInfos uids) - output @LByteString - ( encodeDefaultOrderedByNameWith - defaultEncodeOptions - (mapMaybe teamExportUser members) - ) - where - headerLine :: LByteString - headerLine = encodeDefaultOrderedByNameWith (defaultEncodeOptions {encIncludeHeader = True}) ([] :: [TeamExportUser]) - - defaultEncodeOptions :: EncodeOptions - defaultEncodeOptions = - EncodeOptions - { encDelimiter = fromIntegral (ord ','), - encUseCrLf = True, -- to be compatible with Mac and Windows - encIncludeHeader = False, -- (so we can flush when the header is on the wire) - encQuoting = QuoteAll - } - - mkTeamExportUser :: - (UserId -> Maybe User) -> - (UserId -> Maybe Handle.Handle) -> - (UserId -> Maybe RichInfo) -> - (UserId -> Int) -> - (UserId -> Maybe ScimUserInfo) -> - TeamMember -> - Maybe TeamExportUser - mkTeamExportUser users inviters richInfos numClients scimUserInfo member = do - let uid = member ^. userId - user <- users uid - pure $ - TeamExportUser - { tExportDisplayName = U.userDisplayName user, - tExportHandle = U.userHandle user, - tExportEmail = U.userIdentity user >>= U.emailIdentity, - tExportRole = permissionsRole . view permissions $ member, - tExportCreatedOn = maybe (scimUserInfo uid >>= suiCreatedOn) (Just . snd) (view invitation member), - tExportInvitedBy = inviters . fst =<< member ^. invitation, - tExportIdpIssuer = userToIdPIssuer user, - tExportManagedBy = U.userManagedBy user, - tExportSAMLNamedId = fromMaybe "" (samlNamedId user), - tExportSCIMExternalId = fromMaybe "" (userSCIMExternalId user), - tExportSCIMRichInfo = richInfos uid, - tExportUserId = U.userId user, - tExportNumDevices = numClients uid - } - - lookupInviterHandle :: (Member BrigAccess r) => [TeamMember] -> Sem r (UserId -> Maybe Handle.Handle) - lookupInviterHandle members = do - let inviterIds :: [UserId] - inviterIds = nub $ mapMaybe (fmap fst . view invitation) members - - userList :: [User] <- accountUser <$$> E.getUsers inviterIds - - let userMap :: M.Map UserId Handle.Handle - userMap = M.fromList (mapMaybe extract userList) - where - extract u = (U.userId u,) <$> U.userHandle u - - pure (`M.lookup` userMap) - - userToIdPIssuer :: U.User -> Maybe HttpsUrl - userToIdPIssuer usr = case (U.userIdentity >=> U.ssoIdentity) usr of - Just (U.UserSSOId (SAML.UserRef issuer _)) -> either (const Nothing) Just . mkHttpsUrl $ issuer ^. SAML.fromIssuer - Just _ -> Nothing - Nothing -> Nothing - - lookupScimUserInfo :: [ScimUserInfo] -> (UserId -> Maybe ScimUserInfo) - lookupScimUserInfo infos = (`M.lookup` M.fromList (infos <&> (\sui -> (suiUserId sui, sui)))) - - lookupUser :: [U.User] -> (UserId -> Maybe U.User) - lookupUser users = (`M.lookup` M.fromList (users <&> \user -> (U.userId user, user))) - - lookupRichInfo :: [(UserId, RichInfo)] -> (UserId -> Maybe RichInfo) - lookupRichInfo pairs = (`M.lookup` M.fromList pairs) - - lookupClients :: Conv.UserClients -> UserId -> Int - lookupClients userClients uid = maybe 0 length (M.lookup uid (Conv.userClients userClients)) - - samlNamedId :: User -> Maybe Text - samlNamedId = - userSSOId >=> \case - (UserSSOId (SAML.UserRef _idp nameId)) -> Just . CI.original . SAML.unsafeShowNameID $ nameId - (UserScimExternalId _) -> Nothing - -- | like 'getTeamMembers', but with an explicit list of users we are to return. bulkGetTeamMembers :: ( Member (ErrorS 'BulkGetMemberLimitExceeded) r, @@ -652,7 +492,7 @@ bulkGetTeamMembers :: Local UserId -> TeamId -> Maybe (Range 1 HardTruncationLimit Int32) -> - UserIdList -> + U.UserIdList -> Sem r TeamMemberListOptPerms bulkGetTeamMembers lzusr tid mbMaxResults uids = do unless (length (U.mUsers uids) <= fromIntegral (fromRange (fromMaybe (unsafeRange Public.hardTruncationLimit) mbMaxResults))) $ @@ -991,7 +831,7 @@ deleteTeamMember' lusr zcon tid remove mBody = do Journal.teamUpdate tid sizeAfterDelete $ filter (/= remove) owners pure TeamMemberDeleteAccepted else do - getFeatureStatus @LimitedEventFanoutConfig DontDoAuth tid + getFeatureForTeam @LimitedEventFanoutConfig tid >>= ( \case FeatureStatusEnabled -> do admins <- E.getTeamAdmins tid @@ -1000,7 +840,7 @@ deleteTeamMember' lusr zcon tid remove mBody = do mems <- getTeamMembersForFanout tid uncheckedDeleteTeamMember lusr (Just zcon) tid remove (Right mems) ) - . wsStatus + . (.status) pure TeamMemberDeleteCompleted -- This function is "unchecked" because it does not validate that the user has the `RemoveTeamMember` permission. @@ -1256,8 +1096,8 @@ ensureNonBindingTeam tid = do ensureNotElevated :: (Member (ErrorS 'InvalidPermissions) r) => Permissions -> TeamMember -> Sem r () ensureNotElevated targetPermissions member = unless - ( (targetPermissions ^. self) - `Set.isSubsetOf` (member ^. permissions . copy) + ( targetPermissions.self + `Set.isSubsetOf` (member ^. permissions).copy ) $ throwS @'InvalidPermissions @@ -1341,28 +1181,6 @@ addTeamMemberInternal tid origin originConn (ntmNewTeamMember -> new) = do APITeamQueue.pushTeamEvent tid e pure sizeBeforeAdd -finishCreateTeam :: - ( Member NotificationSubsystem r, - Member (Input UTCTime) r, - Member TeamStore r - ) => - Team -> - TeamMember -> - [TeamMember] -> - Maybe ConnId -> - Sem r () -finishCreateTeam team owner others zcon = do - let zusr = owner ^. userId - for_ (owner : others) $ - E.createTeamMember (team ^. teamId) - now <- input - let e = newEvent (team ^. teamId) now (EdTeamCreate team) - let r = membersToRecipients Nothing others - pushNotifications - [ newPushLocal1 zusr (toJSONObject e) (userRecipient zusr :| r) - & pushConn .~ zcon - ] - getBindingTeamMembers :: ( Member (ErrorS 'TeamNotFound) r, Member (ErrorS 'NonBindingTeam) r, diff --git a/services/galley/src/Galley/API/Teams/Export.hs b/services/galley/src/Galley/API/Teams/Export.hs new file mode 100644 index 00000000000..f4f3d57c245 --- /dev/null +++ b/services/galley/src/Galley/API/Teams/Export.hs @@ -0,0 +1,177 @@ +module Galley.API.Teams.Export (getTeamMembersCSV) where + +import Control.Concurrent +import Control.Concurrent.Async qualified as Async +import Control.Lens (view, (^.)) +import Control.Monad.Codensity +import Data.ByteString (toStrict) +import Data.ByteString.Builder +import Data.Csv +import Data.Handle +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.BrigAccess +import Galley.Effects.SparAccess qualified as Spar +import Galley.Effects.TeamMemberStore (listTeamMembers) +import Galley.Effects.TeamStore +import Imports hiding (atomicModifyIORef, newEmptyMVar, newIORef, putMVar, readMVar, takeMVar, threadDelay, tryPutMVar) +import Polysemy +import Polysemy.Async +import Polysemy.Resource +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.Routes.LowLevelStream (LowLevelStreamingBody) +import Wire.API.Team.Export +import Wire.API.Team.Member +import Wire.API.User (ScimUserInfo (suiCreatedOn), User (..)) +import Wire.Sem.Concurrency +import Wire.Sem.Concurrency.IO +import Wire.Sem.Paging qualified as E +import Wire.Sem.Paging.Cassandra (InternalPaging) + +-- | Cache of inviter handles. +-- +-- This is used to make sure that inviters are only looked up once in brig, +-- even if they appear as inviters of several users in the team. +type InviterCache = IORef (Map UserId (MVar (Maybe Handle))) + +lookupInviter :: + (Member Resource r, Member BrigAccess r, Member (Final IO) r) => + InviterCache -> + UserId -> + Sem r (Maybe Handle) +lookupInviter cache uid = flip onException ensureCache $ do + empty <- embedFinal newEmptyMVar + (cached, var) <- + embedFinal $ atomicModifyIORef cache $ \m -> case Map.lookup uid m of + Nothing -> (Map.insert uid empty m, (False, empty)) + Just v -> (m, (True, v)) + -- the cache did not contain this user, so write it in the corresponding MVar + unless cached $ do + u <- listToMaybe <$> getUsers [uid] + embedFinal $ putMVar var (u >>= userHandle) + -- at this point, we know that the MVar contains a value or some other thread + -- is about to write one, so it is safe to just read from the MVar with a + -- blocking call + embedFinal $ readMVar var + where + -- this is run in case of errors to guarantee that other threads will never + -- deadlock while reading the cache + ensureCache = embedFinal $ do + m <- readIORef cache + for_ (Map.lookup uid m) $ \var -> + tryPutMVar var Nothing + +getUserRecord :: + ( Member BrigAccess r, + Member Spar.SparAccess r, + Member (ErrorS TeamMemberNotFound) r, + Member (Final IO) r, + Member Resource r + ) => + InviterCache -> + TeamMember -> + Sem r TeamExportUser +getUserRecord cache member = do + let uid = member ^. userId + export <- getUserExportData uid >>= noteS @TeamMemberNotFound + mCreatedOn <- do + let mFromInvitation = snd <$> member ^. invitation + case mFromInvitation of + Just ts -> pure $ Just ts + Nothing -> suiCreatedOn <$> Spar.lookupScimUserInfo uid + -- look up inviter handle from the cache + let mInviterId = fst <$> member ^. invitation + invitedBy <- join <$> traverse (lookupInviter cache) mInviterId + pure + export + { tExportInvitedBy = invitedBy, + tExportRole = permissionsRole . view permissions $ member, + tExportCreatedOn = mCreatedOn + } + +-- | Export team info as a CSV, and stream it to the client. +-- +-- We paginate through the team member list, then spawn a thread for each user +-- (out of a thread pool) in order to fetch information for that user from brig +-- and spar. Inviter IDs are resolved to handles via a brig request, then +-- stored in a cache so that they can be reused by subsequent requests. +getTeamMembersCSV :: + forall r. + ( Member BrigAccess r, + Member (ErrorS 'AccessDenied) r, + Member (TeamMemberStore InternalPaging) r, + Member TeamStore r, + Member (Final IO) r, + Member SparAccess r + ) => + Local UserId -> + TeamId -> + Sem r LowLevelStreamingBody +getTeamMembersCSV lusr tid = do + getTeamMember tid (tUnqualified lusr) >>= \case + Nothing -> throwS @'AccessDenied + Just member -> unless (member `hasPermission` DownloadTeamMembersCsv) $ throwS @'AccessDenied + + chan <- embedFinal newChan + cache <- embedFinal $ newIORef mempty + + let encodeRow r = encodeDefaultOrderedByNameWith customEncodeOptions [r] + let produceTeamExportUsers = do + embedFinal $ writeChan chan (Just headerLine) + E.withChunks (\mps -> listTeamMembers @InternalPaging tid mps maxBound) $ + \members -> unsafePooledForConcurrentlyN_ 8 members $ \member -> do + mRecord <- + runErrorS @TeamMemberNotFound $ + getUserRecord cache member + let mRow = encodeRow <$> mRecord + when (isJust mRow) $ + embedFinal $ + writeChan chan mRow + + -- In case an exception is thrown inside the producer thread, the response + -- will not contain a correct error message, but rather be an http error such + -- as 'InvalidChunkHeaders'. The exception however still reaches the + -- middleware and is being tracked in logging and metrics. + let producerThread = + produceTeamExportUsers + `finally` embedFinal (writeChan chan Nothing) + + asyncToIOFinal . resourceToIOFinal . unsafelyPerformConcurrency @_ @Unsafe $ do + -- Here we should really capture the Wai continuation and run the finaliser + -- after that. Unfortunately, this is not really possible with Servant, + -- because the continuation is not exposed by the Handler monad. The best + -- we can do is return a Codensity value with the correct finaliser, but + -- that still leaves a short window between when the resource is acquired + -- and when the finaliser is installed where the resource might be leaked. + -- I don't have a good solution for that. + bracketOnError + (async producerThread) + cancel + $ \producer -> do + pure $ do + void $ Codensity $ \k -> do + r <- k () + Async.cancel producer + pure r + pure $ \write flush -> do + let go = do + readChan chan >>= \case + Nothing -> write "" >> flush + Just line -> write (byteString (toStrict line)) >> flush >> go + go + +headerLine :: LByteString +headerLine = encodeDefaultOrderedByNameWith (customEncodeOptions {encIncludeHeader = True}) ([] :: [TeamExportUser]) + +customEncodeOptions :: EncodeOptions +customEncodeOptions = + EncodeOptions + { encDelimiter = fromIntegral (ord ','), + encUseCrLf = True, -- to be compatible with Mac and Windows + encIncludeHeader = False, -- (so we can flush when the header is on the wire) + encQuoting = QuoteAll + } diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index aa65a594ed4..01edafee051 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -16,23 +18,19 @@ -- with this program. If not, see . module Galley.API.Teams.Features - ( getFeatureStatus, - getFeatureStatusMulti, - setFeatureStatus, - setFeatureStatusInternal, - patchFeatureStatusInternal, - getFeatureStatusForUser, - getAllFeatureConfigsForTeam, - getAllFeatureConfigsForUser, + ( getFeatureMulti, + setFeature, + setFeatureInternal, + patchFeatureInternal, + getAllTeamFeaturesForTeam, + getAllTeamFeaturesForUser, updateLockStatus, - -- Don't export methods of this typeclass - GetFeatureConfig, - -- Don't export methods of this typeclass - SetFeatureConfig, + GetFeatureConfig (..), + SetFeatureConfig (..), guardSecondFactorDisabled, - DoAuth (..), featureEnabledForTeam, guardMlsE2EIdConfig, + initialiseTeamFeatures, ) where @@ -43,12 +41,10 @@ import Data.Id import Data.Json.Util import Data.Kind import Data.Qualified (Local) -import Data.Schema import Data.Time (UTCTime) -import GHC.TypeLits (KnownSymbol) import Galley.API.Error (InternalError) import Galley.API.LegalHold qualified as LegalHold -import Galley.API.Teams (ensureNotTooLargeToActivateLegalHold) +import Galley.API.LegalHold.Team qualified as LegalHold import Galley.API.Teams.Features.Get import Galley.API.Util (assertTeamExists, getTeamMembersForFanout, membersToRecipients, permissionCheck) import Galley.App @@ -56,8 +52,8 @@ import Galley.Effects import Galley.Effects.BrigAccess (updateSearchVisibilityInbound) import Galley.Effects.SearchVisibilityStore qualified as SearchVisibilityData import Galley.Effects.TeamFeatureStore -import Galley.Effects.TeamFeatureStore qualified as TeamFeatures import Galley.Effects.TeamStore (getLegalHoldFlag, getTeamMember) +import Galley.Options import Galley.Types.Teams import Imports import Polysemy @@ -68,7 +64,7 @@ import System.Logger.Class qualified as Log import Wire.API.Conversation.Role (Action (RemoveConversationMember)) import Wire.API.Error (ErrorS) import Wire.API.Error.Galley -import Wire.API.Event.FeatureConfig qualified as Event +import Wire.API.Event.FeatureConfig import Wire.API.Federation.Error import Wire.API.Team.Feature import Wire.API.Team.Member @@ -76,84 +72,97 @@ import Wire.NotificationSubsystem import Wire.Sem.Paging import Wire.Sem.Paging.Cassandra -patchFeatureStatusInternal :: +patchFeatureInternal :: forall cfg r. ( SetFeatureConfig cfg, - GetConfigForTeamConstraints cfg r, - SetConfigForTeamConstraints cfg r, - Member (ErrorS 'NotATeamMember) r, + ComputeFeatureConstraints cfg r, + SetFeatureForTeamConstraints cfg r, Member (ErrorS 'TeamNotFound) r, + Member (Input Opts) r, Member TeamStore r, Member TeamFeatureStore r, - Member (P.Logger (Log.Msg -> Log.Msg)) r, + Member P.TinyLog r, Member NotificationSubsystem r ) => TeamId -> - WithStatusPatch cfg -> - Sem r (WithStatus cfg) -patchFeatureStatusInternal tid patch = do + LockableFeaturePatch cfg -> + Sem r (LockableFeature cfg) +patchFeatureInternal tid patch = do assertTeamExists tid - currentFeatureStatus <- getFeatureStatus @cfg DontDoAuth tid + currentFeatureStatus <- getFeatureForTeam @cfg tid let newFeatureStatus = applyPatch currentFeatureStatus - -- setting the config can fail, so we need to do it first - void $ setConfigForTeam @cfg tid (forgetLock newFeatureStatus) - when (isJust $ wspLockStatus patch) $ void $ updateLockStatus @cfg tid (wsLockStatus newFeatureStatus) - getFeatureStatus @cfg DontDoAuth tid + setFeatureForTeam @cfg tid newFeatureStatus where - applyPatch :: WithStatus cfg -> WithStatus cfg + applyPatch :: LockableFeature cfg -> LockableFeature cfg applyPatch current = current - & setStatus (fromMaybe (wsStatus current) (wspStatus patch)) - & setLockStatus (fromMaybe (wsLockStatus current) (wspLockStatus patch)) - & setConfig (fromMaybe (wsConfig current) (wspConfig patch)) - & setWsTTL (fromMaybe (wsTTL current) (wspTTL patch)) + { status = fromMaybe current.status patch.status, + lockStatus = fromMaybe current.lockStatus patch.lockStatus, + config = fromMaybe current.config patch.config + } -setFeatureStatus :: +setFeature :: forall cfg r. ( SetFeatureConfig cfg, - GetConfigForTeamConstraints cfg r, - SetConfigForTeamConstraints cfg r, + ComputeFeatureConstraints cfg r, + SetFeatureForTeamConstraints cfg r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS OperationDenied) r, - Member (ErrorS 'TeamNotFound) r, Member (Error TeamFeatureError) r, + Member (Input Opts) r, Member TeamStore r, Member TeamFeatureStore r, - Member (P.Logger (Log.Msg -> Log.Msg)) r, + Member P.TinyLog r, Member NotificationSubsystem r ) => - DoAuth -> + UserId -> TeamId -> - WithStatusNoLock cfg -> - Sem r (WithStatus cfg) -setFeatureStatus doauth tid wsnl = do - case doauth of - DoAuth uid -> do - zusrMembership <- getTeamMember tid uid - void $ permissionCheck ChangeTeamFeature zusrMembership - DontDoAuth -> - assertTeamExists tid - guardLockStatus . wsLockStatus =<< getConfigForTeam @cfg tid - setConfigForTeam @cfg tid wsnl - -setFeatureStatusInternal :: + Feature cfg -> + Sem r (LockableFeature cfg) +setFeature uid tid feat = do + zusrMembership <- getTeamMember tid uid + void $ permissionCheck ChangeTeamFeature zusrMembership + setFeatureUnchecked tid feat + +setFeatureInternal :: forall cfg r. ( SetFeatureConfig cfg, - GetConfigForTeamConstraints cfg r, - SetConfigForTeamConstraints cfg r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS OperationDenied) r, + ComputeFeatureConstraints cfg r, + SetFeatureForTeamConstraints cfg r, Member (ErrorS 'TeamNotFound) r, Member (Error TeamFeatureError) r, + Member (Input Opts) r, + Member TeamStore r, + Member TeamFeatureStore r, + Member P.TinyLog r, + Member NotificationSubsystem r + ) => + TeamId -> + Feature cfg -> + Sem r (LockableFeature cfg) +setFeatureInternal tid feat = do + assertTeamExists tid + setFeatureUnchecked tid feat + +setFeatureUnchecked :: + forall cfg r. + ( SetFeatureConfig cfg, + ComputeFeatureConstraints cfg r, + SetFeatureForTeamConstraints cfg r, + Member (Error TeamFeatureError) r, + Member (Input Opts) r, Member TeamStore r, Member TeamFeatureStore r, Member (P.Logger (Log.Msg -> Log.Msg)) r, Member NotificationSubsystem r ) => TeamId -> - WithStatusNoLock cfg -> - Sem r (WithStatus cfg) -setFeatureStatusInternal = setFeatureStatus @cfg DontDoAuth + Feature cfg -> + Sem r (LockableFeature cfg) +setFeatureUnchecked tid feat = do + feat0 <- getFeatureForTeam @cfg tid + guardLockStatus feat0.lockStatus + setFeatureForTeam @cfg tid (withLockStatus feat0.lockStatus feat) updateLockStatus :: forall cfg r. @@ -167,44 +176,38 @@ updateLockStatus :: Sem r LockStatusResponse updateLockStatus tid lockStatus = do assertTeamExists tid - TeamFeatures.setFeatureLockStatus (featureSingleton @cfg) tid lockStatus + setFeatureLockStatus @cfg tid lockStatus pure $ LockStatusResponse lockStatus -persistAndPushEvent :: +persistFeature :: forall cfg r. - ( KnownSymbol (FeatureSymbol cfg), - ToSchema cfg, - GetFeatureConfig cfg, - GetConfigForTeamConstraints cfg r, - Member TeamFeatureStore r, - Member (P.Logger (Log.Msg -> Log.Msg)) r, - Member NotificationSubsystem r, - Member TeamStore r + ( GetFeatureConfig cfg, + ComputeFeatureConstraints cfg r, + Member (Input Opts) r, + Member TeamFeatureStore r ) => TeamId -> - WithStatusNoLock cfg -> - Sem r (WithStatus cfg) -persistAndPushEvent tid wsnl = do - setFeatureConfig (featureSingleton @cfg) tid wsnl - fs <- getConfigForTeam @cfg tid - pushFeatureConfigEvent tid (Event.mkUpdateEvent fs) - pure fs - -pushFeatureConfigEvent :: + LockableFeature cfg -> + Sem r (LockableFeature cfg) +persistFeature tid feat = do + setDbFeature tid feat + getFeatureForTeam @cfg tid + +pushFeatureEvent :: ( Member NotificationSubsystem r, Member TeamStore r, Member P.TinyLog r ) => TeamId -> - Event.Event -> + Event -> Sem r () -pushFeatureConfigEvent tid event = do +pushFeatureEvent tid event = do memList <- getTeamMembersForFanout tid if ((memList ^. teamMemberListType) == ListTruncated) then do P.warn $ Log.field "action" (Log.val "Features.pushFeatureConfigEvent") - . Log.field "feature" (Log.val (toByteString' . Event._eventFeatureName $ event)) + . Log.field "feature" (Log.val (toByteString' . _eventFeatureName $ event)) . Log.field "team" (Log.val (UTF8.fromString . show $ tid)) . Log.msg @Text "Fanout limit exceeded. Events will not be sent." else do @@ -222,63 +225,87 @@ guardLockStatus = \case LockStatusUnlocked -> pure () LockStatusLocked -> throw FeatureLocked +setFeatureForTeam :: + ( SetFeatureConfig cfg, + SetFeatureForTeamConstraints cfg r, + ComputeFeatureConstraints cfg r, + Member (Input Opts) r, + Member P.TinyLog r, + Member NotificationSubsystem r, + Member TeamFeatureStore r, + Member TeamStore r + ) => + TeamId -> + LockableFeature cfg -> + Sem r (LockableFeature cfg) +setFeatureForTeam tid feat = do + preparedFeat <- prepareFeature tid feat + newFeat <- persistFeature tid preparedFeat + pushFeatureEvent tid (mkUpdateEvent newFeat) + pure newFeat + +initialiseTeamFeatures :: + ( Member (Input Opts) r, + Member TeamFeatureStore r + ) => + TeamId -> + Sem r () +initialiseTeamFeatures tid = do + flags :: FeatureFlags <- inputs $ view (settings . featureFlags) + + -- set MLS initial config + let MLSDefaults fdef = npProject flags + let feat = initialFeature fdef + setDbFeature tid feat + pure () + ------------------------------------------------------------------------------- -- SetFeatureConfig instances -- | Don't export methods of this typeclass class (GetFeatureConfig cfg) => SetFeatureConfig cfg where - type SetConfigForTeamConstraints cfg (r :: EffectRow) :: Constraint - type SetConfigForTeamConstraints cfg (r :: EffectRow) = () - - -- | This method should generate the side-effects of changing the feature and - -- also (depending on the feature) persist the new setting to the database and - -- push a event to clients (see 'persistAndPushEvent'). - setConfigForTeam :: - ( SetConfigForTeamConstraints cfg r, - GetConfigForTeamConstraints cfg r, - ( Member TeamFeatureStore r, - Member (P.Logger (Log.Msg -> Log.Msg)) r, - Member NotificationSubsystem r, - Member TeamStore r - ) - ) => + type SetFeatureForTeamConstraints cfg (r :: EffectRow) :: Constraint + type SetFeatureForTeamConstraints cfg (r :: EffectRow) = () + + -- | This method takes a feature about to be set, performs the required + -- checks, makes any related updates via the internal API, then finally + -- returns the feature to be persisted and pushed to clients. + -- + -- The default simply returns the original feature unchanged, which should be + -- enough for most features. + prepareFeature :: + (SetFeatureForTeamConstraints cfg r) => TeamId -> - WithStatusNoLock cfg -> - Sem r (WithStatus cfg) - default setConfigForTeam :: - ( GetConfigForTeamConstraints cfg r, - KnownSymbol (FeatureSymbol cfg), - ToSchema cfg, - Members - '[ TeamFeatureStore, - P.Logger (Log.Msg -> Log.Msg), - NotificationSubsystem, - TeamStore - ] - r - ) => - TeamId -> - WithStatusNoLock cfg -> - Sem r (WithStatus cfg) - setConfigForTeam tid wsnl = persistAndPushEvent tid wsnl + LockableFeature cfg -> + Sem r (LockableFeature cfg) + default prepareFeature :: TeamId -> LockableFeature cfg -> Sem r (LockableFeature cfg) + prepareFeature _tid feat = pure feat instance SetFeatureConfig SSOConfig where - type SetConfigForTeamConstraints SSOConfig (r :: EffectRow) = (Member (Error TeamFeatureError) r) + type + SetFeatureForTeamConstraints SSOConfig (r :: EffectRow) = + ( Member (Input Opts) r, + Member (Error TeamFeatureError) r + ) - setConfigForTeam tid wsnl = do - case wssStatus wsnl of + prepareFeature _tid feat = do + case feat.status of FeatureStatusEnabled -> pure () FeatureStatusDisabled -> throw DisableSsoNotImplemented - persistAndPushEvent tid wsnl + pure feat instance SetFeatureConfig SearchVisibilityAvailableConfig where - type SetConfigForTeamConstraints SearchVisibilityAvailableConfig (r :: EffectRow) = (Member SearchVisibilityStore r) + type + SetFeatureForTeamConstraints SearchVisibilityAvailableConfig (r :: EffectRow) = + ( Member SearchVisibilityStore r, + Member (Input Opts) r + ) - setConfigForTeam tid wsnl = do - case wssStatus wsnl of + prepareFeature tid feat = do + case feat.status of FeatureStatusEnabled -> pure () FeatureStatusDisabled -> SearchVisibilityData.resetSearchVisibility tid - persistAndPushEvent tid wsnl + pure feat instance SetFeatureConfig ValidateSAMLEmailsConfig @@ -286,14 +313,13 @@ instance SetFeatureConfig DigitalSignaturesConfig instance SetFeatureConfig LegalholdConfig where type - SetConfigForTeamConstraints LegalholdConfig (r :: EffectRow) = + SetFeatureForTeamConstraints LegalholdConfig (r :: EffectRow) = ( Bounded (PagingBounds InternalPaging TeamMember), Member BackendNotificationQueueAccess r, Member BotAccess r, Member BrigAccess r, Member CodeStore r, Member ConversationStore r, - Member (Error AuthenticationError) r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, @@ -321,11 +347,11 @@ instance SetFeatureConfig LegalholdConfig where Member TeamStore r, Member (TeamMemberStore InternalPaging) r, Member P.TinyLog r, - Member Random r + Member Random r, + Member (Embed IO) r ) - -- we're good to update the status now. - setConfigForTeam tid wsnl = do + prepareFeature tid feat = do -- this extra do is to encapsulate the assertions running before the actual operation. -- enabling LH for teams is only allowed in normal operation; disabled-permanently and -- whitelist-teams have no or their own way to do that, resp. @@ -338,20 +364,20 @@ instance SetFeatureConfig LegalholdConfig where FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> do throw LegalHoldWhitelistedOnly - case wssStatus wsnl of + case feat.status of FeatureStatusDisabled -> LegalHold.removeSettings' @InternalPaging tid - FeatureStatusEnabled -> ensureNotTooLargeToActivateLegalHold tid - persistAndPushEvent tid wsnl + FeatureStatusEnabled -> LegalHold.ensureNotTooLargeToActivateLegalHold tid + pure feat instance SetFeatureConfig FileSharingConfig instance SetFeatureConfig AppLockConfig where - type SetConfigForTeamConstraints AppLockConfig r = Member (Error TeamFeatureError) r + type SetFeatureForTeamConstraints AppLockConfig r = Member (Error TeamFeatureError) r - setConfigForTeam tid wsnl = do - when ((applockInactivityTimeoutSecs . wssConfig $ wsnl) < 30) $ + prepareFeature _tid feat = do + when ((applockInactivityTimeoutSecs feat.config) < 30) $ throw AppLockInactivityTimeoutTooLow - persistAndPushEvent tid wsnl + pure feat instance SetFeatureConfig ConferenceCallingConfig @@ -362,23 +388,28 @@ instance SetFeatureConfig GuestLinksConfig instance SetFeatureConfig SndFactorPasswordChallengeConfig instance SetFeatureConfig SearchVisibilityInboundConfig where - type SetConfigForTeamConstraints SearchVisibilityInboundConfig (r :: EffectRow) = (Member BrigAccess r) - setConfigForTeam tid wsnl = do - updateSearchVisibilityInbound $ toTeamStatus tid wsnl - persistAndPushEvent tid wsnl + type SetFeatureForTeamConstraints SearchVisibilityInboundConfig (r :: EffectRow) = (Member BrigAccess r) + prepareFeature tid feat = do + updateSearchVisibilityInbound $ toTeamStatus tid feat + pure feat instance SetFeatureConfig MLSConfig where - type SetConfigForTeamConstraints MLSConfig (r :: EffectRow) = (Member (Error TeamFeatureError) r) - setConfigForTeam tid wsnl = do - mlsMigrationConfig <- getConfigForTeam @MlsMigrationConfig tid + type + SetFeatureForTeamConstraints MLSConfig (r :: EffectRow) = + ( Member (Input Opts) r, + Member TeamFeatureStore r, + Member (Error TeamFeatureError) r + ) + prepareFeature tid feat = do + mlsMigrationConfig <- getFeatureForTeam @MlsMigrationConfig tid unless ( -- default protocol needs to be included in supported protocols - mlsDefaultProtocol (wssConfig wsnl) `elem` mlsSupportedProtocols (wssConfig wsnl) + feat.config.mlsDefaultProtocol `elem` feat.config.mlsSupportedProtocols -- when MLS migration is enabled, MLS needs to be enabled as well - && (wsStatus mlsMigrationConfig == FeatureStatusDisabled || wssStatus wsnl == FeatureStatusEnabled) + && (mlsMigrationConfig.status == FeatureStatusDisabled || feat.status == FeatureStatusEnabled) ) $ throw MLSProtocolMismatch - persistAndPushEvent tid wsnl + pure feat instance SetFeatureConfig ExposeInvitationURLsToTeamAdminConfig @@ -389,26 +420,41 @@ instance SetFeatureConfig MlsE2EIdConfig guardMlsE2EIdConfig :: forall r a. (Member (Error TeamFeatureError) r) => - (UserId -> TeamId -> WithStatusNoLock MlsE2EIdConfig -> Sem r a) -> + (UserId -> TeamId -> Feature MlsE2EIdConfig -> Sem r a) -> UserId -> TeamId -> - WithStatusNoLock MlsE2EIdConfig -> + Feature MlsE2EIdConfig -> Sem r a -guardMlsE2EIdConfig handler uid tid conf = do - when (isNothing . crlProxy . wssConfig $ conf) $ throw MLSE2EIDMissingCrlProxy - handler uid tid conf +guardMlsE2EIdConfig handler uid tid feat = do + when (isNothing feat.config.crlProxy) $ throw MLSE2EIDMissingCrlProxy + handler uid tid feat instance SetFeatureConfig MlsMigrationConfig where - type SetConfigForTeamConstraints MlsMigrationConfig (r :: EffectRow) = (Member (Error TeamFeatureError) r) - setConfigForTeam tid wsnl = do - mlsConfig <- getConfigForTeam @MLSConfig tid + type + SetFeatureForTeamConstraints MlsMigrationConfig (r :: EffectRow) = + ( Member (Input Opts) r, + Member (Error TeamFeatureError) r, + Member TeamFeatureStore r + ) + prepareFeature tid feat = do + mlsConfig <- getFeatureForTeam @MLSConfig tid unless ( -- when MLS migration is enabled, MLS needs to be enabled as well - wssStatus wsnl == FeatureStatusDisabled || wsStatus mlsConfig == FeatureStatusEnabled + feat.status == FeatureStatusDisabled || mlsConfig.status == FeatureStatusEnabled ) $ throw MLSProtocolMismatch - persistAndPushEvent tid wsnl + pure feat -instance SetFeatureConfig EnforceFileDownloadLocationConfig +instance SetFeatureConfig EnforceFileDownloadLocationConfig where + type + SetFeatureForTeamConstraints EnforceFileDownloadLocationConfig r = + (Member (Error TeamFeatureError) r) + + prepareFeature _ feat = do + -- empty download location is not allowed + -- this is consistent with all other features, and least surprising for clients + when (feat.config.enforcedDownloadLocation == Just "") $ do + throw EmptyDownloadLocation + pure feat instance SetFeatureConfig LimitedEventFanoutConfig diff --git a/services/galley/src/Galley/API/Teams/Features/Get.hs b/services/galley/src/Galley/API/Teams/Features/Get.hs index 685266b3ea1..d4d448fd700 100644 --- a/services/galley/src/Galley/API/Teams/Features/Get.hs +++ b/services/galley/src/Galley/API/Teams/Features/Get.hs @@ -1,3 +1,6 @@ +{-# LANGUAGE UndecidableSuperClasses #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -16,13 +19,16 @@ -- with this program. If not, see . module Galley.API.Teams.Features.Get - ( getFeatureStatus, - getFeatureStatusMulti, - getFeatureStatusForUser, - getAllFeatureConfigsForServer, - getAllFeatureConfigsForTeam, - getAllFeatureConfigsForUser, + ( getFeature, + getFeatureInternal, + getFeatureMulti, + getAllTeamFeaturesForServer, + getAllTeamFeaturesForTeam, + getAllTeamFeaturesForUser, + getSingleFeatureForUser, GetFeatureConfig (..), + getFeatureForTeam, + getFeatureForServer, guardSecondFactorDisabled, DoAuth (..), featureEnabledForTeam, @@ -30,215 +36,202 @@ module Galley.API.Teams.Features.Get ) where +import Control.Error (hush) import Control.Lens -import Data.Bifunctor (second) +import Data.Default import Data.Id import Data.Kind import Data.Qualified (Local, tUnqualified) -import Galley.API.LegalHold.Team (isLegalHoldEnabledForTeam) +import Data.SOP +import Data.Tagged +import Galley.API.LegalHold.Team import Galley.API.Util import Galley.Effects import Galley.Effects.BrigAccess (getAccountConferenceCallingConfigClient) import Galley.Effects.ConversationStore as ConversationStore -import Galley.Effects.TeamFeatureStore qualified as TeamFeatures -import Galley.Effects.TeamStore (getOneUserTeam, getTeam, getTeamMember) +import Galley.Effects.TeamFeatureStore +import Galley.Effects.TeamStore (getOneUserTeam, getTeamMember) import Galley.Options import Galley.Types.Teams import Imports import Polysemy +import Polysemy.Error import Polysemy.Input import Wire.API.Conversation (cnvmTeam) -import Wire.API.Error (ErrorS, throwS) +import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Multi import Wire.API.Team.Feature data DoAuth = DoAuth UserId | DontDoAuth +type DefaultGetFeatureForUserConstraints cfg r = + ( Member (Input Opts) r, + Member TeamFeatureStore r, + ComputeFeatureConstraints cfg r + ) + -- | Don't export methods of this typeclass -class (IsFeatureConfig cfg) => GetFeatureConfig cfg where - type GetConfigForTeamConstraints cfg (r :: EffectRow) :: Constraint +class + ( IsFeatureConfig cfg, + GetFeatureDefaults (FeatureDefaults cfg), + NpProject cfg Features + ) => + GetFeatureConfig cfg + where + type GetFeatureForUserConstraints cfg (r :: EffectRow) :: Constraint type - GetConfigForTeamConstraints cfg (r :: EffectRow) = - ( Member (Input Opts) r, - Member TeamFeatureStore r - ) + GetFeatureForUserConstraints cfg (r :: EffectRow) = + DefaultGetFeatureForUserConstraints cfg r - type GetConfigForUserConstraints cfg (r :: EffectRow) :: Constraint - type - GetConfigForUserConstraints cfg (r :: EffectRow) = - ( Member (Input Opts) r, - Member (ErrorS OperationDenied) r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'TeamNotFound) r, - Member TeamStore r, - Member TeamFeatureStore r - ) + type ComputeFeatureConstraints cfg (r :: EffectRow) :: Constraint + type ComputeFeatureConstraints cfg r = () - getConfigForServer :: - (Member (Input Opts) r) => - Sem r (WithStatus cfg) - -- only override if there is additional business logic for getting the feature config - -- and/or if the feature flag is configured for the backend in 'FeatureFlags' for galley in 'Galley.Types.Teams' - -- otherwise this will return the default config from wire-api - default getConfigForServer :: Sem r (WithStatus cfg) - getConfigForServer = pure defFeatureStatus - - getConfigForTeam :: - (GetConfigForTeamConstraints cfg r) => - TeamId -> - Sem r (WithStatus cfg) - default getConfigForTeam :: - ( Member (Input Opts) r, - Member TeamFeatureStore r - ) => - TeamId -> - Sem r (WithStatus cfg) - getConfigForTeam = genericGetConfigForTeam - - getConfigForUser :: - (GetConfigForUserConstraints cfg r) => + getFeatureForUser :: + (GetFeatureForUserConstraints cfg r) => UserId -> - Sem r (WithStatus cfg) - default getConfigForUser :: - ( Member (Input Opts) r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'TeamNotFound) r, - Member TeamStore r, - Member TeamFeatureStore r - ) => + Sem r (LockableFeature cfg) + default getFeatureForUser :: + (DefaultGetFeatureForUserConstraints cfg r) => UserId -> - Sem r (WithStatus cfg) - getConfigForUser = genericGetConfigForUser + Sem r (LockableFeature cfg) + getFeatureForUser _ = getFeatureForServer -getFeatureStatus :: + computeFeature :: + (ComputeFeatureConstraints cfg r) => + TeamId -> + LockableFeature cfg -> + DbFeature cfg -> + Sem r (LockableFeature cfg) + default computeFeature :: + TeamId -> + LockableFeature cfg -> + DbFeature cfg -> + Sem r (LockableFeature cfg) + computeFeature _tid defFeature dbFeature = + pure $ + genericComputeFeature @cfg defFeature dbFeature + +getFeature :: forall cfg r. ( GetFeatureConfig cfg, - GetConfigForTeamConstraints cfg r, - ( Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'TeamNotFound) r, - Member TeamStore r - ) + ComputeFeatureConstraints cfg r, + Member (Input Opts) r, + Member TeamFeatureStore r, + Member (ErrorS 'NotATeamMember) r, + Member TeamStore r + ) => + UserId -> + TeamId -> + Sem r (LockableFeature cfg) +getFeature uid tid = do + void $ getTeamMember tid uid >>= noteS @'NotATeamMember + getFeatureForTeam @cfg tid + +getFeatureInternal :: + ( GetFeatureConfig cfg, + ComputeFeatureConstraints cfg r, + Member (Input Opts) r, + Member (ErrorS 'TeamNotFound) r, + Member TeamFeatureStore r, + Member TeamStore r ) => - DoAuth -> TeamId -> - Sem r (WithStatus cfg) -getFeatureStatus doauth tid = do - case doauth of - DoAuth uid -> - getTeamMember tid uid >>= maybe (throwS @'NotATeamMember) (const $ pure ()) - DontDoAuth -> - assertTeamExists tid - getConfigForTeam @cfg tid - -getFeatureStatusMulti :: + Sem r (LockableFeature cfg) +getFeatureInternal tid = do + assertTeamExists tid + getFeatureForTeam tid + +getFeatureMulti :: forall cfg r. ( GetFeatureConfig cfg, + ComputeFeatureConstraints cfg r, Member (Input Opts) r, Member TeamFeatureStore r ) => Multi.TeamFeatureNoConfigMultiRequest -> Sem r (Multi.TeamFeatureNoConfigMultiResponse cfg) -getFeatureStatusMulti (Multi.TeamFeatureNoConfigMultiRequest tids) = do - cfgs <- genericGetConfigForMultiTeam @cfg tids - let xs = uncurry toTeamStatus . second forgetLock <$> cfgs +getFeatureMulti (Multi.TeamFeatureNoConfigMultiRequest tids) = do + cfgs <- getFeatureForMultiTeam @cfg tids + let xs = uncurry toTeamStatus <$> cfgs pure $ Multi.TeamFeatureNoConfigMultiResponse xs -toTeamStatus :: TeamId -> WithStatusNoLock cfg -> Multi.TeamStatus cfg -toTeamStatus tid ws = Multi.TeamStatus tid (wssStatus ws) +toTeamStatus :: TeamId -> LockableFeature cfg -> Multi.TeamStatus cfg +toTeamStatus tid feat = Multi.TeamStatus tid feat.status --- | For individual users to get feature config for their account (personal or team). --- This looks supposedly redundant to the implementations of `getConfigForUser` but it's not. --- Here we explicitly return the team setting if the user is a team member. --- In `getConfigForUser` this is mostly also the case. But there are exceptions, e.g. `ConferenceCallingConfig` -getFeatureStatusForUser :: - forall cfg r. - ( Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'TeamNotFound) r, - Member TeamStore r, - GetConfigForTeamConstraints cfg r, - GetConfigForUserConstraints cfg r, - GetFeatureConfig cfg +getTeamAndCheckMembership :: + ( Member TeamStore r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TeamNotFound) r ) => UserId -> - Sem r (WithStatus cfg) -getFeatureStatusForUser zusr = do - mbTeam <- getOneUserTeam zusr - case mbTeam of - Nothing -> - getConfigForUser @cfg zusr - Just tid -> do - zusrMembership <- getTeamMember tid zusr - void $ maybe (throwS @'NotATeamMember) pure zusrMembership - assertTeamExists tid - getConfigForTeam @cfg tid - -getAllFeatureConfigsForUser :: + Sem r (Maybe TeamId) +getTeamAndCheckMembership uid = do + mTid <- getOneUserTeam uid + for_ mTid $ \tid -> do + zusrMembership <- getTeamMember tid uid + void $ maybe (throwS @'NotATeamMember) pure zusrMembership + assertTeamExists tid + pure mTid + +getAllTeamFeaturesForTeam :: forall r. - ( Member BrigAccess r, + ( Member (Input Opts) r, Member (ErrorS 'NotATeamMember) r, - Member (ErrorS OperationDenied) r, - Member (ErrorS 'TeamNotFound) r, - Member (Input Opts) r, Member LegalHoldStore r, Member TeamFeatureStore r, Member TeamStore r ) => - UserId -> - Sem r AllFeatureConfigs -getAllFeatureConfigsForUser zusr = do - mbTeam <- getOneUserTeam zusr - when (isJust mbTeam) $ do - zusrMembership <- maybe (pure Nothing) (`getTeamMember` zusr) mbTeam - maybe (throwS @'NotATeamMember) (const $ pure ()) zusrMembership - case mbTeam of - Just tid -> - TeamFeatures.getAllFeatureConfigs tid - Nothing -> - getAllFeatureConfigsUser zusr - -getAllFeatureConfigsForTeam :: - forall r. - ( Member (ErrorS 'NotATeamMember) r, - Member TeamFeatureStore r, - Member TeamStore r - ) => Local UserId -> TeamId -> - Sem r AllFeatureConfigs -getAllFeatureConfigsForTeam luid tid = do - zusrMembership <- getTeamMember tid (tUnqualified luid) - maybe (throwS @'NotATeamMember) (const $ pure ()) zusrMembership - TeamFeatures.getAllFeatureConfigs tid + Sem r AllTeamFeatures +getAllTeamFeaturesForTeam luid tid = do + void $ getTeamMember tid (tUnqualified luid) >>= noteS @'NotATeamMember + getAllTeamFeatures tid -getAllFeatureConfigsForServer :: +class + (GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => + GetAllFeaturesForServerConstraints r cfg + +instance + (GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => + GetAllFeaturesForServerConstraints r cfg + +getAllTeamFeaturesForServer :: forall r. (Member (Input Opts) r) => - Sem r AllFeatureConfigs -getAllFeatureConfigsForServer = - AllFeatureConfigs - <$> getConfigForServer @LegalholdConfig - <*> getConfigForServer @SSOConfig - <*> getConfigForServer @SearchVisibilityAvailableConfig - <*> getConfigForServer @SearchVisibilityInboundConfig - <*> getConfigForServer @ValidateSAMLEmailsConfig - <*> getConfigForServer @DigitalSignaturesConfig - <*> getConfigForServer @AppLockConfig - <*> getConfigForServer @FileSharingConfig - <*> getConfigForServer @ClassifiedDomainsConfig - <*> getConfigForServer @ConferenceCallingConfig - <*> getConfigForServer @SelfDeletingMessagesConfig - <*> getConfigForServer @GuestLinksConfig - <*> getConfigForServer @SndFactorPasswordChallengeConfig - <*> getConfigForServer @MLSConfig - <*> getConfigForServer @ExposeInvitationURLsToTeamAdminConfig - <*> getConfigForServer @OutlookCalIntegrationConfig - <*> getConfigForServer @MlsE2EIdConfig - <*> getConfigForServer @MlsMigrationConfig - <*> getConfigForServer @EnforceFileDownloadLocationConfig - <*> getConfigForServer @LimitedEventFanoutConfig - -getAllFeatureConfigsUser :: + Sem r AllTeamFeatures +getAllTeamFeaturesForServer = + hsequence' $ + hcpure (Proxy @GetFeatureConfig) $ + Comp getFeatureForServer + +getAllTeamFeatures :: + forall r. + ( Member (Input Opts) r, + Member LegalHoldStore r, + Member TeamFeatureStore r, + Member TeamStore r + ) => + TeamId -> + Sem r AllTeamFeatures +getAllTeamFeatures tid = do + features <- getAllDbFeatures tid + defFeatures <- getAllTeamFeaturesForServer + hsequence' $ hcliftA2 (Proxy @(GetAllFeaturesForServerConstraints r)) compute defFeatures features + where + compute :: + (ComputeFeatureConstraints p r, GetFeatureConfig p) => + LockableFeature p -> + DbFeature p -> + (Sem r :.: LockableFeature) p + compute defFeature feat = Comp $ computeFeature tid defFeature feat + +class (GetFeatureForUserConstraints cfg r, GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => GetAllTeamFeaturesForUserConstraints r cfg + +instance (GetFeatureForUserConstraints cfg r, GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => GetAllTeamFeaturesForUserConstraints r cfg + +getAllTeamFeaturesForUser :: forall r. ( Member BrigAccess r, Member (ErrorS 'NotATeamMember) r, @@ -250,117 +243,98 @@ getAllFeatureConfigsUser :: Member TeamStore r ) => UserId -> - Sem r AllFeatureConfigs -getAllFeatureConfigsUser uid = - AllFeatureConfigs - <$> getConfigForUser @LegalholdConfig uid - <*> getConfigForUser @SSOConfig uid - <*> getConfigForUser @SearchVisibilityAvailableConfig uid - <*> getConfigForUser @SearchVisibilityInboundConfig uid - <*> getConfigForUser @ValidateSAMLEmailsConfig uid - <*> getConfigForUser @DigitalSignaturesConfig uid - <*> getConfigForUser @AppLockConfig uid - <*> getConfigForUser @FileSharingConfig uid - <*> getConfigForUser @ClassifiedDomainsConfig uid - <*> getConfigForUser @ConferenceCallingConfig uid - <*> getConfigForUser @SelfDeletingMessagesConfig uid - <*> getConfigForUser @GuestLinksConfig uid - <*> getConfigForUser @SndFactorPasswordChallengeConfig uid - <*> getConfigForUser @MLSConfig uid - <*> getConfigForUser @ExposeInvitationURLsToTeamAdminConfig uid - <*> getConfigForUser @OutlookCalIntegrationConfig uid - <*> getConfigForUser @MlsE2EIdConfig uid - <*> getConfigForUser @MlsMigrationConfig uid - <*> getConfigForUser @EnforceFileDownloadLocationConfig uid - <*> getConfigForUser @LimitedEventFanoutConfig uid - --- | Note: this is an internal function which doesn't cover all features, e.g. LegalholdConfig -genericGetConfigForTeam :: + Sem r AllTeamFeatures +getAllTeamFeaturesForUser uid = do + mTid <- getTeamAndCheckMembership uid + hsequence' $ hcpure (Proxy @(GetAllTeamFeaturesForUserConstraints r)) $ Comp $ getFeatureForTeamUser uid mTid + +getSingleFeatureForUser :: forall cfg r. - (GetFeatureConfig cfg) => - (Member TeamFeatureStore r) => - (Member (Input Opts) r) => + ( GetFeatureConfig cfg, + Member (Input Opts) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TeamNotFound) r, + Member TeamStore r, + Member TeamFeatureStore r, + GetFeatureForUserConstraints cfg r, + ComputeFeatureConstraints cfg r + ) => + UserId -> + Sem r (LockableFeature cfg) +getSingleFeatureForUser uid = do + mTid <- getTeamAndCheckMembership uid + getFeatureForTeamUser @cfg uid mTid + +getFeatureForTeam :: + forall cfg r. + ( GetFeatureConfig cfg, + ComputeFeatureConstraints cfg r, + Member (Input Opts) r, + Member TeamFeatureStore r + ) => TeamId -> - Sem r (WithStatus cfg) -genericGetConfigForTeam tid = do - computeFeatureConfigForTeamUser - <$> TeamFeatures.getFeatureConfig (featureSingleton @cfg) tid - <*> TeamFeatures.getFeatureLockStatus (featureSingleton @cfg) tid - <*> getConfigForServer - --- Note: this function assumes the feature cannot be locked -genericGetConfigForMultiTeam :: + Sem r (LockableFeature cfg) +getFeatureForTeam tid = do + dbFeature <- getDbFeature tid + defFeature <- getFeatureForServer + computeFeature @cfg + tid + defFeature + dbFeature + +getFeatureForMultiTeam :: forall cfg r. - (GetFeatureConfig cfg) => - (Member TeamFeatureStore r) => - (Member (Input Opts) r) => + ( GetFeatureConfig cfg, + ComputeFeatureConstraints cfg r, + Member TeamFeatureStore r, + Member (Input Opts) r + ) => [TeamId] -> - Sem r [(TeamId, WithStatus cfg)] -genericGetConfigForMultiTeam tids = do - def <- getConfigForServer - (\(tid, mwsnl) -> (tid, computeFeatureConfigForTeamUser mwsnl (Just LockStatusUnlocked) def)) - <$$> TeamFeatures.getFeatureConfigMulti (featureSingleton @cfg) tids - --- | Note: this is an internal function which doesn't cover all features, e.g. conference calling -genericGetConfigForUser :: + Sem r [(TeamId, LockableFeature cfg)] +getFeatureForMultiTeam tids = do + defFeature <- getFeatureForServer + features <- getDbFeatureMulti tids + for features $ \(tid, dbFeature) -> do + feat <- computeFeature @cfg tid defFeature dbFeature + pure (tid, feat) + +getFeatureForTeamUser :: forall cfg r. - ( Member (Input Opts) r, - Member TeamFeatureStore r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'TeamNotFound) r, - Member TeamStore r, - GetFeatureConfig cfg + ( GetFeatureConfig cfg, + GetFeatureForUserConstraints cfg r, + ComputeFeatureConstraints cfg r, + Member (Input Opts) r, + Member TeamFeatureStore r ) => UserId -> - Sem r (WithStatus cfg) -genericGetConfigForUser uid = do - mbTeam <- getOneUserTeam uid - case mbTeam of - Nothing -> do - getConfigForServer - Just tid -> do - zusrMembership <- getTeamMember tid uid - maybe (throwS @'NotATeamMember) (const $ pure ()) zusrMembership - assertTeamExists tid - genericGetConfigForTeam tid + Maybe TeamId -> + Sem r (LockableFeature cfg) +getFeatureForTeamUser uid Nothing = getFeatureForUser uid +getFeatureForTeamUser _ (Just tid) = getFeatureForTeam @cfg tid + +getFeatureForServer :: + forall cfg r. + ( GetFeatureDefaults (FeatureDefaults cfg), + NpProject cfg Features, + Member (Input Opts) r + ) => + Sem r (LockableFeature cfg) +getFeatureForServer = inputs $ view (settings . featureFlags . to (featureDefaults @cfg)) ------------------------------------------------------------------------------- -- GetFeatureConfig instances -instance GetFeatureConfig SSOConfig where - getConfigForServer = do - status <- - inputs (view (settings . featureFlags . flagSSO)) <&> \case - FeatureSSOEnabledByDefault -> FeatureStatusEnabled - FeatureSSODisabledByDefault -> FeatureStatusDisabled - pure $ setStatus status defFeatureStatus +instance GetFeatureConfig SSOConfig - getConfigForUser = genericGetConfigForUser +instance GetFeatureConfig SearchVisibilityAvailableConfig -instance GetFeatureConfig SearchVisibilityAvailableConfig where - getConfigForServer = do - status <- - inputs (view (settings . featureFlags . flagTeamSearchVisibility)) <&> \case - FeatureTeamSearchVisibilityAvailableByDefault -> FeatureStatusEnabled - FeatureTeamSearchVisibilityUnavailableByDefault -> FeatureStatusDisabled - pure $ setStatus status defFeatureStatus - -instance GetFeatureConfig ValidateSAMLEmailsConfig where - getConfigForServer = - inputs (view (settings . featureFlags . flagsTeamFeatureValidateSAMLEmailsStatus . unDefaults . unImplicitLockStatus)) +instance GetFeatureConfig ValidateSAMLEmailsConfig instance GetFeatureConfig DigitalSignaturesConfig instance GetFeatureConfig LegalholdConfig where type - GetConfigForTeamConstraints LegalholdConfig (r :: EffectRow) = - ( Member (Input Opts) r, - Member TeamFeatureStore r, - Member LegalHoldStore r, - Member TeamStore r - ) - type - GetConfigForUserConstraints LegalholdConfig (r :: EffectRow) = + GetFeatureForUserConstraints LegalholdConfig (r :: EffectRow) = ( Member (Input Opts) r, Member TeamFeatureStore r, Member LegalHoldStore r, @@ -369,29 +343,33 @@ instance GetFeatureConfig LegalholdConfig where Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'TeamNotFound) r ) + type + ComputeFeatureConstraints LegalholdConfig r = + (Member TeamStore r, Member LegalHoldStore r) - getConfigForTeam tid = do - status <- - isLegalHoldEnabledForTeam tid <&> \case - True -> FeatureStatusEnabled - False -> FeatureStatusDisabled - pure $ setStatus status defFeatureStatus + computeFeature tid defFeature dbFeature = do + status <- computeLegalHoldFeatureStatus tid dbFeature + pure $ defFeature {status = status} -instance GetFeatureConfig FileSharingConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagFileSharing . unDefaults) +instance GetFeatureConfig FileSharingConfig -instance GetFeatureConfig AppLockConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagAppLockDefaults . unDefaults . unImplicitLockStatus) +instance GetFeatureConfig AppLockConfig -instance GetFeatureConfig ClassifiedDomainsConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagClassifiedDomains . unImplicitLockStatus) +instance GetFeatureConfig ClassifiedDomainsConfig +-- | Conference calling gets enabled automatically once unlocked. To achieve +-- that, the default feature status in the unlocked case is forced to be +-- "enabled" before the database data is applied. +-- +-- Previously, we were assuming that this feature would be left as "unlocked", +-- and the clients were simply setting the status field. Now, the pre-existing +-- status field is reinterpreted as the lock status, which means that the +-- status will be NULL in many cases. The defaulting logic in 'computeFeature' +-- here makes sure that the status is aligned with the lock status in those +-- situations. instance GetFeatureConfig ConferenceCallingConfig where type - GetConfigForUserConstraints ConferenceCallingConfig r = + GetFeatureForUserConstraints ConferenceCallingConfig r = ( Member (Input Opts) r, Member (ErrorS OperationDenied) r, Member (ErrorS 'NotATeamMember) r, @@ -401,74 +379,48 @@ instance GetFeatureConfig ConferenceCallingConfig where Member BrigAccess r ) - getConfigForServer = - input <&> view (settings . featureFlags . flagConferenceCalling . unDefaults . unImplicitLockStatus) + getFeatureForUser uid = do + feat <- getAccountConferenceCallingConfigClient uid + pure $ withLockStatus (def @(LockableFeature ConferenceCallingConfig)).lockStatus feat - getConfigForUser uid = do - wsnl <- getAccountConferenceCallingConfigClient uid - pure $ withLockStatus (wsLockStatus (defFeatureStatus @ConferenceCallingConfig)) wsnl + computeFeature _tid defFeature dbFeature = + pure $ + let feat = applyDbFeature dbFeature defFeature {status = FeatureStatusEnabled} + in case feat.lockStatus of + LockStatusLocked -> defFeature {lockStatus = LockStatusLocked} + LockStatusUnlocked -> feat -instance GetFeatureConfig SelfDeletingMessagesConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagSelfDeletingMessages . unDefaults) +instance GetFeatureConfig SelfDeletingMessagesConfig -instance GetFeatureConfig GuestLinksConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagConversationGuestLinks . unDefaults) +instance GetFeatureConfig GuestLinksConfig -instance GetFeatureConfig SndFactorPasswordChallengeConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagTeamFeatureSndFactorPasswordChallengeStatus . unDefaults) +instance GetFeatureConfig SndFactorPasswordChallengeConfig -instance GetFeatureConfig SearchVisibilityInboundConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagTeamFeatureSearchVisibilityInbound . unDefaults . unImplicitLockStatus) +instance GetFeatureConfig SearchVisibilityInboundConfig -instance GetFeatureConfig MLSConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagMLS . unDefaults) +instance GetFeatureConfig MLSConfig instance GetFeatureConfig ExposeInvitationURLsToTeamAdminConfig where - getConfigForTeam tid = do + type + ComputeFeatureConstraints ExposeInvitationURLsToTeamAdminConfig r = + (Member (Input Opts) r) + + -- the lock status of this feature is calculated from the allow list, not the database + computeFeature tid defFeature dbFeature = do allowList <- input <&> view (settings . exposeInvitationURLsTeamAllowlist . to (fromMaybe [])) - mbOldStatus <- TeamFeatures.getFeatureConfig FeatureSingletonExposeInvitationURLsToTeamAdminConfig tid <&> fmap wssStatus let teamAllowed = tid `elem` allowList - pure $ computeConfigForTeam teamAllowed (fromMaybe FeatureStatusDisabled mbOldStatus) - where - computeConfigForTeam :: Bool -> FeatureStatus -> WithStatus ExposeInvitationURLsToTeamAdminConfig - computeConfigForTeam teamAllowed teamDbStatus = - if teamAllowed - then makeConfig LockStatusUnlocked teamDbStatus - else -- FUTUREWORK: use default feature status instead - makeConfig LockStatusLocked FeatureStatusDisabled - - makeConfig :: LockStatus -> FeatureStatus -> WithStatus ExposeInvitationURLsToTeamAdminConfig - makeConfig lockStatus status = - withStatus - status - lockStatus - ExposeInvitationURLsToTeamAdminConfig - FeatureTTLUnlimited - -instance GetFeatureConfig OutlookCalIntegrationConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagOutlookCalIntegration . unDefaults) - -instance GetFeatureConfig MlsE2EIdConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagMlsE2EId . unDefaults) - -instance GetFeatureConfig MlsMigrationConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagMlsMigration . unDefaults) - -instance GetFeatureConfig EnforceFileDownloadLocationConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagEnforceFileDownloadLocation . unDefaults) - -instance GetFeatureConfig LimitedEventFanoutConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagLimitedEventFanout . unDefaults . unImplicitLockStatus) + lockStatus = if teamAllowed then LockStatusUnlocked else LockStatusLocked + pure $ genericComputeFeature defFeature (dbFeatureLockStatus lockStatus <> dbFeature) + +instance GetFeatureConfig OutlookCalIntegrationConfig + +instance GetFeatureConfig MlsE2EIdConfig + +instance GetFeatureConfig MlsMigrationConfig + +instance GetFeatureConfig EnforceFileDownloadLocationConfig + +instance GetFeatureConfig LimitedEventFanoutConfig -- | If second factor auth is enabled, make sure that end-points that don't support it, but -- should, are blocked completely. (This is a workaround until we have 2FA for those @@ -476,43 +428,40 @@ instance GetFeatureConfig LimitedEventFanoutConfig where -- -- This function exists to resolve a cyclic dependency. guardSecondFactorDisabled :: - forall r a. - ( Member (Input Opts) r, - Member TeamFeatureStore r, + forall r. + ( Member TeamFeatureStore r, + Member (Input Opts) r, Member (ErrorS 'AccessDenied) r, - Member (ErrorS OperationDenied) r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'TeamNotFound) r, Member TeamStore r, Member ConversationStore r ) => UserId -> ConvId -> - Sem r a -> - Sem r a -guardSecondFactorDisabled uid cid action = do - mbCnvData <- ConversationStore.getConversationMetadata cid - tf <- case mbCnvData >>= cnvmTeam of - Nothing -> getConfigForUser @SndFactorPasswordChallengeConfig uid - Just tid -> do - teamExists <- isJust <$> getTeam tid - if teamExists - then getConfigForTeam @SndFactorPasswordChallengeConfig tid - else getConfigForUser @SndFactorPasswordChallengeConfig uid - case wsStatus tf of - FeatureStatusDisabled -> action + Sem r () +guardSecondFactorDisabled uid cid = do + mTid <- fmap hush . runError @() $ do + convData <- ConversationStore.getConversationMetadata cid >>= note () + tid <- note () convData.cnvmTeam + mapError (unTagged @'TeamNotFound @()) $ assertTeamExists tid + pure tid + + tf <- getFeatureForTeamUser @SndFactorPasswordChallengeConfig uid mTid + case tf.status of + FeatureStatusDisabled -> pure () FeatureStatusEnabled -> throwS @'AccessDenied featureEnabledForTeam :: forall cfg r. ( GetFeatureConfig cfg, - GetConfigForTeamConstraints cfg r, - ( Member (ErrorS OperationDenied) r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'TeamNotFound) r, - Member TeamStore r - ) + Member (Input Opts) r, + Member (ErrorS 'TeamNotFound) r, + Member TeamStore r, + Member TeamFeatureStore r, + ComputeFeatureConstraints cfg r ) => TeamId -> Sem r Bool -featureEnabledForTeam tid = (==) FeatureStatusEnabled . wsStatus <$> getFeatureStatus @cfg DontDoAuth tid +featureEnabledForTeam tid = + (==) FeatureStatusEnabled + . (.status) + <$> getFeatureInternal @cfg tid diff --git a/services/galley/src/Galley/API/Teams/Notifications.hs b/services/galley/src/Galley/API/Teams/Notifications.hs index f3e31f9ec33..38e562dacda 100644 --- a/services/galley/src/Galley/API/Teams/Notifications.hs +++ b/services/galley/src/Galley/API/Teams/Notifications.hs @@ -65,7 +65,7 @@ getTeamNotifications :: Range 1 10000 Int32 -> Sem r QueuedNotificationList getTeamNotifications zusr since size = do - tid <- (noteS @'TeamNotFound =<<) $ (userTeam . accountUser =<<) <$> Intra.getUser zusr + tid <- (noteS @'TeamNotFound =<<) $ (userTeam =<<) <$> Intra.getUser zusr page <- E.getTeamNotifications tid since size pure $ queuedNotificationList diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 6fe22e53beb..6c4280eb441 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -125,12 +125,12 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import Wire.API.Message -import Wire.API.Password (mkSafePasswordScrypt) import Wire.API.Routes.Public (ZHostValue) import Wire.API.Routes.Public.Galley.Messaging import Wire.API.Routes.Public.Util (UpdateResult (..)) import Wire.API.ServantProto (RawProto (..)) import Wire.API.User.Client +import Wire.HashPassword as HashPassword import Wire.NotificationSubsystem acceptConv :: @@ -497,7 +497,7 @@ addCodeUnqualifiedWithReqBody :: Member NotificationSubsystem r, Member (Input (Local ())) r, Member (Input UTCTime) r, - Member (Embed IO) r, + Member HashPassword r, Member (Input Opts) r, Member TeamFeatureStore r ) => @@ -522,7 +522,7 @@ addCodeUnqualified :: Member (Input (Local ())) r, Member (Input UTCTime) r, Member (Input Opts) r, - Member (Embed IO) r, + Member HashPassword r, Member TeamFeatureStore r ) => Maybe CreateConversationCodeRequest -> @@ -545,11 +545,11 @@ addCode :: Member (ErrorS 'GuestLinksDisabled) r, Member (ErrorS 'CreateConversationCodeConflict) r, Member ExternalAccess r, + Member HashPassword r, Member NotificationSubsystem r, Member (Input UTCTime) r, Member (Input Opts) r, - Member TeamFeatureStore r, - Member (Embed IO) r + Member TeamFeatureStore r ) => Local UserId -> Maybe ZHostValue -> @@ -569,7 +569,7 @@ addCode lusr mbZHost mZcon lcnv mReq = do Nothing -> do ttl <- realToFrac . unGuestLinkTTLSeconds . fromMaybe defGuestLinkTTLSeconds . view (settings . guestLinkTTLSeconds) <$> input code <- E.generateCode (tUnqualified lcnv) ReusableCode (Timeout ttl) - mPw <- for (mReq >>= (.password)) mkSafePasswordScrypt + mPw <- for (mReq >>= (.password)) HashPassword.hashPassword8 E.createCode code mPw now <- input let event = Event (tUntagged lcnv) Nothing (tUntagged lusr) now (EdConvCodeUpdate (mkConversationCodeInfo (isJust mPw) (codeKey code) (codeValue code) convUri)) @@ -704,8 +704,7 @@ updateConversationProtocolWithLocalUser :: Member Random r, Member ProposalStore r, Member SubConversationStore r, - Member TeamFeatureStore r, - Member TeamStore r + Member TeamFeatureStore r ) => Local UserId -> ConnId -> diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index b87dcf5e051..617c7c231cc 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -19,10 +19,9 @@ module Galley.API.Util where -import Control.Lens (set, view, (.~), (^.)) +import Control.Lens (set, to, view, (.~), (^.)) import Control.Monad.Extra (allM, anyM) import Data.Bifunctor -import Data.ByteString.Conversion import Data.Code qualified as Code import Data.Domain (Domain) import Data.Id as Id @@ -38,7 +37,6 @@ import Data.Set qualified as Set import Data.Singletons import Data.Text qualified as T import Data.Time -import GHC.TypeLits import Galley.API.Error import Galley.API.Mapping import Galley.Data.Conversation qualified as Data @@ -61,17 +59,12 @@ import Galley.Types.Conversations.Members import Galley.Types.Conversations.Roles import Galley.Types.Teams import Galley.Types.UserList -import Gundeck.Types.Push.V2 qualified as PushV2 import Imports hiding (forkIO) import Network.AMQP qualified as Q -import Network.HTTP.Types -import Network.Wai -import Network.Wai.Utilities qualified as Wai import Polysemy import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog qualified as P -import System.Logger qualified as Log import Wire.API.Connection import Wire.API.Conversation hiding (Member, cnvAccess, cnvAccessRoles, cnvName, cnvType) import Wire.API.Conversation qualified as Public @@ -85,8 +78,10 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import Wire.API.Password +import Wire.API.Push.V2 qualified as PushV2 import Wire.API.Routes.Public.Galley.Conversation import Wire.API.Routes.Public.Util +import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.API.Team.Member qualified as Mem import Wire.API.Team.Role @@ -520,9 +515,6 @@ localBotsAndUsers = foldMap botOrUser Just _ -> (toList (newBotMember m), []) Nothing -> ([], [m]) -location :: (ToByteString a) => a -> Response -> Response -location = Wai.addHeader hLocation . toByteString' - nonTeamMembers :: [LocalMember] -> [TeamMember] -> [LocalMember] nonTeamMembers cm tm = filter (not . isMemberOfTeam . lmId) cm where @@ -970,7 +962,7 @@ anyLegalholdActivated :: Sem r Bool anyLegalholdActivated uids = do opts <- input - case view (settings . featureFlags . flagLegalHold) opts of + case view (settings . featureFlags . to npProject) opts of FeatureLegalHoldDisabledPermanently -> pure False FeatureLegalHoldDisabledByDefault -> check FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> check @@ -989,7 +981,7 @@ allLegalholdConsentGiven :: Sem r Bool allLegalholdConsentGiven uids = do opts <- input - case view (settings . featureFlags . flagLegalHold) opts of + case view (settings . featureFlags . to npProject) opts of FeatureLegalHoldDisabledPermanently -> pure False FeatureLegalHoldDisabledByDefault -> do flip allM (chunksOf 32 uids) $ \uidsPage -> do @@ -1089,13 +1081,3 @@ instance if err' == demote @e then throwS @e else rethrowErrors @effs @r err' - -logRemoteNotificationError :: - forall rpc r. - (Member P.TinyLog r, KnownSymbol rpc) => - FederationError -> - Sem r () -logRemoteNotificationError e = - P.warn $ - Log.field "federation call" (symbolVal (Proxy @rpc)) - . Log.msg (displayException e) diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 0eab89da5c7..6d29d6081ca 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -81,8 +81,7 @@ import Galley.Options hiding (brig, endpoint, federator) import Galley.Options qualified as O import Galley.Queue import Galley.Queue qualified as Q -import Galley.Types.Teams (FeatureLegalHold) -import Galley.Types.Teams qualified as Teams +import Galley.Types.Teams import HTTP2.Client.Manager (Http2Manager, http2ManagerWithSSLCtx) import Imports hiding (forkIO) import Network.AMQP.Extended (mkRabbitMqChannelMVar) @@ -93,6 +92,7 @@ import OpenSSL.Session as Ssl import Polysemy import Polysemy.Async import Polysemy.Error +import Polysemy.Fail import Polysemy.Input import Polysemy.Internal (Append) import Polysemy.Resource @@ -108,6 +108,7 @@ import Wire.API.Error import Wire.API.Federation.Error import Wire.API.Team.Feature import Wire.GundeckAPIAccess (runGundeckAPIAccess) +import Wire.HashPassword import Wire.NotificationSubsystem.Interpreter (runNotificationSubsystemGundeck) import Wire.Rpc import Wire.Sem.Delay @@ -118,6 +119,7 @@ import Wire.Sem.Random.IO type GalleyEffects0 = '[ Input ClientState, Input Env, + HashPassword, Error InvalidInput, Error InternalError, -- federation errors can be thrown by almost every endpoint, so we avoid @@ -125,6 +127,7 @@ type GalleyEffects0 = Error FederationError, Async, Delay, + Fail, Embed IO, Error JSONResponse, Resource, @@ -146,9 +149,9 @@ validateOptions o = do (Nothing, Just _) -> error "RabbitMQ config is specified and federator is not, please specify both or none" (Just _, Nothing) -> error "Federator is specified and RabbitMQ config is not, please specify both or none" _ -> pure () - let mlsFlag = settings' ^. featureFlags . Teams.flagMLS . Teams.unDefaults - mlsConfig = wsConfig mlsFlag - migrationStatus = wsStatus $ settings' ^. featureFlags . Teams.flagMlsMigration . Teams.unDefaults + let mlsFlag = settings' ^. featureFlags . to (featureDefaults @MLSConfig) + mlsConfig = mlsFlag.config + migrationStatus = (.status) $ settings' ^. featureFlags . to (featureDefaults @MlsMigrationConfig) when (migrationStatus == FeatureStatusEnabled && ProtocolMLSTag `notElem` mlsSupportedProtocols mlsConfig) $ error "For starting MLS migration, MLS must be included in the supportedProtocol list" unless (mlsDefaultProtocol mlsConfig `elem` mlsSupportedProtocols mlsConfig) $ @@ -166,7 +169,7 @@ createEnv o l = do mgr <- initHttpManager o h2mgr <- initHttp2Manager codeURIcfg <- validateOptions o - Env (RequestId "N/A") o l mgr h2mgr (o ^. O.federator) (o ^. O.brig) cass + Env (RequestId defRequestId) o l mgr h2mgr (o ^. O.federator) (o ^. O.brig) cass <$> Q.new 16000 <*> initExtEnv <*> maybe (pure Nothing) (fmap Just . Aws.mkEnv l mgr) (o ^. journal) @@ -244,11 +247,13 @@ evalGalley e = . resourceToIOFinal . runError . embedToFinal @IO + . failToEmbed @IO . runDelay . asyncToIOFinal . mapError toResponse . mapError toResponse . mapError toResponse + . runHashPassword e._options._settings._passwordHashingOptions . runInputConst e . runInputConst (e ^. cstate) . mapError toResponse -- DynError @@ -258,7 +263,7 @@ evalGalley e = . runInputConst (e ^. options) . runInputConst (toLocalUnsafe (e ^. options . settings . federationDomain) ()) . interpretTeamFeatureSpecialContext e - . runInputSem getAllFeatureConfigsForServer + . runInputSem getAllTeamFeaturesForServer . interpretInternalTeamListToCassandra . interpretTeamListToCassandra . interpretLegacyConversationListToCassandra @@ -291,11 +296,11 @@ evalGalley e = . interpretSparAccess . interpretBrigAccess where - lh = view (options . settings . featureFlags . Teams.flagLegalHold) e + lh = view (options . settings . featureFlags . to npProject) e -interpretTeamFeatureSpecialContext :: Env -> Sem (Input (Maybe [TeamId], FeatureLegalHold) ': r) a -> Sem r a +interpretTeamFeatureSpecialContext :: Env -> Sem (Input (Maybe [TeamId], FeatureDefaults LegalholdConfig) ': r) a -> Sem r a interpretTeamFeatureSpecialContext e = runInputConst ( e ^. options . settings . exposeInvitationURLsTeamAllowlist, - e ^. options . settings . featureFlags . Teams.flagLegalHold + e ^. options . settings . featureFlags . to npProject ) diff --git a/services/galley/src/Galley/Cassandra/Access.hs b/services/galley/src/Galley/Cassandra/Access.hs index 05c566bfd11..3d196cd54c2 100644 --- a/services/galley/src/Galley/Cassandra/Access.hs +++ b/services/galley/src/Galley/Cassandra/Access.hs @@ -32,6 +32,3 @@ defAccess ConnectConv (Just (Set [])) = [PrivateAccess] defAccess One2OneConv (Just (Set [])) = [PrivateAccess] defAccess RegularConv (Just (Set [])) = defRegularConvAccess defAccess _ (Just (Set (x : xs))) = x : xs - -privateOnly :: Set Access -privateOnly = Set [PrivateAccess] diff --git a/services/galley/src/Galley/Cassandra/FeatureTH.hs b/services/galley/src/Galley/Cassandra/FeatureTH.hs new file mode 100644 index 00000000000..cf52cdc6caf --- /dev/null +++ b/services/galley/src/Galley/Cassandra/FeatureTH.hs @@ -0,0 +1,53 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskellQuotes #-} + +module Galley.Cassandra.FeatureTH where + +import Data.Kind +import Generics.SOP.TH +import Imports +import Language.Haskell.TH hiding (Type) +import Wire.API.Team.Feature + +featureCases :: ExpQ -> Q Exp +featureCases rhsQ = do + rhs <- rhsQ + TyConI (DataD _ _ _ _ constructors _) <- reify ''FeatureSingleton + pure $ + LamCaseE + [ Match (ConP c [] []) (NormalB rhs) [] + | GadtC [c] _ _ <- constructors + ] + +generateTupleP :: Q [Dec] +generateTupleP = do + let maxSize = 64 :: Int + tylist <- [t|[Type]|] + let vars = [VarT (mkName ("a" <> show i)) | i <- [0 .. maxSize - 1]] + pure + [ ClosedTypeFamilyD + (TypeFamilyHead (mkName "TupleP") [KindedTV (mkName "xs") () tylist] NoSig Nothing) + [ TySynEqn + Nothing + ( ConT (mkName "TupleP") + `AppT` mkPattern (take n vars) + ) + (mkTuple (take n vars)) + | n <- [0 .. maxSize] + ] + ] + where + mkPattern = foldr (\x y -> PromotedConsT `AppT` x `AppT` y) PromotedNilT + + mkTuple [] = ConT ''() + mkTuple [v] = ConT ''Identity `AppT` v + mkTuple vs = + let n = length vs + in foldl' AppT (TupleT n) vs + +-- | generates some of the remaining @SOP.Generic@ instances as orphans +-- it is cut off at 50 on purpose to reduce compilation times +-- you may increase up to 64 which is the number at which you +-- you should probably start fixing cql instead. +generateSOPInstances :: Q [Dec] +generateSOPInstances = concat <$> traverse (deriveGeneric . tupleTypeName) [31 .. 50] diff --git a/services/galley/src/Galley/Cassandra/GetAllTeamFeatureConfigs.hs b/services/galley/src/Galley/Cassandra/GetAllTeamFeatureConfigs.hs deleted file mode 100644 index 282e9d916c2..00000000000 --- a/services/galley/src/Galley/Cassandra/GetAllTeamFeatureConfigs.hs +++ /dev/null @@ -1,386 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} - -module Galley.Cassandra.GetAllTeamFeatureConfigs where - -import Cassandra -import Cassandra qualified as C -import Data.Id -import Data.Misc (HttpsUrl) -import Data.Time -import Database.CQL.Protocol -import Galley.Cassandra.Instances () -import Galley.Types.Teams (FeatureLegalHold (..)) -import Imports -import Wire.API.Conversation.Protocol (ProtocolTag) -import Wire.API.MLS.CipherSuite -import Wire.API.Team.Feature - -data AllTeamFeatureConfigsRow = AllTeamFeatureConfigsRow - { -- legalhold - legalhold :: Maybe FeatureStatus, - -- sso - sso :: Maybe FeatureStatus, - -- search visibility - searchVisibility :: Maybe FeatureStatus, - -- validate saml emails - validateSamlEmails :: Maybe FeatureStatus, - -- digital signatures - digitalSignatures :: Maybe FeatureStatus, - -- app lock - appLock :: Maybe FeatureStatus, - appLockEnforce :: Maybe EnforceAppLock, - appLockInactivityTimeoutSecs :: Maybe Int32, - -- file sharing - fileSharing :: Maybe FeatureStatus, - fileSharingLock :: Maybe LockStatus, - -- self deleting messages - selfDeletingMessages :: Maybe FeatureStatus, - selfDeletingMessagesTtl :: Maybe Int32, - selfDeletingMessagesLock :: Maybe LockStatus, - -- conference calling - conferenceCalling :: Maybe FeatureStatus, - conferenceCallingTtl :: Maybe FeatureTTL, - -- guest links - guestLinks :: Maybe FeatureStatus, - guestLinksLock :: Maybe LockStatus, - -- snd factor - sndFactor :: Maybe FeatureStatus, - sndFactorLock :: Maybe LockStatus, - -- mls - mls :: Maybe FeatureStatus, - mlsDefaultProtocol :: Maybe ProtocolTag, - mlsToggleUsers :: Maybe (C.Set UserId), - mlsAllowedCipherSuites :: Maybe (C.Set CipherSuiteTag), - mlsDefaultCipherSuite :: Maybe CipherSuiteTag, - mlsSupportedProtocols :: Maybe (C.Set ProtocolTag), - mlsLock :: Maybe LockStatus, - -- mls e2eid - mlsE2eid :: Maybe FeatureStatus, - mlsE2eidGracePeriod :: Maybe Int32, - mlsE2eidAcmeDiscoverUrl :: Maybe HttpsUrl, - mlsE2eidMaybeCrlProxy :: Maybe HttpsUrl, - mlsE2eidMaybeUseProxyOnMobile :: Maybe Bool, - mlsE2eidLock :: Maybe LockStatus, - -- mls migration - mlsMigration :: Maybe FeatureStatus, - mlsMigrationStartTime :: Maybe UTCTime, - mlsMigrationFinalizeRegardlessAfter :: Maybe UTCTime, - mlsMigrationLock :: Maybe LockStatus, - -- expose invitation urls - exposeInvitationUrls :: Maybe FeatureStatus, - -- outlook calendar integration - outlookCalIntegration :: Maybe FeatureStatus, - outlookCalIntegrationLock :: Maybe LockStatus, - -- enforce download location - enforceDownloadLocation :: Maybe FeatureStatus, - enforceDownloadLocation_Location :: Maybe Text, - enforceDownloadLocationLock :: Maybe LockStatus, - -- limit event fanout - limitEventFanout :: Maybe FeatureStatus - } - deriving (Generic, Show) - -recordInstance ''AllTeamFeatureConfigsRow - -emptyRow :: AllTeamFeatureConfigsRow -emptyRow = - AllTeamFeatureConfigsRow - { legalhold = Nothing, - sso = Nothing, - searchVisibility = Nothing, - validateSamlEmails = Nothing, - digitalSignatures = Nothing, - appLock = Nothing, - appLockEnforce = Nothing, - appLockInactivityTimeoutSecs = Nothing, - fileSharing = Nothing, - fileSharingLock = Nothing, - selfDeletingMessages = Nothing, - selfDeletingMessagesTtl = Nothing, - selfDeletingMessagesLock = Nothing, - conferenceCalling = Nothing, - conferenceCallingTtl = Nothing, - guestLinks = Nothing, - guestLinksLock = Nothing, - sndFactor = Nothing, - sndFactorLock = Nothing, - mls = Nothing, - mlsDefaultProtocol = Nothing, - mlsToggleUsers = Nothing, - mlsAllowedCipherSuites = Nothing, - mlsDefaultCipherSuite = Nothing, - mlsSupportedProtocols = Nothing, - mlsLock = Nothing, - mlsE2eid = Nothing, - mlsE2eidGracePeriod = Nothing, - mlsE2eidAcmeDiscoverUrl = Nothing, - mlsE2eidMaybeCrlProxy = Nothing, - mlsE2eidMaybeUseProxyOnMobile = Nothing, - mlsE2eidLock = Nothing, - mlsMigration = Nothing, - mlsMigrationStartTime = Nothing, - mlsMigrationFinalizeRegardlessAfter = Nothing, - mlsMigrationLock = Nothing, - exposeInvitationUrls = Nothing, - outlookCalIntegration = Nothing, - outlookCalIntegrationLock = Nothing, - enforceDownloadLocation = Nothing, - enforceDownloadLocation_Location = Nothing, - enforceDownloadLocationLock = Nothing, - limitEventFanout = Nothing - } - -allFeatureConfigsFromRow :: - -- id of team of which we want to see the feature - TeamId -> - -- team id list is from "settings.exposeInvitationURLsTeamAllowlist" - Maybe [TeamId] -> - FeatureLegalHold -> - Bool -> - AllFeatureConfigs -> - AllTeamFeatureConfigsRow -> - AllFeatureConfigs -allFeatureConfigsFromRow ourteam allowListForExposeInvitationURLs featureLH hasTeamImplicitLegalhold serverConfigs row = - AllFeatureConfigs - { afcLegalholdStatus = legalholdComputeFeatureStatus row.legalhold, - afcSSOStatus = - computeConfig - row.sso - Nothing - FeatureTTLUnlimited - (Just SSOConfig) - serverConfigs.afcSSOStatus, - afcTeamSearchVisibilityAvailable = - computeConfig - row.searchVisibility - Nothing - FeatureTTLUnlimited - (Just SearchVisibilityAvailableConfig) - serverConfigs.afcTeamSearchVisibilityAvailable, - afcSearchVisibilityInboundConfig = - computeConfig - row.searchVisibility - Nothing - FeatureTTLUnlimited - (Just SearchVisibilityInboundConfig) - serverConfigs.afcSearchVisibilityInboundConfig, - afcValidateSAMLEmails = - computeConfig - row.validateSamlEmails - Nothing - FeatureTTLUnlimited - (Just ValidateSAMLEmailsConfig) - serverConfigs.afcValidateSAMLEmails, - afcDigitalSignatures = - computeConfig - row.digitalSignatures - Nothing - FeatureTTLUnlimited - (Just DigitalSignaturesConfig) - serverConfigs.afcDigitalSignatures, - afcAppLock = - computeConfig - row.appLock - Nothing - FeatureTTLUnlimited - appLockConfig - serverConfigs.afcAppLock, - afcFileSharing = - computeConfig - row.fileSharing - row.fileSharingLock - FeatureTTLUnlimited - (Just FileSharingConfig) - serverConfigs.afcFileSharing, - afcClassifiedDomains = - computeConfig Nothing Nothing FeatureTTLUnlimited Nothing serverConfigs.afcClassifiedDomains, - afcConferenceCalling = - computeConfig - row.conferenceCalling - Nothing - (fromMaybe FeatureTTLUnlimited row.conferenceCallingTtl) - (Just ConferenceCallingConfig) - serverConfigs.afcConferenceCalling, - afcSelfDeletingMessages = - computeConfig - row.selfDeletingMessages - row.selfDeletingMessagesLock - FeatureTTLUnlimited - selfDeletingMessagesConfig - serverConfigs.afcSelfDeletingMessages, - afcGuestLink = - computeConfig - row.guestLinks - row.guestLinksLock - FeatureTTLUnlimited - (Just GuestLinksConfig) - serverConfigs.afcGuestLink, - afcSndFactorPasswordChallenge = - computeConfig - row.sndFactor - row.sndFactorLock - FeatureTTLUnlimited - (Just SndFactorPasswordChallengeConfig) - serverConfigs.afcSndFactorPasswordChallenge, - afcMLS = - computeConfig - row.mls - row.mlsLock - FeatureTTLUnlimited - mlsConfig - serverConfigs.afcMLS, - afcExposeInvitationURLsToTeamAdmin = exposeInvitationURLsComputeFeatureStatus row.exposeInvitationUrls, - afcOutlookCalIntegration = - computeConfig - row.outlookCalIntegration - row.outlookCalIntegrationLock - FeatureTTLUnlimited - (Just OutlookCalIntegrationConfig) - serverConfigs.afcOutlookCalIntegration, - afcMlsE2EId = - computeConfig - row.mlsE2eid - row.mlsE2eidLock - FeatureTTLUnlimited - mlsE2eidConfig - serverConfigs.afcMlsE2EId, - afcMlsMigration = - computeConfig - row.mlsMigration - row.mlsMigrationLock - FeatureTTLUnlimited - mlsMigrationConfig - serverConfigs.afcMlsMigration, - afcEnforceFileDownloadLocation = - computeConfig - row.enforceDownloadLocation - row.enforceDownloadLocationLock - FeatureTTLUnlimited - downloadLocationConfig - serverConfigs.afcEnforceFileDownloadLocation, - afcLimitedEventFanout = - computeConfig - row.limitEventFanout - Nothing - FeatureTTLUnlimited - (Just LimitedEventFanoutConfig) - serverConfigs.afcLimitedEventFanout - } - where - computeConfig :: Maybe FeatureStatus -> Maybe LockStatus -> FeatureTTL -> Maybe cfg -> WithStatus cfg -> WithStatus cfg - computeConfig mDbStatus mDbLock dbTtl mDbCfg serverCfg = - let withStatusNoLock = case (mDbStatus, mDbCfg) of - (Just dbStatus, Just dbCfg) -> - Just $ - WithStatusNoLock - { wssTTL = dbTtl, - wssStatus = dbStatus, - wssConfig = dbCfg - } - _ -> Nothing - in computeFeatureConfigForTeamUser withStatusNoLock mDbLock serverCfg - - -- FUTUREWORK: the following lines are duplicated in - -- "Galley.Cassandra.TeamFeatures"; make sure the pairs don't diverge! - appLockConfig = AppLockConfig <$> row.appLockEnforce <*> row.appLockInactivityTimeoutSecs - - selfDeletingMessagesConfig = SelfDeletingMessagesConfig <$> row.selfDeletingMessagesTtl - - mlsConfig = - MLSConfig - <$> maybe (Just []) (Just . C.fromSet) row.mlsToggleUsers - <*> row.mlsDefaultProtocol - <*> maybe (Just []) (Just . C.fromSet) row.mlsAllowedCipherSuites - <*> row.mlsDefaultCipherSuite - <*> maybe (Just []) (Just . C.fromSet) row.mlsSupportedProtocols - - mlsE2eidConfig = - Just $ - MlsE2EIdConfig - (toGracePeriodOrDefault row.mlsE2eidGracePeriod) - row.mlsE2eidAcmeDiscoverUrl - row.mlsE2eidMaybeCrlProxy - (fromMaybe (useProxyOnMobile . wsConfig $ defFeatureStatus) row.mlsE2eidMaybeUseProxyOnMobile) - where - toGracePeriodOrDefault :: Maybe Int32 -> NominalDiffTime - toGracePeriodOrDefault = maybe (verificationExpiration $ wsConfig defFeatureStatus) fromIntegral - - mlsMigrationConfig = - Just $ - MlsMigrationConfig - { startTime = row.mlsMigrationStartTime, - finaliseRegardlessAfter = row.mlsMigrationFinalizeRegardlessAfter - } - - downloadLocationConfig = Just $ EnforceFileDownloadLocationConfig row.enforceDownloadLocation_Location - - -- FUTUREWORK: this duplicates logic hidden elsewhere for the other getters and setters. do not change lightly! - exposeInvitationURLsComputeFeatureStatus :: - Maybe FeatureStatus -> - WithStatus ExposeInvitationURLsToTeamAdminConfig - exposeInvitationURLsComputeFeatureStatus mFeatureStatus = - if ourteam `elem` fromMaybe [] allowListForExposeInvitationURLs - then - serverConfigs.afcExposeInvitationURLsToTeamAdmin - & maybe id setStatus mFeatureStatus - & setLockStatus LockStatusUnlocked - else serverConfigs.afcExposeInvitationURLsToTeamAdmin - - -- FUTUREWORK: this duplicates logic hidden elsewhere for the other getters and setters. do not change lightly! - legalholdComputeFeatureStatus :: Maybe FeatureStatus -> WithStatus LegalholdConfig - legalholdComputeFeatureStatus mStatusValue = setStatus status defFeatureStatus - where - status = - if isLegalHoldEnabledForTeam - then FeatureStatusEnabled - else FeatureStatusDisabled - isLegalHoldEnabledForTeam = - case featureLH of - FeatureLegalHoldDisabledPermanently -> False - FeatureLegalHoldDisabledByDefault -> maybe False ((==) FeatureStatusEnabled) mStatusValue - FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> hasTeamImplicitLegalhold - -getAllFeatureConfigs :: (MonadClient m) => Maybe [TeamId] -> FeatureLegalHold -> Bool -> AllFeatureConfigs -> TeamId -> m AllFeatureConfigs -getAllFeatureConfigs allowListForExposeInvitationURLs featureLH hasTeamImplicitLegalhold serverConfigs tid = do - mRow <- retry x1 $ query1 select (params LocalQuorum (Identity tid)) - pure - $ allFeatureConfigsFromRow - tid - allowListForExposeInvitationURLs - featureLH - hasTeamImplicitLegalhold - serverConfigs - $ maybe emptyRow asRecord mRow - where - select :: - PrepQuery - R - (Identity TeamId) - (TupleType AllTeamFeatureConfigsRow) - select = - "select \ - \legalhold_status, \ - \sso_status, \ - \search_visibility_status, \ - \validate_saml_emails, \ - \digital_signatures, \ - \app_lock_status, app_lock_enforce, app_lock_inactivity_timeout_secs, \ - \file_sharing, file_sharing_lock_status, \ - \self_deleting_messages_status, self_deleting_messages_ttl, self_deleting_messages_lock_status, \ - \conference_calling, ttl(conference_calling), \ - \guest_links_status, guest_links_lock_status, \ - \snd_factor_password_challenge_status, snd_factor_password_challenge_lock_status, \ - \\ - \mls_status, mls_default_protocol, mls_protocol_toggle_users, mls_allowed_ciphersuites, \ - \mls_default_ciphersuite, mls_supported_protocols, mls_lock_status, \ - \\ - \mls_e2eid_status, mls_e2eid_grace_period, mls_e2eid_acme_discovery_url, mls_e2eid_crl_proxy, mls_e2eid_use_proxy_on_mobile, mls_e2eid_lock_status, \ - \\ - \mls_migration_status, mls_migration_start_time, mls_migration_finalise_regardless_after, \ - \mls_migration_lock_status, \ - \\ - \expose_invitation_urls_to_team_admin, \ - \outlook_cal_integration_status, outlook_cal_integration_lock_status, \ - \enforce_file_download_location_status, enforce_file_download_location, enforce_file_download_location_lock_status, \ - \limited_event_fanout_status \ - \from team_features where team_id = ?" diff --git a/services/galley/src/Galley/Cassandra/GetAllTeamFeatures.hs b/services/galley/src/Galley/Cassandra/GetAllTeamFeatures.hs new file mode 100644 index 00000000000..3019ce00275 --- /dev/null +++ b/services/galley/src/Galley/Cassandra/GetAllTeamFeatures.hs @@ -0,0 +1,86 @@ +{-# OPTIONS_GHC -fconstraint-solver-iterations=0 #-} + +module Galley.Cassandra.GetAllTeamFeatures (getAllDbFeatures) where + +import Cassandra +import Data.Id +import Galley.Cassandra.Instances () +import Galley.Cassandra.MakeFeature +import Galley.Cassandra.Orphans () +import Generics.SOP +import Imports hiding (Map) +import Polysemy.Internal +import Wire.API.Team.Feature + +type family ConcatFeatureRow xs where + ConcatFeatureRow '[] = '[] + ConcatFeatureRow (x : xs) = Append (FeatureRow x) (ConcatFeatureRow xs) + +type AllFeatureRow = ConcatFeatureRow Features + +emptyRow :: NP Maybe AllFeatureRow +emptyRow = hpure Nothing + +class ConcatFeatures cfgs where + rowToAllFeatures :: NP Maybe (ConcatFeatureRow cfgs) -> NP DbFeature cfgs + +instance ConcatFeatures '[] where + rowToAllFeatures Nil = Nil + +instance + ( SplitNP (FeatureRow cfg) (ConcatFeatureRow cfgs), + ConcatFeatures cfgs, + MakeFeature cfg + ) => + ConcatFeatures (cfg : cfgs) + where + rowToAllFeatures row = case splitNP @(FeatureRow cfg) @(ConcatFeatureRow cfgs) row of + (row0, row1) -> rowToFeature row0 :* rowToAllFeatures row1 + +class SplitNP xs ys where + splitNP :: NP f (Append xs ys) -> (NP f xs, NP f ys) + +instance SplitNP '[] ys where + splitNP ys = (Nil, ys) + +instance (SplitNP xs ys) => SplitNP (x ': xs) ys where + splitNP (z :* zs) = case splitNP zs of + (xs, ys) -> (z :* xs, ys) + +class AppendNP xs ys where + appendNP :: NP f xs -> NP f ys -> NP f (Append xs ys) + +instance AppendNP '[] ys where + appendNP Nil ys = ys + +instance (AppendNP xs ys) => AppendNP (x : xs) ys where + appendNP (x :* xs) ys = x :* appendNP xs ys + +class ConcatColumns cfgs where + concatColumns :: NP (K String) (ConcatFeatureRow cfgs) + +instance ConcatColumns '[] where + concatColumns = Nil + +instance + ( AppendNP (FeatureRow cfg) (ConcatFeatureRow cfgs), + MakeFeature cfg, + ConcatColumns cfgs + ) => + ConcatColumns (cfg : cfgs) + where + concatColumns = featureColumns @cfg `appendNP` concatColumns @cfgs + +getAllDbFeatures :: + forall row mrow m. + ( MonadClient m, + row ~ AllFeatureRow, + Tuple (TupleP mrow), + IsProductType (TupleP mrow) mrow, + AllZip (IsF Maybe) row mrow + ) => + TeamId -> + m (AllFeatures DbFeature) +getAllDbFeatures tid = do + mRow <- fetchFeatureRow @row @mrow tid (concatColumns @Features) + pure . rowToAllFeatures $ fromMaybe emptyRow mRow diff --git a/services/galley/src/Galley/Cassandra/Instances.hs b/services/galley/src/Galley/Cassandra/Instances.hs index 57ae885b673..1c9307400c6 100644 --- a/services/galley/src/Galley/Cassandra/Instances.hs +++ b/services/galley/src/Galley/Cassandra/Instances.hs @@ -27,7 +27,6 @@ import Cassandra.CQL import Control.Error (note) import Data.ByteString.Conversion import Data.ByteString.Lazy qualified as LBS -import Data.Either.Combinators hiding (fromRight) import Data.Text qualified as T import Data.Text.Encoding qualified as T import Imports diff --git a/services/galley/src/Galley/Cassandra/LegalHold.hs b/services/galley/src/Galley/Cassandra/LegalHold.hs index 490e46dcaa9..d9270b29a38 100644 --- a/services/galley/src/Galley/Cassandra/LegalHold.hs +++ b/services/galley/src/Galley/Cassandra/LegalHold.hs @@ -54,6 +54,7 @@ import Polysemy.Input import Polysemy.TinyLog import Ssl.Util qualified as SSL import Wire.API.Provider.Service +import Wire.API.Team.Feature import Wire.API.User.Client.Prekey interpretLegalHoldStoreToCassandra :: @@ -62,7 +63,7 @@ interpretLegalHoldStoreToCassandra :: Member (Input Env) r, Member TinyLog r ) => - FeatureLegalHold -> + FeatureDefaults LegalholdConfig -> Sem (LegalHoldStore ': r) a -> Sem r a interpretLegalHoldStoreToCassandra lh = interpret $ \case @@ -159,7 +160,7 @@ unsetTeamLegalholdWhitelisted :: (MonadClient m) => TeamId -> m () unsetTeamLegalholdWhitelisted tid = retry x5 (write Q.removeLegalHoldWhitelistedTeam (params LocalQuorum (Identity tid))) -isTeamLegalholdWhitelisted :: FeatureLegalHold -> TeamId -> Client Bool +isTeamLegalholdWhitelisted :: FeatureDefaults LegalholdConfig -> TeamId -> Client Bool isTeamLegalholdWhitelisted FeatureLegalHoldDisabledPermanently _ = pure False isTeamLegalholdWhitelisted FeatureLegalHoldDisabledByDefault _ = pure False isTeamLegalholdWhitelisted FeatureLegalHoldWhitelistTeamsAndImplicitConsent tid = diff --git a/services/galley/src/Galley/Cassandra/MakeFeature.hs b/services/galley/src/Galley/Cassandra/MakeFeature.hs new file mode 100644 index 00000000000..883369c490d --- /dev/null +++ b/services/galley/src/Galley/Cassandra/MakeFeature.hs @@ -0,0 +1,536 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE TemplateHaskell #-} + +-- | Abstraction to fetch and store feature values from and to the database. +module Galley.Cassandra.MakeFeature where + +import Cassandra +import Cassandra qualified as C +import Data.Functor +import Data.Functor.Identity +import Data.Id +import Data.Kind +import Data.List.Singletons (Length) +import Data.Misc (HttpsUrl) +import Data.Singletons (demote) +import Data.Time +import Data.Time.Clock.POSIX +import GHC.TypeNats +import Galley.Cassandra.FeatureTH +import Galley.Cassandra.Instances () +import Generics.SOP +import Imports hiding (Generic, Map) +import Wire.API.Conversation.Protocol (ProtocolTag) +import Wire.API.MLS.CipherSuite +import Wire.API.Team.Feature + +-- [Note: default values for configuration fields] +-- +-- When reading values for configuration types with multiple fields, we fall +-- back to default values for each field independently, instead of treating the +-- whole configuration as a single value that can be set or not. +-- +-- In most cases, either strategy would produce the same result, because there +-- is no way to set only *some* fields using the public API. However, that can +-- happen when a feature flag changes over time and gains new fields, as it has +-- been the case for mlsE2EId. +-- +-- Therefore, we use the first strategy consistently for all feature flags, +-- even when it does not matter. + +-- | This is necessary in order to convert an @NP f xs@ type to something that +-- CQL can understand. +-- +-- The generated code looks like: +-- @@ +-- instance TupleP xs where +-- TupleP '[] = () +-- TupleP '[a] = Identity a +-- TupleP '[a, b] = (a, b) +-- ... +-- @@ +$generateTupleP + +class MakeFeature cfg where + type FeatureRow cfg :: [Type] + type FeatureRow cfg = '[FeatureStatus] + + featureColumns :: NP (K String) (FeatureRow cfg) + + rowToFeature :: NP Maybe (FeatureRow cfg) -> DbFeature cfg + default rowToFeature :: + (FeatureRow cfg ~ '[FeatureStatus]) => + NP Maybe (FeatureRow cfg) -> + DbFeature cfg + rowToFeature = foldMap dbFeatureStatus . hd + + featureToRow :: LockableFeature cfg -> NP Maybe (FeatureRow cfg) + default featureToRow :: + (FeatureRow cfg ~ '[FeatureStatus]) => + LockableFeature cfg -> + NP Maybe (FeatureRow cfg) + featureToRow feat = Just feat.status :* Nil + +instance MakeFeature LegalholdConfig where + featureColumns = K "legalhold_status" :* Nil + +instance MakeFeature SSOConfig where + featureColumns = K "sso_status" :* Nil + +instance MakeFeature SearchVisibilityAvailableConfig where + featureColumns = K "search_visibility_status" :* Nil + +-- | This feature shares its status column with +-- 'SearchVisibilityAvailableConfig'. This means that when fetching all +-- features, this column is repeated in the query, i.e. the query looks like: +-- @@ +-- select ..., search_visibility_status, search_visibility_status, ... from team_features ... +-- @@ +instance MakeFeature SearchVisibilityInboundConfig where + featureColumns = K "search_visibility_status" :* Nil + +instance MakeFeature ValidateSAMLEmailsConfig where + featureColumns = K "validate_saml_emails" :* Nil + +instance MakeFeature DigitalSignaturesConfig where + featureColumns = K "digital_signatures" :* Nil + +instance MakeFeature AppLockConfig where + type FeatureRow AppLockConfig = '[FeatureStatus, EnforceAppLock, Int32] + featureColumns = + K "app_lock_status" + :* K "app_lock_enforce" + :* K "app_lock_inactivity_timeout_secs" + :* Nil + + rowToFeature (status :* enforce :* timeout :* Nil) = + foldMap dbFeatureStatus status + -- [Note: default values for configuration fields] + <> dbFeatureModConfig + ( \defCfg -> + AppLockConfig + (fromMaybe defCfg.applockEnforceAppLock enforce) + (fromMaybe defCfg.applockInactivityTimeoutSecs timeout) + ) + + featureToRow feat = + Just feat.status + :* Just feat.config.applockEnforceAppLock + :* Just feat.config.applockInactivityTimeoutSecs + :* Nil + +instance MakeFeature ClassifiedDomainsConfig where + type FeatureRow ClassifiedDomainsConfig = '[] + featureColumns = Nil + + rowToFeature Nil = mempty + featureToRow _ = Nil + +instance MakeFeature FileSharingConfig where + type FeatureRow FileSharingConfig = '[LockStatus, FeatureStatus] + featureColumns = K "file_sharing_lock_status" :* K "file_sharing" :* Nil + + rowToFeature (lockStatus :* status :* Nil) = + foldMap dbFeatureLockStatus lockStatus + <> foldMap dbFeatureStatus status + + featureToRow feat = Just feat.lockStatus :* Just feat.status :* Nil + +instance MakeFeature ConferenceCallingConfig where + type FeatureRow ConferenceCallingConfig = '[LockStatus, FeatureStatus, One2OneCalls] + featureColumns = + K "conference_calling" + :* K "conference_calling_status" + :* K "conference_calling_one_to_one" + :* Nil + + rowToFeature (lockStatus :* status :* calls :* Nil) = + foldMap dbFeatureLockStatus lockStatus + <> foldMap dbFeatureStatus status + <> foldMap (dbFeatureConfig . ConferenceCallingConfig) calls + + featureToRow feat = + Just feat.lockStatus + :* Just feat.status + :* Just feat.config.one2OneCalls + :* Nil + +instance MakeFeature SelfDeletingMessagesConfig where + type FeatureRow SelfDeletingMessagesConfig = '[LockStatus, FeatureStatus, Int32] + featureColumns = + K "self_deleting_messages_lock_status" + :* K "self_deleting_messages_status" + :* K "self_deleting_messages_ttl" + :* Nil + + rowToFeature (lockStatus :* status :* ttl :* Nil) = + foldMap dbFeatureLockStatus lockStatus + <> foldMap dbFeatureStatus status + <> foldMap (dbFeatureConfig . SelfDeletingMessagesConfig) ttl + + featureToRow feat = + Just feat.lockStatus + :* Just feat.status + :* Just feat.config.sdmEnforcedTimeoutSeconds + :* Nil + +instance MakeFeature GuestLinksConfig where + type FeatureRow GuestLinksConfig = '[LockStatus, FeatureStatus] + featureColumns = K "guest_links_lock_status" :* K "guest_links_status" :* Nil + + rowToFeature (lockStatus :* status :* Nil) = + foldMap dbFeatureLockStatus lockStatus + <> foldMap dbFeatureStatus status + + featureToRow feat = Just feat.lockStatus :* Just feat.status :* Nil + +instance MakeFeature SndFactorPasswordChallengeConfig where + type FeatureRow SndFactorPasswordChallengeConfig = '[LockStatus, FeatureStatus] + featureColumns = + K "snd_factor_password_challenge_lock_status" + :* K "snd_factor_password_challenge_status" + :* Nil + + rowToFeature (lockStatus :* status :* Nil) = + foldMap dbFeatureLockStatus lockStatus + <> foldMap dbFeatureStatus status + + featureToRow feat = Just feat.lockStatus :* Just feat.status :* Nil + +instance MakeFeature ExposeInvitationURLsToTeamAdminConfig where + featureColumns = K "expose_invitation_urls_to_team_admin" :* Nil + +instance MakeFeature OutlookCalIntegrationConfig where + type FeatureRow OutlookCalIntegrationConfig = '[LockStatus, FeatureStatus] + + featureColumns = + K "outlook_cal_integration_lock_status" + :* K "outlook_cal_integration_status" + :* Nil + + rowToFeature (lockStatus :* status :* Nil) = + foldMap dbFeatureLockStatus lockStatus + <> foldMap dbFeatureStatus status + + featureToRow feat = Just feat.lockStatus :* Just feat.status :* Nil + +instance MakeFeature MLSConfig where + type + FeatureRow MLSConfig = + '[ LockStatus, + FeatureStatus, + ProtocolTag, + (C.Set UserId), + (C.Set CipherSuiteTag), + CipherSuiteTag, + (C.Set ProtocolTag) + ] + featureColumns = + K "mls_lock_status" + :* K "mls_status" + :* K "mls_default_protocol" + :* K "mls_protocol_toggle_users" + :* K "mls_allowed_ciphersuites" + :* K "mls_default_ciphersuite" + :* K "mls_supported_protocols" + :* Nil + + rowToFeature + ( lockStatus + :* status + :* defProto + :* toggleUsers + :* ciphersuites + :* defCiphersuite + :* supportedProtos + :* Nil + ) = + foldMap dbFeatureLockStatus lockStatus + <> foldMap dbFeatureStatus status + <> dbFeatureModConfig + ( \defCfg -> + -- [Note: default values for configuration fields] + -- + -- This case is a bit special, because Cassandra sets do not + -- distinguish between 'null' and 'empty'. To differentiate + -- between these cases, we use the `mls_default_protocol` field: + -- if set, we interpret null sets as empty, otherwise we use the + -- default. + let configIsSet = isJust defProto + in MLSConfig + ( maybe + (if configIsSet then [] else defCfg.mlsProtocolToggleUsers) + C.fromSet + toggleUsers + ) + (fromMaybe defCfg.mlsDefaultProtocol defProto) + ( maybe + (if configIsSet then [] else defCfg.mlsAllowedCipherSuites) + C.fromSet + ciphersuites + ) + (fromMaybe defCfg.mlsDefaultCipherSuite defCiphersuite) + ( maybe + (if configIsSet then [] else defCfg.mlsSupportedProtocols) + C.fromSet + supportedProtos + ) + ) + + featureToRow feat = + Just feat.lockStatus + :* Just feat.status + :* Just feat.config.mlsDefaultProtocol + :* Just (C.Set feat.config.mlsProtocolToggleUsers) + :* Just (C.Set feat.config.mlsAllowedCipherSuites) + :* Just feat.config.mlsDefaultCipherSuite + :* Just (C.Set feat.config.mlsSupportedProtocols) + :* Nil + +instance MakeFeature MlsE2EIdConfig where + type + FeatureRow MlsE2EIdConfig = + '[ LockStatus, + FeatureStatus, + Int32, + HttpsUrl, + HttpsUrl, + Bool + ] + featureColumns = + K "mls_e2eid_lock_status" + :* K "mls_e2eid_status" + :* K "mls_e2eid_grace_period" + :* K "mls_e2eid_acme_discovery_url" + :* K "mls_e2eid_crl_proxy" + :* K "mls_e2eid_use_proxy_on_mobile" + :* Nil + + rowToFeature + ( lockStatus + :* status + :* gracePeriod + :* acmeDiscoveryUrl + :* crlProxy + :* useProxyOnMobile + :* Nil + ) = + foldMap dbFeatureLockStatus lockStatus + <> foldMap dbFeatureStatus status + <> dbFeatureModConfig + ( \defCfg -> + defCfg + { verificationExpiration = + maybe defCfg.verificationExpiration fromIntegral gracePeriod, + acmeDiscoveryUrl = acmeDiscoveryUrl <|> defCfg.acmeDiscoveryUrl, + crlProxy = crlProxy <|> defCfg.crlProxy, + useProxyOnMobile = fromMaybe defCfg.useProxyOnMobile useProxyOnMobile + } + ) + + featureToRow feat = + Just feat.lockStatus + :* Just feat.status + :* Just (truncate feat.config.verificationExpiration) + :* feat.config.acmeDiscoveryUrl + :* feat.config.crlProxy + :* Just feat.config.useProxyOnMobile + :* Nil + +-- Optional time stamp. A 'Nothing' value is represented as 0. +newtype OptionalUTCTime = OptionalUTCTime {unOptionalUTCTime :: Maybe UTCTime} + +instance Cql OptionalUTCTime where + ctype = Tagged (untag (ctype @UTCTime)) + + toCql = toCql . fromMaybe (posixSecondsToUTCTime 0) . unOptionalUTCTime + + fromCql x = do + t <- fromCql x + pure . OptionalUTCTime $ guard (utcTimeToPOSIXSeconds t /= 0) $> t + +instance MakeFeature MlsMigrationConfig where + type + FeatureRow MlsMigrationConfig = + '[LockStatus, FeatureStatus, OptionalUTCTime, OptionalUTCTime] + + featureColumns = + K "mls_migration_lock_status" + :* K "mls_migration_status" + :* K "mls_migration_start_time" + :* K "mls_migration_finalise_regardless_after" + :* Nil + + rowToFeature (lockStatus :* status :* startTime :* finalizeAfter :* Nil) = + foldMap dbFeatureLockStatus lockStatus + <> foldMap dbFeatureStatus status + <> dbFeatureModConfig + ( \defCfg -> + defCfg + { startTime = maybe defCfg.startTime unOptionalUTCTime startTime, + finaliseRegardlessAfter = + maybe + defCfg.finaliseRegardlessAfter + unOptionalUTCTime + finalizeAfter + } + ) + + featureToRow feat = + Just feat.lockStatus + :* Just feat.status + :* Just (OptionalUTCTime feat.config.startTime) + :* Just (OptionalUTCTime feat.config.finaliseRegardlessAfter) + :* Nil + +instance MakeFeature EnforceFileDownloadLocationConfig where + type FeatureRow EnforceFileDownloadLocationConfig = '[LockStatus, FeatureStatus, Text] + + featureColumns = + K "enforce_file_download_location_lock_status" + :* K "enforce_file_download_location_status" + :* K "enforce_file_download_location" + :* Nil + + rowToFeature (lockStatus :* status :* location :* Nil) = + foldMap dbFeatureLockStatus lockStatus + <> foldMap dbFeatureStatus status + <> foldMap + dbFeatureConfig + ( case location of + Nothing -> Nothing + -- convert empty string to 'Nothing' + Just "" -> Just (EnforceFileDownloadLocationConfig Nothing) + Just loc -> Just (EnforceFileDownloadLocationConfig (Just loc)) + ) + + featureToRow feat = + Just feat.lockStatus + :* Just feat.status + -- represent 'Nothing' as the empty string + :* Just (fromMaybe "" feat.config.enforcedDownloadLocation) + :* Nil + +instance MakeFeature LimitedEventFanoutConfig where + featureColumns = K "limited_event_fanout_status" :* Nil + +fetchFeature :: + forall cfg m row mrow. + ( MonadClient m, + row ~ FeatureRow cfg, + MakeFeature cfg, + IsProductType (TupleP mrow) mrow, + AllZip (IsF Maybe) row mrow, + Tuple (TupleP mrow) + ) => + TeamId -> + m (DbFeature cfg) +fetchFeature tid = do + case featureColumns @cfg of + Nil -> pure (rowToFeature Nil) + cols -> do + mRow <- fetchFeatureRow @row @mrow tid cols + pure $ foldMap rowToFeature mRow + +fetchFeatureRow :: + forall row mrow m. + ( MonadClient m, + IsProductType (TupleP mrow) mrow, + AllZip (IsF Maybe) row mrow, + Tuple (TupleP mrow) + ) => + TeamId -> + NP (K String) row -> + m (Maybe (NP Maybe row)) +fetchFeatureRow tid cols = do + let select :: PrepQuery R (Identity TeamId) (TupleP mrow) + select = + fromString $ + "select " + <> intercalate ", " (hcollapse cols) + <> " from team_features where team_id = ?" + row <- retry x1 $ query1 select (params LocalQuorum (Identity tid)) + pure $ fmap (unfactorI . productTypeFrom) row + +storeFeature :: + forall cfg m row mrow. + ( MonadClient m, + row ~ FeatureRow cfg, + MakeFeature cfg, + IsProductType (TupleP (TeamId : mrow)) (TeamId : mrow), + AllZip (IsF Maybe) row mrow, + Tuple (TupleP (TeamId : mrow)), + KnownNat (Length row) + ) => + TeamId -> + LockableFeature cfg -> + m () +storeFeature tid feat = do + if n == 0 + then pure () + else + retry x5 $ + write + insert + ( params LocalQuorum (productTypeTo (I tid :* factorI (featureToRow feat))) + ) + where + n :: Int + n = fromIntegral (demote @(Length row)) + + insert :: PrepQuery W (TupleP (TeamId ': mrow)) () + insert = + fromString $ + "insert into team_features (team_id, " + <> intercalate ", " (hcollapse (featureColumns @cfg)) + <> ") values (" + <> intercalate "," (replicate (succ n) "?") + <> ")" + +class (FeatureRow cfg ~ row) => StoreFeatureLockStatus (row :: [Type]) cfg where + storeFeatureLockStatus' :: (MonadClient m) => TeamId -> Tagged cfg LockStatus -> m () + +instance + {-# OVERLAPPING #-} + ( FeatureRow cfg ~ (LockStatus ': row), + MakeFeature cfg + ) => + StoreFeatureLockStatus (LockStatus ': row) cfg + where + storeFeatureLockStatus' tid lock = do + let col = unK (hd (featureColumns @cfg)) + insert :: PrepQuery W (TeamId, LockStatus) () + insert = + fromString $ + "insert into team_features (team_id, " <> col <> ") values (?, ?)" + retry x5 $ write insert (params LocalQuorum (tid, (untag lock))) + +instance (FeatureRow cfg ~ row) => StoreFeatureLockStatus row cfg where + storeFeatureLockStatus' _ _ = pure () + +storeFeatureLockStatus :: + forall cfg m. + (MonadClient m, StoreFeatureLockStatus (FeatureRow cfg) cfg) => + TeamId -> + Tagged cfg LockStatus -> + m () +storeFeatureLockStatus = storeFeatureLockStatus' @(FeatureRow cfg) + +-- | Convert @NP f [x1, ..., xn]@ to @NP I [f x1, ..., f xn]@. +-- +-- This works because @I . f = f@. +factorI :: forall f xs ys. (AllZip (IsF f) xs ys) => NP f xs -> NP I ys +factorI Nil = Nil +factorI (x :* xs) = I x :* factorI xs + +-- | Convert @NP I [f x1, ..., f xn]@ to @NP f [x1, ..., xn]@. +-- +-- See 'factorI'. +unfactorI :: forall f xs ys. (AllZip (IsF f) xs ys) => NP I ys -> NP f xs +unfactorI Nil = Nil +unfactorI (I x :* xs) = x :* unfactorI xs + +-- | This is to emulate a constraint-level lambda. +class (f x ~ y) => IsF f x y | y -> x + +instance (f x ~ y) => IsF f x y diff --git a/services/galley/src/Galley/Cassandra/Orphans.hs b/services/galley/src/Galley/Cassandra/Orphans.hs new file mode 100644 index 00000000000..d939cdafdb0 --- /dev/null +++ b/services/galley/src/Galley/Cassandra/Orphans.hs @@ -0,0 +1,8 @@ +{-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -Wno-orphans #-} + +module Galley.Cassandra.Orphans where + +import Galley.Cassandra.FeatureTH + +$generateSOPInstances diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index fa8c5c89042..309996486b6 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -234,9 +234,6 @@ selectConv :: ) selectConv = "select type, creator, access, access_role, access_roles_v2, name, team, deleted, message_timer, receipt_mode, protocol, group_id, epoch, WRITETIME(epoch), cipher_suite from conversation where conv = ?" -selectReceiptMode :: PrepQuery R (Identity ConvId) (Identity (Maybe ReceiptMode)) -selectReceiptMode = "select receipt_mode from conversation where conv = ?" - isConvDeleted :: PrepQuery R (Identity ConvId) (Identity (Maybe Bool)) isConvDeleted = "select deleted from conversation where conv = ?" @@ -364,9 +361,6 @@ insertCipherSuiteForSubConversation = "UPDATE subconversation set cipher_suite = listSubConversations :: PrepQuery R (Identity ConvId) (SubConvId, CipherSuiteTag, Epoch, Writetime Epoch, GroupId) listSubConversations = "SELECT subconv_id, cipher_suite, epoch, WRITETIME(epoch), group_id FROM subconversation WHERE conv_id = ?" -selectSubConversations :: PrepQuery R (Identity ConvId) (Identity SubConvId) -selectSubConversations = "SELECT subconv_id FROM subconversation WHERE conv_id = ?" - deleteSubConversation :: PrepQuery W (ConvId, SubConvId) () deleteSubConversation = "DELETE FROM subconversation where conv_id = ? and subconv_id = ?" @@ -460,9 +454,6 @@ updateRemoteOtrMemberArchived = {- `IF EXISTS`, but that requires benchmarking - updateRemoteMemberHidden :: PrepQuery W (Bool, Maybe Text, Domain, ConvId, UserId) () updateRemoteMemberHidden = {- `IF EXISTS`, but that requires benchmarking -} "update user_remote_conv set hidden = ?, hidden_ref = ? where conv_remote_domain = ? and conv_remote_id = ? and user = ?" -selectRemoteMemberStatus :: PrepQuery R (Domain, ConvId, UserId) (Maybe MutedStatus, Maybe Text, Maybe Bool, Maybe Text, Maybe Bool, Maybe Text) -selectRemoteMemberStatus = "select otr_muted_status, otr_muted_ref, otr_archived, otr_archived_ref, hidden, hidden_ref from user_remote_conv where conv_remote_domain = ? and conv_remote_id = ? and user = ?" - -- Clients ------------------------------------------------------------------ selectClients :: PrepQuery R (Identity [UserId]) (UserId, C.Set ClientId) diff --git a/services/galley/src/Galley/Cassandra/Team.hs b/services/galley/src/Galley/Cassandra/Team.hs index 84b5458d115..484c769ab0c 100644 --- a/services/galley/src/Galley/Cassandra/Team.hs +++ b/services/galley/src/Galley/Cassandra/Team.hs @@ -60,6 +60,7 @@ import UnliftIO qualified import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Team import Wire.API.Team.Conversation +import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.API.Team.Permission (Perm (SetBilling), Permissions, self) import Wire.Sem.Paging.Cassandra @@ -70,7 +71,7 @@ interpretTeamStoreToCassandra :: Member (Input ClientState) r, Member TinyLog r ) => - FeatureLegalHold -> + FeatureDefaults LegalholdConfig -> Sem (TeamStore ': r) a -> Sem r a interpretTeamStoreToCassandra lh = interpret $ \case @@ -154,7 +155,7 @@ interpretTeamStoreToCassandra lh = interpret $ \case embedApp (currentFanoutLimit <$> view options) GetLegalHoldFlag -> do logEffect "TeamStore.GetLegalHoldFlag" - view (options . settings . featureFlags . flagLegalHold) <$> input + view (options . settings . featureFlags . to npProject) <$> input EnqueueTeamEvent e -> do logEffect "TeamStore.EnqueueTeamEvent" menv <- inputs (view aEnv) @@ -197,7 +198,7 @@ interpretTeamMemberStoreToCassandra :: Member (Input ClientState) r, Member TinyLog r ) => - FeatureLegalHold -> + FeatureDefaults LegalholdConfig -> Sem (TeamMemberStore InternalPaging ': r) a -> Sem r a interpretTeamMemberStoreToCassandra lh = interpret $ \case @@ -214,7 +215,7 @@ interpretTeamMemberStoreToCassandraWithPaging :: Member (Input ClientState) r, Member TinyLog r ) => - FeatureLegalHold -> + FeatureDefaults LegalholdConfig -> Sem (TeamMemberStore CassandraPaging ': r) a -> Sem r a interpretTeamMemberStoreToCassandraWithPaging lh = interpret $ \case @@ -277,7 +278,7 @@ teamIdsForPagination usr range (fromRange -> max) = Just c -> paginate Cql.selectUserTeamsFrom (paramsP LocalQuorum (usr, c) max) Nothing -> paginate Cql.selectUserTeams (paramsP LocalQuorum (Identity usr) max) -teamMember :: FeatureLegalHold -> TeamId -> UserId -> Client (Maybe TeamMember) +teamMember :: FeatureDefaults LegalholdConfig -> TeamId -> UserId -> Client (Maybe TeamMember) teamMember lh t u = newTeamMember'' u =<< retry x1 (query1 Cql.selectTeamMember (params LocalQuorum (t, u))) where @@ -325,7 +326,7 @@ updateTeamMember oldPerms tid uid newPerms = do addPrepQuery Cql.updatePermissions (newPerms, tid, uid) -- update billing_team_member table - let permDiff = Set.difference `on` view self + let permDiff = Set.difference `on` self acquiredPerms = newPerms `permDiff` oldPerms lostPerms = oldPerms `permDiff` newPerms @@ -368,7 +369,7 @@ teamIdsOf usr tids = map runIdentity <$> retry x1 (query Cql.selectUserTeamsIn (params LocalQuorum (usr, toList tids))) teamMembersWithLimit :: - FeatureLegalHold -> + FeatureDefaults LegalholdConfig -> TeamId -> Range 1 HardTruncationLimit Int32 -> Client TeamMemberList @@ -383,7 +384,7 @@ teamMembersWithLimit lh t (fromRange -> limit) = do -- NOTE: Use this function with care... should only be required when deleting a team! -- Maybe should be left explicitly for the caller? -teamMembersCollectedWithPagination :: FeatureLegalHold -> TeamId -> Client [TeamMember] +teamMembersCollectedWithPagination :: FeatureDefaults LegalholdConfig -> TeamId -> Client [TeamMember] teamMembersCollectedWithPagination lh tid = do mems <- teamMembersForPagination tid Nothing (unsafeRange 2000) collectTeamMembersPaginated [] mems @@ -397,7 +398,7 @@ teamMembersCollectedWithPagination lh tid = do -- Lookup only specific team members: this is particularly useful for large teams when -- needed to look up only a small subset of members (typically 2, user to perform the action -- and the target user) -teamMembersLimited :: FeatureLegalHold -> TeamId -> [UserId] -> Client [TeamMember] +teamMembersLimited :: FeatureDefaults LegalholdConfig -> TeamId -> [UserId] -> Client [TeamMember] teamMembersLimited lh t u = mapM (newTeamMember' lh t) =<< retry x1 (query Cql.selectTeamMembers' (params LocalQuorum (t, u))) @@ -499,7 +500,7 @@ updateTeam tid u = retry x5 . batch $ do -- Throw an exception if one of invitation timestamp and inviter is 'Nothing' and the -- other is 'Just', which can only be caused by inconsistent database content. newTeamMember' :: - FeatureLegalHold -> + FeatureDefaults LegalholdConfig -> TeamId -> (UserId, Permissions, Maybe UserId, Maybe UTCTimeMillis, Maybe UserLegalHoldStatus) -> Client TeamMember @@ -550,7 +551,7 @@ teamMembersForPagination tid start (fromRange -> max) = Nothing -> paginate Cql.selectTeamMembers (paramsP LocalQuorum (Identity tid) max) teamMembersPageFrom :: - FeatureLegalHold -> + FeatureDefaults LegalholdConfig -> TeamId -> Maybe PagingState -> Range 1 HardTruncationLimit Int32 -> @@ -561,7 +562,7 @@ teamMembersPageFrom lh tid pagingState (fromRange -> max) = do pure $ PageWithState members (pwsState page) selectSomeTeamMembersPaginated :: - FeatureLegalHold -> + FeatureDefaults LegalholdConfig -> TeamId -> [UserId] -> Maybe PagingState -> diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/services/galley/src/Galley/Cassandra/TeamFeatures.hs index 618b242efaf..55bb2a9b840 100644 --- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs +++ b/services/galley/src/Galley/Cassandra/TeamFeatures.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE TemplateHaskell #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -17,406 +19,66 @@ module Galley.Cassandra.TeamFeatures ( interpretTeamFeatureStoreToCassandra, - getFeatureConfigMulti, - getAllFeatureConfigsForServer, + getDbFeatureMulti, + getAllTeamFeaturesForServer, ) where import Cassandra -import Cassandra qualified as C -import Control.Monad.Trans.Maybe import Data.Id -import Data.Misc (HttpsUrl) -import Data.Time import Galley.API.Teams.Features.Get -import Galley.Cassandra.GetAllTeamFeatureConfigs +import Galley.Cassandra.FeatureTH +import Galley.Cassandra.GetAllTeamFeatures import Galley.Cassandra.Instances () +import Galley.Cassandra.MakeFeature import Galley.Cassandra.Store import Galley.Cassandra.Util -import Galley.Effects (LegalHoldStore) -import Galley.Effects.LegalHoldStore qualified as LH import Galley.Effects.TeamFeatureStore qualified as TFS -import Galley.Types.Teams (FeatureLegalHold) import Imports import Polysemy import Polysemy.Input import Polysemy.TinyLog import UnliftIO.Async (pooledMapConcurrentlyN) -import Wire.API.Conversation.Protocol (ProtocolTag) -import Wire.API.MLS.CipherSuite import Wire.API.Team.Feature interpretTeamFeatureStoreToCassandra :: ( Member (Embed IO) r, Member (Input ClientState) r, - Member (Input AllFeatureConfigs) r, - Member (Input (Maybe [TeamId], FeatureLegalHold)) r, - Member LegalHoldStore r, Member TinyLog r ) => Sem (TFS.TeamFeatureStore ': r) a -> Sem r a interpretTeamFeatureStoreToCassandra = interpret $ \case - TFS.GetFeatureConfig sing tid -> do + TFS.GetDbFeature sing tid -> do logEffect "TeamFeatureStore.GetFeatureConfig" - embedClient $ getFeatureConfig sing tid - TFS.GetFeatureConfigMulti sing tids -> do + embedClient $ getDbFeature sing tid + TFS.GetDbFeatureMulti sing tids -> do logEffect "TeamFeatureStore.GetFeatureConfigMulti" - embedClient $ getFeatureConfigMulti sing tids - TFS.SetFeatureConfig sing tid wsnl -> do + embedClient $ getDbFeatureMulti sing tids + TFS.SetDbFeature sing tid feat -> do logEffect "TeamFeatureStore.SetFeatureConfig" - embedClient $ setFeatureConfig sing tid wsnl - TFS.GetFeatureLockStatus sing tid -> do - logEffect "TeamFeatureStore.GetFeatureLockStatus" - embedClient $ getFeatureLockStatus sing tid - TFS.SetFeatureLockStatus sing tid ls -> do + embedClient $ setDbFeature sing tid feat + TFS.SetFeatureLockStatus sing tid lock -> do logEffect "TeamFeatureStore.SetFeatureLockStatus" - embedClient $ setFeatureLockStatus sing tid ls - TFS.GetAllFeatureConfigs tid -> do - logEffect "TeamFeatureStore.GetAllFeatureConfigs" - serverConfigs <- input - (allowListForExposeInvitationURLs, featureLH) <- input - hasTeamImplicitLegalhold <- LH.isTeamLegalholdWhitelisted tid - embedClient $ - getAllFeatureConfigs - allowListForExposeInvitationURLs - featureLH - hasTeamImplicitLegalhold - serverConfigs - tid - -getFeatureConfig :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> m (Maybe (WithStatusNoLock cfg)) -getFeatureConfig FeatureSingletonLegalholdConfig tid = getTrivialConfigC "legalhold_status" tid -getFeatureConfig FeatureSingletonSSOConfig tid = getTrivialConfigC "sso_status" tid -getFeatureConfig FeatureSingletonSearchVisibilityAvailableConfig tid = getTrivialConfigC "search_visibility_status" tid -getFeatureConfig FeatureSingletonValidateSAMLEmailsConfig tid = getTrivialConfigC "validate_saml_emails" tid -getFeatureConfig FeatureSingletonClassifiedDomainsConfig _tid = pure Nothing -- TODO(fisx): what's this about? -getFeatureConfig FeatureSingletonDigitalSignaturesConfig tid = getTrivialConfigC "digital_signatures" tid -getFeatureConfig FeatureSingletonAppLockConfig tid = runMaybeT $ do - (mStatus, mEnforce, mTimeout) <- - MaybeT . retry x1 $ - query1 select (params LocalQuorum (Identity tid)) - maybe mzero pure $ - WithStatusNoLock - <$> mStatus - <*> (AppLockConfig <$> mEnforce <*> mTimeout) - -- FUTUREWORK: the above line is duplicated in - -- "Galley.Cassandra.GetAllTeamFeatureConfigs"; make sure the two don't diverge! - <*> Just FeatureTTLUnlimited - where - select :: PrepQuery R (Identity TeamId) (Maybe FeatureStatus, Maybe EnforceAppLock, Maybe Int32) - select = - "select app_lock_status, app_lock_enforce, app_lock_inactivity_timeout_secs \ - \ from team_features where team_id = ?" -getFeatureConfig FeatureSingletonFileSharingConfig tid = getTrivialConfigC "file_sharing" tid -getFeatureConfig FeatureSingletonSelfDeletingMessagesConfig tid = runMaybeT $ do - (mEnabled, mTimeout) <- - MaybeT . retry x1 $ - query1 select (params LocalQuorum (Identity tid)) - maybe mzero pure $ - WithStatusNoLock - <$> mEnabled - <*> fmap SelfDeletingMessagesConfig mTimeout - -- FUTUREWORK: the above line is duplicated in - -- "Galley.Cassandra.GetAllTeamFeatureConfigs"; make sure the two don't diverge! - <*> Just FeatureTTLUnlimited - where - select :: PrepQuery R (Identity TeamId) (Maybe FeatureStatus, Maybe Int32) - select = - "select self_deleting_messages_status, self_deleting_messages_ttl\ - \ from team_features where team_id = ?" -getFeatureConfig FeatureSingletonConferenceCallingConfig tid = do - let q = query1 select (params LocalQuorum (Identity tid)) - retry x1 q <&> \case - Nothing -> Nothing - Just (Nothing, _) -> Nothing - Just (Just status, mTtl) -> - Just - . forgetLock - . setStatus status - . setWsTTL (fromMaybe FeatureTTLUnlimited mTtl) - $ defFeatureStatus - where - select :: PrepQuery R (Identity TeamId) (Maybe FeatureStatus, Maybe FeatureTTL) - select = - fromString $ - "select conference_calling, ttl(conference_calling) from team_features where team_id = ?" -getFeatureConfig FeatureSingletonGuestLinksConfig tid = getTrivialConfigC "guest_links_status" tid -getFeatureConfig FeatureSingletonSndFactorPasswordChallengeConfig tid = getTrivialConfigC "snd_factor_password_challenge_status" tid -getFeatureConfig FeatureSingletonSearchVisibilityInboundConfig tid = getTrivialConfigC "search_visibility_status" tid -getFeatureConfig FeatureSingletonMLSConfig tid = do - m <- retry x1 $ query1 select (params LocalQuorum (Identity tid)) - pure $ case m of - Nothing -> Nothing - Just (status, defaultProtocol, protocolToggleUsers, allowedCipherSuites, defaultCipherSuite, supportedProtocols) -> - WithStatusNoLock - <$> status - <*> ( -- FUTUREWORK: this block is duplicated in - -- "Galley.Cassandra.GetAllTeamFeatureConfigs"; make sure the two don't diverge! - MLSConfig - <$> maybe (Just []) (Just . C.fromSet) protocolToggleUsers - <*> defaultProtocol - <*> maybe (Just []) (Just . C.fromSet) allowedCipherSuites - <*> defaultCipherSuite - <*> maybe (Just []) (Just . C.fromSet) supportedProtocols - ) - <*> Just FeatureTTLUnlimited - where - select :: PrepQuery R (Identity TeamId) (Maybe FeatureStatus, Maybe ProtocolTag, Maybe (C.Set UserId), Maybe (C.Set CipherSuiteTag), Maybe CipherSuiteTag, Maybe (C.Set ProtocolTag)) - select = - "select mls_status, mls_default_protocol, mls_protocol_toggle_users, mls_allowed_ciphersuites, \ - \mls_default_ciphersuite, mls_supported_protocols from team_features where team_id = ?" -getFeatureConfig FeatureSingletonMlsE2EIdConfig tid = do - let q = query1 select (params LocalQuorum (Identity tid)) - retry x1 q <&> \case - Nothing -> Nothing - Just (Nothing, _, _, _, _) -> Nothing - Just (Just fs, mGracePeriod, mUrl, mCrlProxy, mUseProxyOnMobile) -> - Just $ - WithStatusNoLock - fs - ( -- FUTUREWORK: this block is duplicated in - -- "Galley.Cassandra.GetAllTeamFeatureConfigs"; make sure the two don't diverge! - MlsE2EIdConfig (toGracePeriodOrDefault mGracePeriod) mUrl mCrlProxy (fromMaybe (useProxyOnMobile . wsConfig $ defFeatureStatus @MlsE2EIdConfig) mUseProxyOnMobile) - ) - FeatureTTLUnlimited - where - toGracePeriodOrDefault :: Maybe Int32 -> NominalDiffTime - toGracePeriodOrDefault = maybe (verificationExpiration $ wsConfig defFeatureStatus) fromIntegral - - select :: PrepQuery R (Identity TeamId) (Maybe FeatureStatus, Maybe Int32, Maybe HttpsUrl, Maybe HttpsUrl, Maybe Bool) - select = - fromString $ - "select mls_e2eid_status, mls_e2eid_grace_period, mls_e2eid_acme_discovery_url, mls_e2eid_crl_proxy, mls_e2eid_use_proxy_on_mobile from team_features where team_id = ?" -getFeatureConfig FeatureSingletonMlsMigration tid = do - let q = query1 select (params LocalQuorum (Identity tid)) - retry x1 q <&> \case - Nothing -> Nothing - Just (Nothing, _, _) -> Nothing - Just (Just fs, startTime, finaliseRegardlessAfter) -> - Just $ - WithStatusNoLock - fs - -- FUTUREWORK: the following expression is duplicated in - -- "Galley.Cassandra.GetAllTeamFeatureConfigs"; make sure the two don't diverge! - MlsMigrationConfig - { startTime = startTime, - finaliseRegardlessAfter = finaliseRegardlessAfter - } - FeatureTTLUnlimited - where - select :: PrepQuery R (Identity TeamId) (Maybe FeatureStatus, Maybe UTCTime, Maybe UTCTime) - select = "select mls_migration_status, mls_migration_start_time, mls_migration_finalise_regardless_after from team_features where team_id = ?" -getFeatureConfig FeatureSingletonExposeInvitationURLsToTeamAdminConfig tid = getTrivialConfigC "expose_invitation_urls_to_team_admin" tid -getFeatureConfig FeatureSingletonOutlookCalIntegrationConfig tid = getTrivialConfigC "outlook_cal_integration_status" tid -getFeatureConfig FeatureSingletonEnforceFileDownloadLocationConfig tid = do - let q = query1 select (params LocalQuorum (Identity tid)) - retry x1 q <&> \case - Nothing -> Nothing - Just (Nothing, _) -> Nothing - Just (Just fs, mbLocation) -> - Just $ WithStatusNoLock fs (EnforceFileDownloadLocationConfig mbLocation) FeatureTTLUnlimited - where - select :: PrepQuery R (Identity TeamId) (Maybe FeatureStatus, Maybe Text) - select = "select enforce_file_download_location_status, enforce_file_download_location from team_features where team_id = ?" -getFeatureConfig FeatureSingletonLimitedEventFanoutConfig tid = - getTrivialConfigC "limited_event_fanout_status" tid - -setFeatureConfig :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> WithStatusNoLock cfg -> m () -setFeatureConfig FeatureSingletonLegalholdConfig tid statusNoLock = setFeatureStatusC "legalhold_status" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonSSOConfig tid statusNoLock = setFeatureStatusC "sso_status" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonSearchVisibilityAvailableConfig tid statusNoLock = setFeatureStatusC "search_visibility_status" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonValidateSAMLEmailsConfig tid statusNoLock = setFeatureStatusC "validate_saml_emails" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonClassifiedDomainsConfig _tid _statusNoLock = pure () -setFeatureConfig FeatureSingletonDigitalSignaturesConfig tid statusNoLock = setFeatureStatusC "digital_signatures" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonAppLockConfig tid status = do - let enforce = applockEnforceAppLock (wssConfig status) - timeout = applockInactivityTimeoutSecs (wssConfig status) - - retry x5 $ write insert (params LocalQuorum (tid, wssStatus status, enforce, timeout)) - where - insert :: PrepQuery W (TeamId, FeatureStatus, EnforceAppLock, Int32) () - insert = - fromString $ - "insert into team_features (team_id, app_lock_status, app_lock_enforce,\ - \ app_lock_inactivity_timeout_secs) values (?, ?, ?, ?)" -setFeatureConfig FeatureSingletonFileSharingConfig tid statusNoLock = setFeatureStatusC "file_sharing" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonSelfDeletingMessagesConfig tid status = do - let statusValue = wssStatus status - timeout = sdmEnforcedTimeoutSeconds . wssConfig $ status - retry x5 $ write insert (params LocalQuorum (tid, statusValue, timeout)) - where - insert :: PrepQuery W (TeamId, FeatureStatus, Int32) () - insert = - "insert into team_features (team_id, self_deleting_messages_status,\ - \ self_deleting_messages_ttl) values (?, ?, ?)" -setFeatureConfig FeatureSingletonConferenceCallingConfig tid statusNoLock = - retry x5 $ write insert (params LocalQuorum (tid, wssStatus statusNoLock)) - where - renderFeatureTtl :: FeatureTTL -> String - renderFeatureTtl = \case - FeatureTTLSeconds d | d > 0 -> " using ttl " <> show d - _ -> " using ttl 0" -- 0 or unlimited (delete a column's existing TTL by setting its value to zero) - insert :: PrepQuery W (TeamId, FeatureStatus) () - insert = - fromString $ - "insert into team_features (team_id,conference_calling) values (?, ?)" - <> renderFeatureTtl (wssTTL statusNoLock) -setFeatureConfig FeatureSingletonGuestLinksConfig tid statusNoLock = setFeatureStatusC "guest_links_status" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonSndFactorPasswordChallengeConfig tid statusNoLock = - setFeatureStatusC "snd_factor_password_challenge_status" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonSearchVisibilityInboundConfig tid statusNoLock = setFeatureStatusC "search_visibility_status" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonMLSConfig tid statusNoLock = do - let status = wssStatus statusNoLock - let MLSConfig protocolToggleUsers defaultProtocol allowedCipherSuites defaultCipherSuite supportedProtocols = wssConfig statusNoLock - retry x5 $ - write - insert - ( params - LocalQuorum - ( tid, - status, - defaultProtocol, - C.Set protocolToggleUsers, - C.Set allowedCipherSuites, - defaultCipherSuite, - C.Set supportedProtocols - ) - ) - where - insert :: PrepQuery W (TeamId, FeatureStatus, ProtocolTag, C.Set UserId, C.Set CipherSuiteTag, CipherSuiteTag, C.Set ProtocolTag) () - insert = - "insert into team_features (team_id, mls_status, mls_default_protocol, \ - \mls_protocol_toggle_users, mls_allowed_ciphersuites, mls_default_ciphersuite, mls_supported_protocols) values (?, ?, ?, ?, ?, ?, ?)" -setFeatureConfig FeatureSingletonMlsE2EIdConfig tid status = do - let statusValue = wssStatus status - vex = verificationExpiration . wssConfig $ status - mUrl = acmeDiscoveryUrl . wssConfig $ status - mCrlProxy = crlProxy . wssConfig $ status - useProxy = useProxyOnMobile . wssConfig $ status - retry x5 $ write insert (params LocalQuorum (tid, statusValue, truncate vex, mUrl, mCrlProxy, useProxy)) - where - insert :: PrepQuery W (TeamId, FeatureStatus, Int32, Maybe HttpsUrl, Maybe HttpsUrl, Bool) () - insert = - "insert into team_features (team_id, mls_e2eid_status, mls_e2eid_grace_period, mls_e2eid_acme_discovery_url, mls_e2eid_crl_proxy, mls_e2eid_use_proxy_on_mobile) values (?, ?, ?, ?, ?, ?)" -setFeatureConfig FeatureSingletonMlsMigration tid status = do - let statusValue = wssStatus status - config = wssConfig status - - retry x5 $ write insert (params LocalQuorum (tid, statusValue, config.startTime, config.finaliseRegardlessAfter)) - where - insert :: PrepQuery W (TeamId, FeatureStatus, Maybe UTCTime, Maybe UTCTime) () - insert = - "insert into team_features (team_id, mls_migration_status, mls_migration_start_time, mls_migration_finalise_regardless_after) values (?, ?, ?, ?)" -setFeatureConfig FeatureSingletonExposeInvitationURLsToTeamAdminConfig tid statusNoLock = setFeatureStatusC "expose_invitation_urls_to_team_admin" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonOutlookCalIntegrationConfig tid statusNoLock = setFeatureStatusC "outlook_cal_integration_status" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonEnforceFileDownloadLocationConfig tid status = do - let statusValue = wssStatus status - config = wssConfig status - - retry x5 $ write insert (params LocalQuorum (tid, statusValue, config.enforcedDownloadLocation)) - where - insert :: PrepQuery W (TeamId, FeatureStatus, Maybe Text) () - insert = - "insert into team_features (team_id, enforce_file_download_location_status, enforce_file_download_location) values (?, ?, ?)" -setFeatureConfig FeatureSingletonLimitedEventFanoutConfig tid statusNoLock = - setFeatureStatusC "limited_event_fanout_status" tid (wssStatus statusNoLock) - -getFeatureLockStatus :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> m (Maybe LockStatus) -getFeatureLockStatus FeatureSingletonFileSharingConfig tid = getLockStatusC "file_sharing_lock_status" tid -getFeatureLockStatus FeatureSingletonSelfDeletingMessagesConfig tid = getLockStatusC "self_deleting_messages_lock_status" tid -getFeatureLockStatus FeatureSingletonGuestLinksConfig tid = getLockStatusC "guest_links_lock_status" tid -getFeatureLockStatus FeatureSingletonSndFactorPasswordChallengeConfig tid = getLockStatusC "snd_factor_password_challenge_lock_status" tid -getFeatureLockStatus FeatureSingletonMlsE2EIdConfig tid = getLockStatusC "mls_e2eid_lock_status" tid -getFeatureLockStatus FeatureSingletonMlsMigration tid = getLockStatusC "mls_migration_lock_status" tid -getFeatureLockStatus FeatureSingletonOutlookCalIntegrationConfig tid = getLockStatusC "outlook_cal_integration_lock_status" tid -getFeatureLockStatus FeatureSingletonMLSConfig tid = getLockStatusC "mls_lock_status" tid -getFeatureLockStatus FeatureSingletonEnforceFileDownloadLocationConfig tid = getLockStatusC "enforce_file_download_location_lock_status" tid -getFeatureLockStatus _ _ = pure Nothing + embedClient $ setFeatureLockStatus sing tid (Tagged lock) + TFS.GetAllDbFeatures tid -> do + logEffect "TeamFeatureStore.GetAllTeamFeatures" + embedClient $ getAllDbFeatures tid -setFeatureLockStatus :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> LockStatus -> m () -setFeatureLockStatus FeatureSingletonFileSharingConfig tid status = setLockStatusC "file_sharing_lock_status" tid status -setFeatureLockStatus FeatureSingletonSelfDeletingMessagesConfig tid status = setLockStatusC "self_deleting_messages_lock_status" tid status -setFeatureLockStatus FeatureSingletonGuestLinksConfig tid status = setLockStatusC "guest_links_lock_status" tid status -setFeatureLockStatus FeatureSingletonSndFactorPasswordChallengeConfig tid status = setLockStatusC "snd_factor_password_challenge_lock_status" tid status -setFeatureLockStatus FeatureSingletonMlsE2EIdConfig tid status = setLockStatusC "mls_e2eid_lock_status" tid status -setFeatureLockStatus FeatureSingletonMlsMigration tid status = setLockStatusC "mls_migration_lock_status" tid status -setFeatureLockStatus FeatureSingletonOutlookCalIntegrationConfig tid status = setLockStatusC "outlook_cal_integration_lock_status" tid status -setFeatureLockStatus FeatureSingletonMLSConfig tid status = setLockStatusC "mls_lock_status" tid status -setFeatureLockStatus FeatureSingletonEnforceFileDownloadLocationConfig tid status = setLockStatusC "enforce_file_download_location_lock_status" tid status -setFeatureLockStatus _ _tid _status = pure () - -getTrivialConfigC :: - forall m cfg. - (MonadClient m, IsFeatureConfig cfg) => - String -> - TeamId -> - m (Maybe (WithStatusNoLock cfg)) -getTrivialConfigC statusCol tid = do - let q = query1 select (params LocalQuorum (Identity tid)) - mFeatureStatus <- (>>= runIdentity) <$> retry x1 q - pure $ case mFeatureStatus of - Nothing -> Nothing - Just status -> Just . forgetLock $ setStatus status defFeatureStatus - where - select :: PrepQuery R (Identity TeamId) (Identity (Maybe FeatureStatus)) - select = - fromString $ - "select " - <> statusCol - <> " from team_features where team_id = ?" - -setFeatureStatusC :: - forall m. - (MonadClient m) => - String -> - TeamId -> - FeatureStatus -> - m () -setFeatureStatusC statusCol tid status = do - retry x5 $ write insert (params LocalQuorum (tid, status)) - where - insert :: PrepQuery W (TeamId, FeatureStatus) () - insert = - fromString $ - "insert into team_features (team_id, " <> statusCol <> ") values (?, ?)" - -getLockStatusC :: - forall m. - (MonadClient m) => - String -> - TeamId -> - m (Maybe LockStatus) -getLockStatusC lockStatusCol tid = do - let q = query1 select (params LocalQuorum (Identity tid)) - (>>= runIdentity) <$> retry x1 q - where - select :: PrepQuery R (Identity TeamId) (Identity (Maybe LockStatus)) - select = - fromString $ - "select " - <> lockStatusCol - <> " from team_features where team_id = ?" - -setLockStatusC :: - (MonadClient m) => - String -> - TeamId -> - LockStatus -> - m () -setLockStatusC col tid status = do - retry x5 $ write insert (params LocalQuorum (tid, status)) - where - insert :: PrepQuery W (TeamId, LockStatus) () - insert = - fromString $ - "insert into team_features (team_id, " <> col <> ") values (?, ?)" - -getFeatureConfigMulti :: +getDbFeatureMulti :: forall cfg m. (MonadClient m, MonadUnliftIO m) => FeatureSingleton cfg -> [TeamId] -> - m [(TeamId, Maybe (WithStatusNoLock cfg))] -getFeatureConfigMulti proxy = - pooledMapConcurrentlyN 8 (\tid -> getFeatureConfig proxy tid <&> (tid,)) + m [(TeamId, DbFeature cfg)] +getDbFeatureMulti proxy = + pooledMapConcurrentlyN 8 (\tid -> getDbFeature proxy tid <&> (tid,)) + +getDbFeature :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> m (DbFeature cfg) +getDbFeature = $(featureCases [|fetchFeature|]) + +setDbFeature :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> LockableFeature cfg -> m () +setDbFeature = $(featureCases [|storeFeature|]) + +setFeatureLockStatus :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> Tagged cfg LockStatus -> m () +setFeatureLockStatus = $(featureCases [|storeFeatureLockStatus|]) diff --git a/services/galley/src/Galley/Data/Conversation.hs b/services/galley/src/Galley/Data/Conversation.hs index fe18548b2c0..aeec69ae609 100644 --- a/services/galley/src/Galley/Data/Conversation.hs +++ b/services/galley/src/Galley/Data/Conversation.hs @@ -27,7 +27,6 @@ module Galley.Data.Conversation convAccess, convAccessData, convAccessRoles, - convCreator, convMessageTimer, convName, convReceiptMode, @@ -86,9 +85,6 @@ convAccessData c = (Set.fromList (convAccess c)) (convAccessRoles c) -convCreator :: Conversation -> Maybe UserId -convCreator = cnvmCreator . convMetadata - convName :: Conversation -> Maybe Text convName = cnvmName . convMetadata diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs index 35279a4882e..1a9be889d25 100644 --- a/services/galley/src/Galley/Effects.hs +++ b/services/galley/src/Galley/Effects.hs @@ -91,14 +91,14 @@ import Galley.Effects.TeamNotificationStore import Galley.Effects.TeamStore import Galley.Env import Galley.Options -import Galley.Types.Teams (FeatureLegalHold) +import Galley.Types.Teams import Imports import Polysemy import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog import Wire.API.Error -import Wire.API.Team.Feature (AllFeatureConfigs) +import Wire.API.Team.Feature import Wire.GundeckAPIAccess import Wire.NotificationSubsystem import Wire.Rpc @@ -138,8 +138,8 @@ type GalleyEffects1 = ListItems LegacyPaging ConvId, ListItems LegacyPaging TeamId, ListItems InternalPaging TeamId, - Input AllFeatureConfigs, - Input (Maybe [TeamId], FeatureLegalHold), + Input AllTeamFeatures, + Input (Maybe [TeamId], FeatureDefaults LegalholdConfig), Input (Local ()), Input Opts, Input UTCTime, diff --git a/services/galley/src/Galley/Effects/BrigAccess.hs b/services/galley/src/Galley/Effects/BrigAccess.hs index c825a3e7129..0e5725513c4 100644 --- a/services/galley/src/Galley/Effects/BrigAccess.hs +++ b/services/galley/src/Galley/Effects/BrigAccess.hs @@ -35,6 +35,7 @@ module Galley.Effects.BrigAccess deleteUser, getContactList, getRichInfoMultiUser, + getUserExportData, -- * Teams getSize, @@ -71,9 +72,9 @@ import Wire.API.Error.Galley import Wire.API.MLS.CipherSuite import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Multi +import Wire.API.Team.Export import Wire.API.Team.Feature import Wire.API.Team.Size -import Wire.API.User import Wire.API.User.Auth.ReAuth import Wire.API.User.Client import Wire.API.User.Client.Prekey @@ -99,7 +100,7 @@ data BrigAccess m a where PutConnectionInternal :: UpdateConnectionsInternal -> BrigAccess m Status ReauthUser :: UserId -> ReAuthUser -> BrigAccess m (Either AuthenticationError ()) LookupActivatedUsers :: [UserId] -> BrigAccess m [User] - GetUsers :: [UserId] -> BrigAccess m [UserAccount] + GetUsers :: [UserId] -> BrigAccess m [User] DeleteUser :: UserId -> BrigAccess m () GetContactList :: UserId -> BrigAccess m [UserId] GetRichInfoMultiUser :: [UserId] -> BrigAccess m [(UserId, RichInfo)] @@ -122,15 +123,16 @@ data BrigAccess m a where LastPrekey -> BrigAccess m (Either AuthenticationError ClientId) RemoveLegalHoldClientFromUser :: UserId -> BrigAccess m () - GetAccountConferenceCallingConfigClient :: UserId -> BrigAccess m (WithStatusNoLock ConferenceCallingConfig) + GetAccountConferenceCallingConfigClient :: UserId -> BrigAccess m (Feature ConferenceCallingConfig) GetLocalMLSClients :: Local UserId -> CipherSuiteTag -> BrigAccess m (Set ClientInfo) UpdateSearchVisibilityInbound :: Multi.TeamStatus SearchVisibilityInboundConfig -> BrigAccess m () + GetUserExportData :: UserId -> BrigAccess m (Maybe TeamExportUser) makeSem ''BrigAccess -getUser :: (Member BrigAccess r) => UserId -> Sem r (Maybe UserAccount) +getUser :: (Member BrigAccess r) => UserId -> Sem r (Maybe User) getUser = fmap listToMaybe . getUsers . pure addLegalHoldClientToUser :: diff --git a/services/galley/src/Galley/Effects/FederatorAccess.hs b/services/galley/src/Galley/Effects/FederatorAccess.hs index eaa5e70ba01..73ab4ca9844 100644 --- a/services/galley/src/Galley/Effects/FederatorAccess.hs +++ b/services/galley/src/Galley/Effects/FederatorAccess.hs @@ -25,7 +25,6 @@ module Galley.Effects.FederatorAccess runFederatedConcurrently, runFederatedConcurrentlyEither, runFederatedConcurrentlyBucketsEither, - runFederatedConcurrently_, isFederationConfigured, ) where @@ -71,10 +70,3 @@ data FederatorAccess m a where IsFederationConfigured :: FederatorAccess m Bool makeSem ''FederatorAccess - -runFederatedConcurrently_ :: - (KnownComponent c, Foldable f, Functor f, Member FederatorAccess r) => - f (Remote a) -> - (Remote [a] -> FederatorClient c x) -> - Sem r () -runFederatedConcurrently_ xs = void . runFederatedConcurrently xs diff --git a/services/galley/src/Galley/Effects/SparAccess.hs b/services/galley/src/Galley/Effects/SparAccess.hs index 4b9b0df882d..f84e3ac87ec 100644 --- a/services/galley/src/Galley/Effects/SparAccess.hs +++ b/services/galley/src/Galley/Effects/SparAccess.hs @@ -25,6 +25,6 @@ import Wire.API.User (ScimUserInfo) data SparAccess m a where DeleteTeam :: TeamId -> SparAccess m () - LookupScimUserInfos :: [UserId] -> SparAccess m [ScimUserInfo] + LookupScimUserInfo :: UserId -> SparAccess m ScimUserInfo makeSem ''SparAccess diff --git a/services/galley/src/Galley/Effects/TeamFeatureStore.hs b/services/galley/src/Galley/Effects/TeamFeatureStore.hs index 5011d72a3ce..a7773e1fe7f 100644 --- a/services/galley/src/Galley/Effects/TeamFeatureStore.hs +++ b/services/galley/src/Galley/Effects/TeamFeatureStore.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE TemplateHaskell #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -20,35 +18,60 @@ module Galley.Effects.TeamFeatureStore where import Data.Id -import Imports import Polysemy import Wire.API.Team.Feature data TeamFeatureStore m a where - GetFeatureConfig :: + -- | Returns all stored feature values excluding lock status. + GetDbFeature :: FeatureSingleton cfg -> TeamId -> - TeamFeatureStore m (Maybe (WithStatusNoLock cfg)) - GetFeatureConfigMulti :: + TeamFeatureStore m (DbFeature cfg) + GetDbFeatureMulti :: FeatureSingleton cfg -> [TeamId] -> - TeamFeatureStore m [(TeamId, Maybe (WithStatusNoLock cfg))] - SetFeatureConfig :: + TeamFeatureStore m [(TeamId, DbFeature cfg)] + SetDbFeature :: FeatureSingleton cfg -> TeamId -> - WithStatusNoLock cfg -> + LockableFeature cfg -> TeamFeatureStore m () - GetFeatureLockStatus :: - FeatureSingleton cfg -> - TeamId -> - TeamFeatureStore m (Maybe LockStatus) SetFeatureLockStatus :: FeatureSingleton cfg -> TeamId -> LockStatus -> TeamFeatureStore m () - GetAllFeatureConfigs :: + GetAllDbFeatures :: TeamId -> - TeamFeatureStore m AllFeatureConfigs + TeamFeatureStore m (AllFeatures DbFeature) + +getDbFeature :: + (Member TeamFeatureStore r, IsFeatureConfig cfg) => + TeamId -> + Sem r (DbFeature cfg) +getDbFeature tid = send (GetDbFeature featureSingleton tid) + +getDbFeatureMulti :: + (Member TeamFeatureStore r, IsFeatureConfig cfg) => + [TeamId] -> + Sem r [(TeamId, DbFeature cfg)] +getDbFeatureMulti tids = send (GetDbFeatureMulti featureSingleton tids) + +setDbFeature :: + (Member TeamFeatureStore r, IsFeatureConfig cfg) => + TeamId -> + LockableFeature cfg -> + Sem r () +setDbFeature tid feat = send (SetDbFeature featureSingleton tid feat) + +setFeatureLockStatus :: + forall cfg r. + (Member TeamFeatureStore r, IsFeatureConfig cfg) => + TeamId -> + LockStatus -> + Sem r () +setFeatureLockStatus tid lockStatus = + send (SetFeatureLockStatus (featureSingleton @cfg) tid lockStatus) -makeSem ''TeamFeatureStore +getAllDbFeatures :: (Member TeamFeatureStore r) => TeamId -> Sem r (AllFeatures DbFeature) +getAllDbFeatures tid = send (GetAllDbFeatures tid) diff --git a/services/galley/src/Galley/Effects/TeamStore.hs b/services/galley/src/Galley/Effects/TeamStore.hs index bd403e17f55..6ce47062720 100644 --- a/services/galley/src/Galley/Effects/TeamStore.hs +++ b/services/galley/src/Galley/Effects/TeamStore.hs @@ -90,6 +90,7 @@ import Wire.API.Error.Galley import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Team import Wire.API.Team.Conversation +import Wire.API.Team.Feature import Wire.API.Team.Member (HardTruncationLimit, TeamMember, TeamMemberList) import Wire.API.Team.Permission import Wire.Sem.Paging @@ -124,6 +125,8 @@ data TeamStore m a where Maybe (PagingState CassandraPaging TeamMember) -> PagingBounds CassandraPaging TeamMember -> TeamStore m (Page CassandraPaging TeamMember) + -- FUTUREWORK(mangoiv): this should be a single 'TeamId' (@'Maybe' 'TeamId'@), there's no way + -- a user could be part of multiple teams GetUserTeams :: UserId -> TeamStore m [TeamId] GetUsersTeams :: [UserId] -> TeamStore m (Map UserId TeamId) GetOneUserTeam :: UserId -> TeamStore m (Maybe TeamId) @@ -135,7 +138,7 @@ data TeamStore m a where SetTeamData :: TeamId -> TeamUpdateData -> TeamStore m () SetTeamStatus :: TeamId -> TeamStatus -> TeamStore m () FanoutLimit :: TeamStore m (Range 1 HardTruncationLimit Int32) - GetLegalHoldFlag :: TeamStore m FeatureLegalHold + GetLegalHoldFlag :: TeamStore m (FeatureDefaults LegalholdConfig) EnqueueTeamEvent :: E.TeamEvent -> TeamStore m () makeSem ''TeamStore diff --git a/services/galley/src/Galley/External/LegalHoldService.hs b/services/galley/src/Galley/External/LegalHoldService.hs index cca80ae8800..ddc157b017f 100644 --- a/services/galley/src/Galley/External/LegalHoldService.hs +++ b/services/galley/src/Galley/External/LegalHoldService.hs @@ -30,11 +30,15 @@ where import Bilge qualified import Bilge.Response import Brig.Types.Team.LegalHold +import Control.Monad.Catch (MonadThrow (throwM)) import Data.Aeson +import Data.ByteString.Char8 qualified as BS8 import Data.ByteString.Conversion.To import Data.ByteString.Lazy.Char8 qualified as LC8 import Data.Id import Data.Misc +import Data.Qualified (Local, QualifiedWithTag (tUntagged), tUnqualified) +import Data.Set qualified as Set import Galley.Effects.LegalHoldStore as LegalHoldData import Galley.External.LegalHoldService.Types import Imports @@ -49,6 +53,30 @@ import Wire.API.Team.LegalHold.External ---------------------------------------------------------------------- -- api +data LhApiVersion = V0 | V1 + deriving stock (Eq, Ord, Show, Enum, Bounded, Generic) + +-- | Get /api-version from legal hold service; this does not throw an error because the api-version endpoint may not exist. +getLegalHoldApiVersions :: + ( Member (ErrorS 'LegalHoldServiceNotRegistered) r, + Member LegalHoldStore r + ) => + TeamId -> + Sem r (Maybe (Set LhApiVersion)) +getLegalHoldApiVersions tid = + fmap toLhApiVersion . decode . (.responseBody) <$> makeLegalHoldServiceRequest tid params + where + params = + Bilge.paths ["api-version"] + . Bilge.method GET + . Bilge.acceptJson + + toLhApiVersion :: SupportedVersions -> Set LhApiVersion + toLhApiVersion (SupportedVersions supported) = Set.fromList $ mapMaybe toVersion supported + where + toVersion 0 = Just V0 + toVersion 1 = Just V1 + toVersion _ = Nothing -- | Get /status from legal hold service; throw 'Wai.Error' if things go wrong. checkLegalHoldServiceStatus :: @@ -78,66 +106,119 @@ requestNewDevice :: ( Member (ErrorS 'LegalHoldServiceBadResponse) r, Member (ErrorS 'LegalHoldServiceNotRegistered) r, Member LegalHoldStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member (Embed IO) r ) => TeamId -> - UserId -> + Local UserId -> Sem r NewLegalHoldClient -requestNewDevice tid uid = do - resp <- makeLegalHoldServiceRequest tid reqParams +requestNewDevice tid luid = do + apiVersion <- negotiateVersion tid + resp <- makeLegalHoldServiceRequest tid (reqParams apiVersion) case eitherDecode (responseBody resp) of Left e -> do P.info . Log.msg $ "Error decoding NewLegalHoldClient: " <> e throwS @'LegalHoldServiceBadResponse Right client -> pure client where - reqParams = - Bilge.paths ["initiate"] - . Bilge.json (RequestNewLegalHoldClient uid tid) + reqParams v = + versionedPaths v ["initiate"] + . mkBody v . Bilge.method POST . Bilge.acceptJson . Bilge.expect2xx + mkBody :: LhApiVersion -> Bilge.Request -> Bilge.Request + mkBody V0 = + Bilge.json + RequestNewLegalHoldClientV0 + { userId = tUnqualified luid, + teamId = tid + } + mkBody V1 = + Bilge.json + RequestNewLegalHoldClient + { userId = tUntagged luid, + teamId = tid + } + -- | @POST /confirm@ -- Confirm that a device has been linked to a user and provide an authorization token confirmLegalHold :: ( Member (ErrorS 'LegalHoldServiceNotRegistered) r, - Member LegalHoldStore r + Member P.TinyLog r, + Member LegalHoldStore r, + Member (Embed IO) r ) => ClientId -> TeamId -> - UserId -> + Local UserId -> -- | TODO: Replace with 'LegalHold' token type OpaqueAuthToken -> Sem r () -confirmLegalHold clientId tid uid legalHoldAuthToken = do - void $ makeLegalHoldServiceRequest tid reqParams +confirmLegalHold clientId tid luid legalHoldAuthToken = do + apiVersion <- negotiateVersion tid + void $ makeLegalHoldServiceRequest tid (reqParams apiVersion) where - reqParams = - Bilge.paths ["confirm"] - . Bilge.json (LegalHoldServiceConfirm clientId uid tid (opaqueAuthTokenToText legalHoldAuthToken)) + reqParams v = + versionedPaths v ["confirm"] + . mkBody v . Bilge.method POST . Bilge.acceptJson . Bilge.expect2xx + mkBody :: LhApiVersion -> Bilge.Request -> Bilge.Request + mkBody V0 = + Bilge.json + LegalHoldServiceConfirmV0 + { lhcClientId = clientId, + lhcUserId = tUnqualified luid, + lhcTeamId = tid, + lhcRefreshToken = opaqueAuthTokenToText legalHoldAuthToken + } + mkBody V1 = + Bilge.json + LegalHoldServiceConfirm + { clientId = clientId, + userId = tUntagged luid, + teamId = tid, + refreshToken = opaqueAuthTokenToText legalHoldAuthToken + } + -- | @POST /remove@ -- Inform the LegalHold Service that a user's legalhold has been disabled. removeLegalHold :: ( Member (ErrorS 'LegalHoldServiceNotRegistered) r, - Member LegalHoldStore r + Member P.TinyLog r, + Member LegalHoldStore r, + Member (Embed IO) r ) => TeamId -> - UserId -> + Local UserId -> Sem r () removeLegalHold tid uid = do - void $ makeLegalHoldServiceRequest tid reqParams + apiVersion <- negotiateVersion tid + void $ makeLegalHoldServiceRequest tid (reqParams apiVersion) where - reqParams = - Bilge.paths ["remove"] - . Bilge.json (LegalHoldServiceRemove uid tid) + reqParams v = + versionedPaths v ["remove"] + . mkBody v . Bilge.method POST . Bilge.acceptJson . Bilge.expect2xx + mkBody :: LhApiVersion -> Bilge.Request -> Bilge.Request + mkBody V0 = + Bilge.json + LegalHoldServiceRemoveV0 + { lhrUserId = tUnqualified uid, + lhrTeamId = tid + } + mkBody V1 = + Bilge.json + LegalHoldServiceRemove + { userId = tUntagged uid, + teamId = tid + } ---------------------------------------------------------------------- -- helpers @@ -167,3 +248,46 @@ makeLegalHoldServiceRequest tid reqBuilder = do mkReqBuilder token = reqBuilder . Bilge.header "Authorization" ("Bearer " <> toByteString' token) + +versionToInt :: LhApiVersion -> Int +versionToInt V0 = 0 +versionToInt V1 = 1 + +versionToBS :: LhApiVersion -> ByteString +versionToBS = ("v" <>) . BS8.pack . show . versionToInt + +versionedPaths :: LhApiVersion -> [ByteString] -> Http.Request -> Http.Request +versionedPaths V0 paths = Bilge.paths paths +versionedPaths v paths = Bilge.paths (versionToBS v : paths) + +supportedByWireServer :: Set LhApiVersion +supportedByWireServer = Set.fromList [minBound .. maxBound] + +-- | Find the highest common version between wire-server and the legalhold service. +-- If the legalhold service does not support the `/api-version` endpoint, we assume it's `v0`. +negotiateVersion :: + ( Member (ErrorS 'LegalHoldServiceNotRegistered) r, + Member LegalHoldStore r, + Member P.TinyLog r, + Member (Embed IO) r + ) => + TeamId -> + Sem r LhApiVersion +negotiateVersion tid = do + mSupportedByExternalLhService <- getLegalHoldApiVersions tid + case mSupportedByExternalLhService of + Nothing -> pure V0 + Just supportedByLhService -> do + let commonVersions = Set.intersection supportedByWireServer supportedByLhService + case Set.lookupMax commonVersions of + Nothing -> do + P.warn $ + Log.msg (Log.val "Version negotiation with legal hold service failed. No common versions found.") + . Log.field "team_id" (show tid) + liftIO $ throwM LegalHoldNoCommonVersions + Just v -> pure v + +data LegalHoldVersionNegotiationException = LegalHoldNoCommonVersions + deriving (Show) + +instance Exception LegalHoldVersionNegotiationException diff --git a/services/galley/src/Galley/Intra/Effects.hs b/services/galley/src/Galley/Intra/Effects.hs index ef071400ab0..88fc983c8e1 100644 --- a/services/galley/src/Galley/Intra/Effects.hs +++ b/services/galley/src/Galley/Intra/Effects.hs @@ -83,6 +83,9 @@ interpretBrigAccess = interpret $ \case GetRichInfoMultiUser uids -> do logEffect "BrigAccess.GetRichInfoMultiUser" embedApp $ getRichInfoMultiUser uids + GetUserExportData uid -> do + logEffect "BrigAccess.GetUserExportData" + embedApp $ getUserExportData uid GetSize tid -> do logEffect "BrigAccess.GetSize" embedApp $ getSize tid @@ -125,9 +128,9 @@ interpretSparAccess = interpret $ \case DeleteTeam tid -> do logEffect "SparAccess.DeleteTeam" embedApp $ deleteTeam tid - LookupScimUserInfos uids -> do - logEffect "SparAccess.LookupScimUserInfos" - embedApp $ lookupScimUserInfos uids + LookupScimUserInfo uid -> do + logEffect "SparAccess.LookupScimUserInfo" + embedApp $ lookupScimUserInfo uid interpretBotAccess :: ( Member (Embed IO) r, diff --git a/services/galley/src/Galley/Intra/Spar.hs b/services/galley/src/Galley/Intra/Spar.hs index 989df2563ed..3fede63dc16 100644 --- a/services/galley/src/Galley/Intra/Spar.hs +++ b/services/galley/src/Galley/Intra/Spar.hs @@ -17,19 +17,18 @@ module Galley.Intra.Spar ( deleteTeam, - lookupScimUserInfos, + lookupScimUserInfo, ) where import Bilge import Data.ByteString.Conversion import Data.Id -import Data.Set qualified as Set import Galley.Intra.Util import Galley.Monad import Imports import Network.HTTP.Types.Method -import Wire.API.User (ScimUserInfo, UserSet (..), scimUserInfos) +import Wire.API.User (ScimUserInfo) -- | Notify Spar that a team is being deleted. deleteTeam :: TeamId -> App () @@ -40,11 +39,10 @@ deleteTeam tid = do . expect2xx -- | Get the SCIM user info for a user. -lookupScimUserInfos :: [UserId] -> App [ScimUserInfo] -lookupScimUserInfos uids = do +lookupScimUserInfo :: UserId -> App ScimUserInfo +lookupScimUserInfo uid = do response <- call Spar $ method POST - . paths ["i", "scim", "userinfos"] - . json (UserSet $ Set.fromList uids) - pure $ foldMap scimUserInfos $ responseJsonMaybe response + . paths ["i", "scim", "userinfo", toByteString' uid] + responseJsonError response diff --git a/services/galley/src/Galley/Intra/User.hs b/services/galley/src/Galley/Intra/User.hs index 5419b68ecea..27ced33fdee 100644 --- a/services/galley/src/Galley/Intra/User.hs +++ b/services/galley/src/Galley/Intra/User.hs @@ -30,6 +30,7 @@ module Galley.Intra.User getContactList, chunkify, getRichInfoMultiUser, + getUserExportData, getAccountConferenceCallingConfigClient, updateSearchVisibilityInbound, ) @@ -37,9 +38,8 @@ where import Bilge hiding (getHeader, host, options, port, statusCode) import Bilge.RPC -import Brig.Types.Intra qualified as Brig import Control.Error hiding (bool, isRight) -import Control.Lens (view, (^.)) +import Control.Lens (view) import Control.Monad.Catch import Data.ByteString.Char8 (pack) import Data.ByteString.Char8 qualified as BSC @@ -67,6 +67,7 @@ import Wire.API.Routes.Internal.Brig qualified as IAPI import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Multi import Wire.API.Routes.Named +import Wire.API.Team.Export import Wire.API.Team.Feature import Wire.API.User import Wire.API.User.Auth.ReAuth @@ -198,7 +199,7 @@ chunkify doChunk keys = mconcat <$> (doChunk `mapM` chunks keys) chunks uids = case splitAt maxSize uids of (h, t) -> h : chunks t -- | Calls 'Brig.API.listActivatedAccountsH'. -getUsers :: [UserId] -> App [Brig.UserAccount] +getUsers :: [UserId] -> App [User] getUsers = chunkify $ \uids -> do resp <- call Brig $ @@ -238,7 +239,17 @@ getRichInfoMultiUser = chunkify $ \uids -> do . expect2xx parseResponse (mkError status502 "server-error: could not parse response to `GET brig:/i/users/rich-info`") resp -getAccountConferenceCallingConfigClient :: (HasCallStack) => UserId -> App (WithStatusNoLock ConferenceCallingConfig) +-- | Calls 'Brig.API.Internal.getUserExportDataH' +getUserExportData :: UserId -> App (Maybe TeamExportUser) +getUserExportData uid = do + resp <- + call Brig $ + method GET + . paths ["i/users", toByteString' uid, "export-data"] + . expect2xx + parseResponse (mkError status502 "server-error: could not parse response to `GET brig:/i/users/:uid/export-data`") resp + +getAccountConferenceCallingConfigClient :: (HasCallStack) => UserId -> App (Feature ConferenceCallingConfig) getAccountConferenceCallingConfigClient uid = runHereClientM (namedClient @IAPI.API @"get-account-conference-calling-config" uid) >>= handleServantResp @@ -254,7 +265,7 @@ runHereClientM action = do mgr <- view manager brigep <- view brig let env = Client.mkClientEnv mgr baseurl - baseurl = Client.BaseUrl Client.Http (Text.unpack $ brigep ^. host) (fromIntegral $ brigep ^. port) "" + baseurl = Client.BaseUrl Client.Http (Text.unpack brigep.host) (fromIntegral brigep.port) "" liftIO $ Client.runClientM action env handleServantResp :: diff --git a/services/galley/src/Galley/Intra/Util.hs b/services/galley/src/Galley/Intra/Util.hs index 0ebff1f349f..fa24f814ae5 100644 --- a/services/galley/src/Galley/Intra/Util.hs +++ b/services/galley/src/Galley/Intra/Util.hs @@ -18,16 +18,15 @@ module Galley.Intra.Util ( IntraComponent (..), call, - asyncCall, ) where import Bilge hiding (getHeader, host, options, port, statusCode) import Bilge qualified as B -import Bilge.RPC +import Bilge.RPC (rpc) import Bilge.Retry -import Control.Lens (view, (^.)) -import Control.Monad.Catch +import Cassandra.Options (Endpoint (..)) +import Control.Lens (view) import Control.Retry import Data.ByteString.Lazy qualified as LB import Data.Misc (portNumber) @@ -38,9 +37,6 @@ import Galley.Monad import Galley.Options import Imports hiding (log) import Network.HTTP.Types -import System.Logger -import System.Logger.Class qualified as LC -import Util.Options data IntraComponent = Brig | Spar | Gundeck deriving (Show) @@ -52,14 +48,14 @@ componentName Gundeck = "gundeck" componentRequest :: IntraComponent -> Opts -> Request -> Request componentRequest Brig o = - B.host (encodeUtf8 (o ^. brig . host)) - . B.port (portNumber (fromIntegral (o ^. brig . port))) + 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 (o ^. spar . port))) + B.host (encodeUtf8 o._spar.host) + . B.port (portNumber $ fromIntegral . port $ o._spar) componentRequest Gundeck o = - B.host (encodeUtf8 $ o ^. gundeck . host) - . B.port (portNumber $ fromIntegral (o ^. gundeck . port)) + B.host (encodeUtf8 o._gundeck.host) + . B.port (portNumber $ fromIntegral . port $ o._gundeck) . method POST . path "/i/push/v2" . expect2xx @@ -79,15 +75,5 @@ call comp r = do let n = LT.pack (componentName comp) recovering (componentRetryPolicy comp) rpcHandlers (const (rpc n (r . r0))) -asyncCall :: IntraComponent -> (Request -> Request) -> App () -asyncCall comp req = void $ do - let n = LT.pack (componentName comp) - forkIO $ catches (void (call comp req)) (handlers n) - where - handlers n = - [ Handler $ \(x :: RPCException) -> LC.err (rpcExceptionMsg x), - Handler $ \(x :: SomeException) -> LC.err $ "remote" .= n ~~ msg (show x) - ] - x1 :: RetryPolicy x1 = limitRetries 1 diff --git a/services/galley/src/Galley/Options.hs b/services/galley/src/Galley/Options.hs index 3e1b97aa1cf..1f916761387 100644 --- a/services/galley/src/Galley/Options.hs +++ b/services/galley/src/Galley/Options.hs @@ -18,7 +18,7 @@ -- with this program. If not, see . module Galley.Options - ( Settings, + ( Settings (..), httpPoolSize, maxTeamSize, maxFanoutSize, @@ -31,13 +31,14 @@ module Galley.Options concurrentDeletionEvents, deleteConvThrottleMillis, federationDomain, + federationProtocols, mlsPrivateKeyPaths, featureFlags, defConcurrentDeletionEvents, JournalOpts (JournalOpts), queueName, endpoint, - Opts, + Opts (..), galley, cassandra, brig, @@ -53,6 +54,7 @@ module Galley.Options logFormat, guestLinkTTLSeconds, defGuestLinkTTLSeconds, + passwordHashingOptions, GuestLinkTTLSeconds (..), ) where @@ -71,6 +73,7 @@ import Network.AMQP.Extended import System.Logger.Extended (Level, LogFormat) import Util.Options hiding (endpoint) import Util.Options.Common +import Wire.API.Conversation.Protocol import Wire.API.Routes.Version import Wire.API.Team.Member @@ -135,13 +138,15 @@ data Settings = Settings -- - wire.com -- - example.com _federationDomain :: !Domain, + _federationProtocols :: !(Maybe [ProtocolTag]), _mlsPrivateKeyPaths :: !(Maybe MLSPrivateKeyPaths), -- | FUTUREWORK: 'setFeatureFlags' should be renamed to 'setFeatureConfigs' in all types. _featureFlags :: !FeatureFlags, _disabledAPIVersions :: !(Set VersionExp), -- | The lifetime of a conversation guest link in seconds with the maximum of 1 year (31536000 seconds). -- If not set use the default `defGuestLinkTTLSeconds` - _guestLinkTTLSeconds :: !(Maybe GuestLinkTTLSeconds) + _guestLinkTTLSeconds :: !(Maybe GuestLinkTTLSeconds), + _passwordHashingOptions :: !(PasswordHashingOptions) } deriving (Show, Generic) @@ -182,7 +187,7 @@ data Opts = Opts -- | Federator endpoint _federator :: !(Maybe Endpoint), -- | RabbitMQ settings, required when federation is enabled. - _rabbitmq :: !(Maybe RabbitMqOpts), + _rabbitmq :: !(Maybe AmqpEndpoint), -- | Disco URL _discoUrl :: !(Maybe Text), -- | Other settings diff --git a/services/galley/src/Galley/Run.hs b/services/galley/src/Galley/Run.hs index 7d78fdcdbdf..acd1bba5381 100644 --- a/services/galley/src/Galley/Run.hs +++ b/services/galley/src/Galley/Run.hs @@ -14,7 +14,6 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . - module Galley.Run ( run, mkApp, @@ -56,6 +55,8 @@ import Network.Wai.Middleware.Gzip qualified as GZip import Network.Wai.Utilities.Error import Network.Wai.Utilities.Request import Network.Wai.Utilities.Server +import OpenTelemetry.Instrumentation.Wai qualified as Otel +import OpenTelemetry.Trace as Otel import Prometheus qualified as Prom import Servant hiding (route) import System.Logger qualified as Log @@ -65,16 +66,18 @@ import Wire.API.Routes.API import Wire.API.Routes.Public.Galley import Wire.API.Routes.Version import Wire.API.Routes.Version.Wai +import Wire.OpenTelemetry (withTracerC) run :: Opts -> IO () -run opts = lowerCodensity $ do +run opts = lowerCodensity do + tracer <- withTracerC (app, env) <- mkApp opts settings' <- lift $ newSettings $ defaultServer - (unpack $ opts ^. galley . host) - (portNumber $ fromIntegral $ opts ^. galley . port) + (unpack $ opts._galley.host) + (portNumber $ fromIntegral opts._galley.port) (env ^. App.applog) forM_ (env ^. aEnv) $ \aws -> @@ -82,25 +85,28 @@ run opts = lowerCodensity $ do void $ Codensity $ Async.withAsync $ runApp env deleteLoop void $ Codensity $ Async.withAsync $ runApp env refreshMetrics - lift $ finally (runSettingsWithShutdown settings' app Nothing) (closeApp env) + lift $ inSpan tracer "galley" defaultSpanArguments {kind = Otel.Server} (runSettingsWithShutdown settings' app Nothing) `finally` closeApp env mkApp :: Opts -> Codensity IO (Application, Env) mkApp opts = do logger <- lift $ mkLogger (opts ^. logLevel) (opts ^. logNetStrings) (opts ^. logFormat) env <- lift $ App.createEnv opts logger + otelMiddleware <- lift Otel.newOpenTelemetryWaiMiddleware lift $ runClient (env ^. cstate) $ versionCheck schemaVersion let middlewares = versionMiddleware (foldMap expandVersionExp (opts ^. settings . disabledAPIVersions)) . requestIdMiddleware logger defaultRequestIdHeaderName . servantPrometheusMiddleware (Proxy @CombinedAPI) + . otelMiddleware . GZip.gunzip . GZip.gzip GZip.def . catchErrors logger defaultRequestIdHeaderName - Codensity $ \k -> finally (k ()) $ do - Log.info logger $ Log.msg @Text "Galley application finished." - Log.flush logger - Log.close logger + Codensity \k -> + k () `finally` do + Log.info logger $ Log.msg @Text "Galley application finished." + Log.flush logger + Log.close logger pure (middlewares $ servantApp env, env) where -- Used as a last case in the servant tree. Previously, there used to be a diff --git a/services/galley/src/Galley/Schema/Run.hs b/services/galley/src/Galley/Schema/Run.hs index 5039676a3fa..45417b188f8 100644 --- a/services/galley/src/Galley/Schema/Run.hs +++ b/services/galley/src/Galley/Schema/Run.hs @@ -93,6 +93,7 @@ import Galley.Schema.V89_MlsLockStatus qualified as V89_MlsLockStatus import Galley.Schema.V90_EnforceFileDownloadLocationConfig qualified as V90_EnforceFileDownloadLocationConfig import Galley.Schema.V91_TeamMemberDeletedLimitedEventFanout qualified as V91_TeamMemberDeletedLimitedEventFanout import Galley.Schema.V92_MlsE2EIdConfig qualified as V92_MlsE2EIdConfig +import Galley.Schema.V93_ConferenceCallingSftForOneToOne qualified as V93_ConferenceCallingSftForOneToOne import Imports import Options.Applicative import System.Logger.Extended qualified as Log @@ -186,7 +187,8 @@ migrations = V89_MlsLockStatus.migration, V90_EnforceFileDownloadLocationConfig.migration, V91_TeamMemberDeletedLimitedEventFanout.migration, - V92_MlsE2EIdConfig.migration + V92_MlsE2EIdConfig.migration, + V93_ConferenceCallingSftForOneToOne.migration -- FUTUREWORK: once #1726 has made its way to master/production, -- the 'message' field in connections table can be dropped. -- See also https://github.com/wireapp/wire-server/pull/1747/files diff --git a/services/galley/src/Galley/Schema/V93_ConferenceCallingSftForOneToOne.hs b/services/galley/src/Galley/Schema/V93_ConferenceCallingSftForOneToOne.hs new file mode 100644 index 00000000000..f93f63d70f9 --- /dev/null +++ b/services/galley/src/Galley/Schema/V93_ConferenceCallingSftForOneToOne.hs @@ -0,0 +1,16 @@ +module Galley.Schema.V93_ConferenceCallingSftForOneToOne where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 93 "Add conference_calling_one_to_one and status to team_features" $ + -- the existing field `conference_calling` is now repurposed to represent the lock status + schema' + [r| ALTER TABLE team_features ADD ( + conference_calling_one_to_one int, + conference_calling_status int + ) + |] diff --git a/services/galley/src/Galley/Types/Clients.hs b/services/galley/src/Galley/Types/Clients.hs index 4992b1aaaf3..8c4d925f3cd 100644 --- a/services/galley/src/Galley/Types/Clients.hs +++ b/services/galley/src/Galley/Types/Clients.hs @@ -19,21 +19,12 @@ module Galley.Types.Clients ( Clients, - userIds, clientIds, toList, fromList, fromUserClients, toMap, - fromMap, - singleton, - insert, - diff, - filter, contains, - Galley.Types.Clients.null, - Galley.Types.Clients.nil, - rmClient, ) where @@ -54,15 +45,6 @@ instance Bounds Clients where let n = Map.size ((userClients . clients) c) in n >= fromIntegral x && n <= fromIntegral y -null :: Clients -> Bool -null = Map.null . (userClients . clients) - -nil :: Clients -nil = Clients $ UserClients Map.empty - -userIds :: Clients -> [UserId] -userIds = Map.keys . (userClients . clients) - clientIds :: UserId -> Clients -> [ClientId] clientIds u c = Set.toList $ fromMaybe Set.empty (Map.lookup u ((userClients . clients) c)) @@ -79,44 +61,9 @@ fromList = Clients . UserClients . foldr fn Map.empty fromUserClients :: UserClients -> Clients fromUserClients = Clients -fromMap :: Map UserId (Set ClientId) -> Clients -fromMap = Clients . UserClients - toMap :: Clients -> Map UserId (Set ClientId) toMap = userClients . clients -singleton :: UserId -> [ClientId] -> Clients -singleton u c = - Clients . UserClients $ Map.singleton u (Set.fromList c) - -filter :: (UserId -> Bool) -> Clients -> Clients -filter p = - Clients - . UserClients - . Map.filterWithKey (\u _ -> p u) - . (userClients . clients) - contains :: UserId -> ClientId -> Clients -> Bool contains u c = maybe False (Set.member c) . Map.lookup u . (userClients . clients) - -insert :: UserId -> ClientId -> Clients -> Clients -insert u c = - Clients - . UserClients - . Map.insertWith Set.union u (Set.singleton c) - . (userClients . clients) - -diff :: Clients -> Clients -> Clients -diff (Clients (UserClients ca)) (Clients (UserClients cb)) = - Clients . UserClients $ Map.differenceWith fn ca cb - where - fn a b = - let d = a `Set.difference` b - in if Set.null d then Nothing else Just d - -rmClient :: UserId -> ClientId -> Clients -> Clients -rmClient u c (Clients (UserClients m)) = - Clients . UserClients $ Map.update f u m - where - f x = let s = Set.delete c x in if Set.null s then Nothing else Just s diff --git a/services/galley/src/Galley/Types/UserList.hs b/services/galley/src/Galley/Types/UserList.hs index 071403b5c9d..da00086d6a7 100644 --- a/services/galley/src/Galley/Types/UserList.hs +++ b/services/galley/src/Galley/Types/UserList.hs @@ -34,7 +34,7 @@ data UserList a = UserList { ulLocals :: [a], ulRemotes :: [Remote a] } - deriving (Functor, Foldable, Traversable) + deriving (Show, Functor, Foldable, Traversable) instance Semigroup (UserList a) where UserList locals1 remotes1 <> UserList locals2 remotes2 = diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 2a6c1f3a8bf..fab0d096d8b 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} {-# OPTIONS_GHC -Wno-unused-local-binds #-} @@ -26,7 +27,6 @@ where import API.CustomBackend qualified as CustomBackend import API.Federation qualified as Federation -import API.Federation.Util import API.MLS qualified import API.MessageTimer qualified as MessageTimer import API.Roles qualified as Roles @@ -85,7 +85,6 @@ import Wire.API.Conversation import Wire.API.Conversation qualified as C import Wire.API.Conversation.Action import Wire.API.Conversation.Code hiding (Value) -import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Conversation.Typing import Wire.API.Error.Galley @@ -130,7 +129,6 @@ tests s = test s "metrics" metrics, test s "fetch conversation by qualified ID (v2)" testGetConvQualifiedV2, test s "create Proteus conversation" postProteusConvOk, - test s "create conversation with remote users some unreachable" (postConvWithUnreachableRemoteUsers $ Set.fromList [rb1, rb2, rb3, rb4]), test s "get empty conversations" getConvsOk, test s "get conversations by ids" getConvsOk2, test s "fail to get >500 conversations with v2 API" getConvsFailMaxSizeV2, @@ -176,7 +174,6 @@ tests s = test s "get conversations/:domain/:cnv - local" testGetQualifiedLocalConv, test s "get conversations/:domain/:cnv - local, not found" testGetQualifiedLocalConvNotFound, test s "get conversations/:domain/:cnv - local, not participating" testGetQualifiedLocalConvNotParticipating, - test s "get conversations/:domain/:cnv - remote" testGetQualifiedRemoteConv, test s "get conversations/:domain/:cnv - remote, not found" testGetQualifiedRemoteConvNotFound, test s "get conversations/:domain/:cnv - remote, not found on remote" testGetQualifiedRemoteConvNotFoundOnRemote, test s "post conversations/list/v2" testBulkGetQualifiedConvs, @@ -250,39 +247,6 @@ tests s = test s "send typing indicators with invalid pyaload" postTypingIndicatorsHandlesNonsense ] ] - rb1, rb2, rb3, rb4 :: Remote Backend - rb1 = - toRemoteUnsafe - (Domain "c.example.com") - ( Backend - { bReachable = BackendReachable, - bUsers = 2 - } - ) - rb2 = - toRemoteUnsafe - (Domain "d.example.com") - ( Backend - { bReachable = BackendReachable, - bUsers = 1 - } - ) - rb3 = - toRemoteUnsafe - (Domain "e.example.com") - ( Backend - { bReachable = BackendUnreachable, - bUsers = 2 - } - ) - rb4 = - toRemoteUnsafe - (Domain "f.example.com") - ( Backend - { bReachable = BackendUnreachable, - bUsers = 1 - } - ) getNotFullyConnectedBackendsMock :: Mock LByteString getNotFullyConnectedBackendsMock = "get-not-fully-connected-backends" ~> NonConnectedBackends mempty @@ -357,59 +321,6 @@ postProteusConvOk = do EdConversation c' -> assertConvEquals cnv c' _ -> assertFailure "Unexpected event data" -postConvWithUnreachableRemoteUsers :: Set (Remote Backend) -> TestM () -postConvWithUnreachableRemoteUsers rbs = do - c <- view tsCannon - (alice, _qAlice) <- randomUserTuple - (alex, qAlex) <- randomUserTuple - connectUsers alice (singleton alex) - (allRemotes, participatingRemotes) <- do - v <- forM (toList rbs) $ \rb -> do - users <- connectBackend alice rb - pure (users, participating rb users) - pure $ foldr (\(a, p) acc -> bimap ((<>) a) ((<>) p) acc) ([], []) v - liftIO $ - assertBool "No unreachable backend in the test" (allRemotes /= participatingRemotes) - - let convName = "some chat" - otherLocals = [qAlex] - joiners = allRemotes <> otherLocals - unreachableBackends = - Set.fromList $ - foldMap - ( \rb -> - guard (rbReachable rb == BackendUnreachable) - $> tDomain rb - ) - rbs - WS.bracketR2 c alice alex $ \(wsAlice, wsAlex) -> do - void - $ withTempMockFederator' - ( asum - [ "get-not-fully-connected-backends" ~> NonConnectedBackends mempty, - mockUnreachableFor unreachableBackends, - "on-conversation-created" ~> EmptyResponse, - "on-conversation-updated" ~> EmptyResponse - ] - ) - $ postConvQualified - alice - Nothing - defNewProteusConv - { newConvName = checked convName, - newConvQualifiedUsers = joiners - } - getAllConvs alice - liftIO $ - assertEqual - "Alice does have a group conversation, while she should not!" - [] - groupConvs - WS.assertNoEvent (3 # Second) [wsAlice, wsAlex] - --- This test verifies whether a message actually gets sent all the way to --- cannon. postCryptoMessageVerifyMsgSentAndRejectIfMissingClient :: TestM () postCryptoMessageVerifyMsgSentAndRejectIfMissingClient = do localDomain <- viewFederationDomain @@ -496,7 +407,6 @@ postCryptoMessageVerifyMsgSentAndRejectIfMissingClient = do liftIO $ assertBool "unexpected equal clients" (bc /= bc2) assertNoMsg wsB2 (wsAssertOtr qconv qalice ac bc cipher) --- This test verifies basic mismatch behavior of the the JSON endpoint. postCryptoMessageVerifyRejectMissingClientAndRespondMissingPrekeysJson :: TestM () postCryptoMessageVerifyRejectMissingClientAndRespondMissingPrekeysJson = do (alice, ac) <- randomUserWithClient (head someLastPrekeys) @@ -521,7 +431,6 @@ postCryptoMessageVerifyRejectMissingClientAndRespondMissingPrekeysJson = do Map.keys (userClientMap (getUserClientPrekeyMap p)) @=? [eve] Map.keys <$> Map.lookup eve (userClientMap (getUserClientPrekeyMap p)) @=? Just [ec] --- This test verifies basic mismatch behaviour of the protobuf endpoint. postCryptoMessageVerifyRejectMissingClientAndRespondMissingPrekeysProto :: TestM () postCryptoMessageVerifyRejectMissingClientAndRespondMissingPrekeysProto = do (alice, ac) <- randomUserWithClient (head someLastPrekeys) @@ -563,9 +472,6 @@ postCryptoMessageNotAuthorizeUnknownClient = do postProtoOtrMessage alice (ClientId 0x172618352518396) conv m !!! const 403 === statusCode --- This test verifies the following scenario. --- A client sends a message to all clients of a group and one more who is not part of the group. --- The server must not send this message to client ids not part of the group. postMessageClientNotInGroupDoesNotReceiveMsg :: TestM () postMessageClientNotInGroupDoesNotReceiveMsg = do localDomain <- viewFederationDomain @@ -588,8 +494,6 @@ postMessageClientNotInGroupDoesNotReceiveMsg = do checkEveGetsMsg checkChadDoesNotGetMsg --- This test verifies that when a client sends a message not to all clients of a group then the server should reject the message and sent a notification to the sender (412 Missing clients). --- The test is somewhat redundant because this is already tested as part of other tests already. This is a stand alone test that solely tests the behavior described above. postMessageRejectIfMissingClients :: TestM () postMessageRejectIfMissingClients = do (sender, senderClient) : allReceivers <- randomUserWithClient `traverse` someLastPrekeys @@ -615,8 +519,6 @@ postMessageRejectIfMissingClients = do mkMsg :: ByteString -> (UserId, ClientId) -> (UserId, ClientId, Text) mkMsg text (uid, clientId) = (uid, clientId, toBase64Text text) --- This test verifies behaviour under various values of ignore_missing and --- report_missing. Only tests the JSON endpoint. postCryptoMessageVerifyCorrectResponseIfIgnoreAndReportMissingQueryParam :: TestM () postCryptoMessageVerifyCorrectResponseIfIgnoreAndReportMissingQueryParam = do (alice, ac) <- randomUserWithClient (head someLastPrekeys) @@ -672,9 +574,6 @@ postCryptoMessageVerifyCorrectResponseIfIgnoreAndReportMissingQueryParam = do where listToByteString = BS.intercalate "," . map toByteString' --- Sets up a conversation on Backend A known as "owning backend". One of the --- users from Backend A will send the message but have a missing client. It is --- expected that the message will not be sent. postMessageQualifiedLocalOwningBackendMissingClients :: TestM () postMessageQualifiedLocalOwningBackendMissingClients = do -- Cannon for local users @@ -822,10 +721,6 @@ postMessageQualifiedLocalOwningBackendRedundantAndDeletedClients = do -- Wait less for no message WS.assertNoEvent (1 # Second) [wsNonMember] --- Sets up a conversation on Backend A known as "owning backend". One of the --- users from Backend A will send the message but have a missing client. It is --- expected that the message will be sent except when it is specifically --- requested to report on missing clients of a user. postMessageQualifiedLocalOwningBackendIgnoreMissingClients :: TestM () postMessageQualifiedLocalOwningBackendIgnoreMissingClients = do -- WS receive timeout @@ -1152,7 +1047,7 @@ testGetCodeRejectedIfGuestLinksDisabled = do convId <- createConvWithGuestLink let checkGetCode expectedStatus = getConvCode owner convId !!! const expectedStatus === statusCode let setStatus tfStatus = - TeamFeatures.putTeamFeature @Public.GuestLinksConfig owner teamId (Public.WithStatusNoLock tfStatus Public.GuestLinksConfig Public.FeatureTTLUnlimited) !!! do + TeamFeatures.putTeamFeature @Public.GuestLinksConfig owner teamId (Public.Feature tfStatus Public.GuestLinksConfig) !!! do const 200 === statusCode checkGetCode 200 @@ -1168,7 +1063,7 @@ testPostCodeRejectedIfGuestLinksDisabled = do convId <- decodeConvId <$> postTeamConv teamId owner [] (Just "testConversation") [CodeAccess] (Just noGuestsAccess) Nothing let checkPostCode expectedStatus = postConvCode owner convId !!! statusCode === const expectedStatus let setStatus tfStatus = - TeamFeatures.putTeamFeature @Public.GuestLinksConfig owner teamId (Public.WithStatusNoLock tfStatus Public.GuestLinksConfig Public.FeatureTTLUnlimited) !!! do + TeamFeatures.putTeamFeature @Public.GuestLinksConfig owner teamId (Public.Feature tfStatus Public.GuestLinksConfig) !!! do const 200 === statusCode checkPostCode 201 @@ -1177,6 +1072,7 @@ testPostCodeRejectedIfGuestLinksDisabled = do setStatus Public.FeatureStatusEnabled checkPostCode 200 +-- @SF.Separation @TSFI.RESTfulAPI @S2 -- Check if guests cannot join anymore if guest invite feature was disabled on team level testJoinTeamConvGuestLinksDisabled :: TestM () testJoinTeamConvGuestLinksDisabled = do @@ -1191,7 +1087,16 @@ testJoinTeamConvGuestLinksDisabled = do let checkFeatureStatus fstatus = Util.getTeamFeature @Public.GuestLinksConfig owner teamId !!! do const 200 === statusCode - const (Right (Public.withStatus fstatus Public.LockStatusUnlocked Public.GuestLinksConfig Public.FeatureTTLUnlimited)) === responseJsonEither + const + ( Right + ( Public.LockableFeature + { Public.status = fstatus, + Public.lockStatus = Public.LockStatusUnlocked, + Public.config = Public.GuestLinksConfig + } + ) + ) + === responseJsonEither -- guest can join if guest link feature is enabled checkFeatureStatus Public.FeatureStatusEnabled @@ -1204,7 +1109,7 @@ testJoinTeamConvGuestLinksDisabled = do postJoinCodeConv bob cCode !!! const 200 === statusCode -- disabled guest links feature - let disabled = Public.WithStatusNoLock Public.FeatureStatusDisabled Public.GuestLinksConfig Public.FeatureTTLUnlimited + let disabled = Public.Feature Public.FeatureStatusDisabled Public.GuestLinksConfig TeamFeatures.putTeamFeature @Public.GuestLinksConfig owner teamId disabled !!! do const 200 === statusCode @@ -1223,7 +1128,7 @@ testJoinTeamConvGuestLinksDisabled = do checkFeatureStatus Public.FeatureStatusDisabled -- after re-enabling, the old link is still valid - let enabled = Public.WithStatusNoLock Public.FeatureStatusEnabled Public.GuestLinksConfig Public.FeatureTTLUnlimited + let enabled = Public.Feature Public.FeatureStatusEnabled Public.GuestLinksConfig TeamFeatures.putTeamFeature @Public.GuestLinksConfig owner teamId enabled !!! do const 200 === statusCode getJoinCodeConv eve' (conversationKey cCode) (conversationCode cCode) !!! do @@ -1234,6 +1139,8 @@ testJoinTeamConvGuestLinksDisabled = do postJoinCodeConv bob' cCode !!! const 200 === statusCode checkFeatureStatus Public.FeatureStatusEnabled +-- @END + testJoinNonTeamConvGuestLinksDisabled :: TestM () testJoinNonTeamConvGuestLinksDisabled = do let convName = "testConversation" @@ -1249,7 +1156,7 @@ testJoinNonTeamConvGuestLinksDisabled = do const 200 === statusCode -- for non-team conversations it still works if status is disabled for the team but not server wide - let tfStatus = Public.WithStatusNoLock Public.FeatureStatusDisabled Public.GuestLinksConfig Public.FeatureTTLUnlimited + let tfStatus = Public.Feature Public.FeatureStatusDisabled Public.GuestLinksConfig TeamFeatures.putTeamFeature @Public.GuestLinksConfig owner teamId tfStatus !!! do const 200 === statusCode @@ -1257,6 +1164,7 @@ testJoinNonTeamConvGuestLinksDisabled = do const (Right (ConversationCoverView convId (Just convName) False)) === responseJsonEither const 200 === statusCode +-- @SF.Separation @TSFI.RESTfulAPI @S2 -- This test case covers a negative check that if access code of a guest link is revoked no further -- people can join the group conversation. Additionally it covers: -- Random users can use invite link @@ -1311,6 +1219,8 @@ postJoinCodeConvOk = do putQualifiedAccessUpdate alice qconv noCodeAccess !!! const 200 === statusCode postJoinCodeConv dave payload !!! const 404 === statusCode +-- @END + postJoinCodeConvWithPassword :: TestM () postJoinCodeConvWithPassword = do alice <- randomUser @@ -1486,12 +1396,12 @@ getGuestLinksStatusFromForeignTeamConv = do localDomain <- viewFederationDomain galley <- viewGalley let setTeamStatus u tid tfStatus = - TeamFeatures.putTeamFeature @Public.GuestLinksConfig u tid (Public.WithStatusNoLock tfStatus Public.GuestLinksConfig Public.FeatureTTLUnlimited) !!! do + TeamFeatures.putTeamFeature @Public.GuestLinksConfig u tid (Public.Feature tfStatus Public.GuestLinksConfig) !!! do const 200 === statusCode let checkGuestLinksStatus u c s = getGuestLinkStatus galley u c !!! do const 200 === statusCode - const s === (Public.wsStatus . (responseJsonUnsafe @(Public.WithStatus Public.GuestLinksConfig))) + const s === ((.status) . (responseJsonUnsafe @(Public.LockableFeature Public.GuestLinksConfig))) let checkGetGuestLinksStatus s u c = getGuestLinkStatus galley u c !!! do const s === statusCode @@ -2037,7 +1947,7 @@ postConvO2OFailWithSelf = do g <- viewGalley alice <- randomUser let inv = NewConv [alice] [] Nothing mempty Nothing Nothing Nothing Nothing roleNameWireAdmin BaseProtocolProteusTag - post (g . path "/conversations/one2one" . zUser alice . zConn "conn" . zType "access" . json inv) !!! do + post (g . path "one2one-conversations" . zUser alice . zConn "conn" . zType "access" . json inv) !!! do const 403 === statusCode const (Just "invalid-op") === fmap label . responseJsonUnsafe @@ -2157,10 +2067,19 @@ postRepeatConnectConvCancel = do where cancel u c = do g <- viewGalley - let cnvId = qUnqualified . cnvQualifiedId - put (g . paths ["/i/conversations", toByteString' (cnvId c), "block"] . zUser u) + let qConvId = cnvQualifiedId c + put + ( g + . paths + [ "/i/conversations", + toByteString' (qDomain qConvId), + toByteString' (qUnqualified qConvId), + "block" + ] + . zUser u + ) !!! const 200 === statusCode - getConv u (cnvId c) !!! const 403 === statusCode + getConv u (qUnqualified qConvId) !!! const 403 === statusCode putBlockConvOk :: TestM () putBlockConvOk = do @@ -2172,23 +2091,59 @@ putBlockConvOk = do let convId = qUnqualified qconvId getConvQualified alice qconvId !!! const 200 === statusCode getConvQualified bob qconvId !!! const 403 === statusCode - put (g . paths ["/i/conversations", toByteString' convId, "block"] . zUser bob) + put + ( g + . paths + [ "/i/conversations", + toByteString' (qDomain qconvId), + toByteString' convId, + "block" + ] + . zUser bob + ) !!! const 200 === statusCode -- A is still the only member of the 1-1 getConvQualified alice qconvId !!! do const 200 === statusCode const (cnvMembers conv) === cnvMembers . responseJsonUnsafeWithMsg "conversation" -- B accepts the conversation by unblocking - put (g . paths ["/i/conversations", toByteString' convId, "unblock"] . zUser bob) + put + ( g + . paths + [ "/i/conversations", + toByteString' (qDomain qconvId), + toByteString' convId, + "unblock" + ] + . zUser bob + ) !!! const 200 === statusCode getConvQualified bob qconvId !!! const 200 === statusCode -- B blocks A in the 1-1 - put (g . paths ["/i/conversations", toByteString' convId, "block"] . zUser bob) + put + ( g + . paths + [ "/i/conversations", + toByteString' (qDomain qconvId), + toByteString' convId, + "block" + ] + . zUser bob + ) !!! const 200 === statusCode -- B no longer sees the 1-1 getConvQualified bob qconvId !!! const 403 === statusCode -- B unblocks A in the 1-1 - put (g . paths ["/i/conversations", toByteString' convId, "unblock"] . zUser bob) + put + ( g + . paths + [ "/i/conversations", + toByteString' (qDomain qconvId), + toByteString' convId, + "unblock" + ] + . zUser bob + ) !!! const 200 === statusCode -- B sees the blocked 1-1 again getConvQualified bob qconvId !!! do @@ -2282,42 +2237,6 @@ testGetQualifiedLocalConvNotParticipating = do const 403 === statusCode const (Just "access-denied") === view (at "label") . responseJsonUnsafe @Object -testGetQualifiedRemoteConv :: TestM () -testGetQualifiedRemoteConv = do - aliceQ <- randomQualifiedUser - let aliceId = qUnqualified aliceQ - loc <- flip toLocalUnsafe () <$> viewFederationDomain - bobId <- randomId - convId <- randomId - let remoteDomain = Domain "far-away.example.com" - bobQ = Qualified bobId remoteDomain - remoteConvId = Qualified convId remoteDomain - bobAsOtherMember = OtherMember bobQ Nothing roleNameWireAdmin - aliceAsLocal = - LocalMember aliceId defMemberStatus Nothing roleNameWireAdmin - aliceAsOtherMember = localMemberToOther (qDomain aliceQ) aliceAsLocal - aliceAsSelfMember = localMemberToSelf loc aliceAsLocal - - connectWithRemoteUser aliceId bobQ - registerRemoteConv remoteConvId bobId Nothing (Set.fromList [aliceAsOtherMember]) - - let mockConversation = mkProteusConv convId bobId roleNameWireAdmin [bobAsOtherMember] - remoteConversationResponse = GetConversationsResponse [mockConversation] - expected = - Conversation - remoteConvId - mockConversation.metadata - (ConvMembers aliceAsSelfMember mockConversation.members.others) - ProtocolProteus - - (respAll, _) <- - withTempMockFederator' - (mockReply remoteConversationResponse) - (getConvQualified aliceId remoteConvId) - - conv <- responseJsonUnsafe <$> (pure respAll getRequest asum - [ guard (d == remoteDomainA) *> mockReply (GetConversationsResponse [mockConversationA]), - guard (d == remoteDomainB) *> mockReply (GetConversationsResponse [mockConversationB]), + [ guard (d == remoteDomainA) *> mockReply (GetConversationsResponseV2 [mockConversationA]), + guard (d == remoteDomainB) *> mockReply (GetConversationsResponseV2 [mockConversationB]), guard (d == remoteDomainC) *> liftIO (throw (DiscoveryFailureSrvNotAvailable "domainC")), do r <- getRequest @@ -3105,7 +3024,7 @@ putRemoteConvMemberOk update = do (qUnqualified qbob) roleNameWireMember [localMemberToOther remoteDomain bobAsLocal] - remoteConversationResponse = GetConversationsResponse [mockConversation] + remoteConversationResponse = GetConversationsResponseV2 [mockConversation] (rs, _) <- withTempMockFederator' (mockReply remoteConversationResponse) @@ -3427,7 +3346,7 @@ testOne2OneConversationRequest shouldBeLocal actor desired = do pure . map omQualifiedId . cmOthers . cnvMembers $ conv RemoteActor -> do fedGalleyClient <- view tsFedGalleyClient - GetConversationsResponse convs <- + GetConversationsResponseV2 convs <- runFedClient @"get-conversations" fedGalleyClient (tDomain bob) $ GetConversationsRequest { userId = tUnqualified bob, @@ -3446,7 +3365,7 @@ testOne2OneConversationRequest shouldBeLocal actor desired = do found <- do let rconv = mkProteusConv (qUnqualified convId) (tUnqualified bob) roleNameWireAdmin [] (resp, _) <- - withTempMockFederator' (mockReply (GetConversationsResponse [rconv])) $ + withTempMockFederator' (mockReply (GetConversationsResponseV2 [rconv])) $ getConvQualified (tUnqualified alice) convId pure $ statusCode resp == 200 liftIO $ found @?= ((actor, desired) == (LocalActor, Included)) diff --git a/services/galley/test/integration/API/Federation.hs b/services/galley/test/integration/API/Federation.hs index abd5bfccac8..bb178c6f79b 100644 --- a/services/galley/test/integration/API/Federation.hs +++ b/services/galley/test/integration/API/Federation.hs @@ -137,7 +137,7 @@ getConversationsAllFound = do fedGalleyClient <- view tsFedGalleyClient - GetConversationsResponse convs <- + GetConversationsResponseV2 convs <- runFedClient @"get-conversations" fedGalleyClient (qDomain aliceQ) $ GetConversationsRequest (qUnqualified aliceQ) @@ -159,6 +159,7 @@ getConversationsAllFound = do (Just (sort [bob, qUnqualified carlQ])) (fmap (sort . map (qUnqualified . omQualifiedId) . (.members.others)) c2) +-- @SF.Federation @TSFI.RESTfulAPI @S2 -- -- The test asserts that via a federation client a user cannot fetch -- conversation details of a conversation they are not part of: they get an @@ -182,11 +183,13 @@ getConversationsNotPartOf = do fedGalleyClient <- view tsFedGalleyClient rando <- Id <$> liftIO nextRandom - GetConversationsResponse convs <- + GetConversationsResponseV2 convs <- runFedClient @"get-conversations" fedGalleyClient localDomain $ GetConversationsRequest rando [qUnqualified . cnvQualifiedId $ cnv1] liftIO $ assertEqual "conversation list not empty" [] convs +-- @END + onConvCreated :: TestM () onConvCreated = do c <- view tsCannon @@ -916,27 +919,3 @@ sendMessage = do -- check that alice received the message WS.assertMatch_ (5 # Second) ws $ wsAssertOtr' "" conv bob bobClient aliceClient (toBase64Text "hi alice") - -getConvAction :: Sing tag -> SomeConversationAction -> Maybe (ConversationAction tag) -getConvAction tquery (SomeConversationAction tag action) = - case (tag, tquery) of - (SConversationJoinTag, SConversationJoinTag) -> Just action - (SConversationJoinTag, _) -> Nothing - (SConversationLeaveTag, SConversationLeaveTag) -> Just action - (SConversationLeaveTag, _) -> Nothing - (SConversationMemberUpdateTag, SConversationMemberUpdateTag) -> Just action - (SConversationMemberUpdateTag, _) -> Nothing - (SConversationDeleteTag, SConversationDeleteTag) -> Just action - (SConversationDeleteTag, _) -> Nothing - (SConversationRenameTag, SConversationRenameTag) -> Just action - (SConversationRenameTag, _) -> Nothing - (SConversationMessageTimerUpdateTag, SConversationMessageTimerUpdateTag) -> Just action - (SConversationMessageTimerUpdateTag, _) -> Nothing - (SConversationReceiptModeUpdateTag, SConversationReceiptModeUpdateTag) -> Just action - (SConversationReceiptModeUpdateTag, _) -> Nothing - (SConversationAccessDataTag, SConversationAccessDataTag) -> Just action - (SConversationAccessDataTag, _) -> Nothing - (SConversationRemoveMembersTag, SConversationRemoveMembersTag) -> Just action - (SConversationRemoveMembersTag, _) -> Nothing - (SConversationUpdateProtocolTag, SConversationUpdateProtocolTag) -> Just action - (SConversationUpdateProtocolTag, _) -> Nothing diff --git a/services/galley/test/integration/API/Federation/Util.hs b/services/galley/test/integration/API/Federation/Util.hs index c4e6a41ea49..0ee214fa84a 100644 --- a/services/galley/test/integration/API/Federation/Util.hs +++ b/services/galley/test/integration/API/Federation/Util.hs @@ -17,24 +17,16 @@ module API.Federation.Util ( mkHandler, - - -- * the remote backend type - BackendReachability (..), - Backend (..), - rbReachable, - participating, ) where import Data.Kind -import Data.Qualified import Data.SOP import Data.String.Conversions import GHC.TypeLits import Imports import Servant import Wire.API.Federation.Domain -import Wire.API.MakesFederatedCall import Wire.API.Routes.Named import Wire.API.VersionInfo @@ -50,9 +42,6 @@ instance (HasTrivialHandler api) => HasTrivialHandler ((path :: Symbol) :> api) instance (HasTrivialHandler api) => HasTrivialHandler (OriginDomainHeader :> api) where trivialHandler name _ = trivialHandler @api name -instance (HasTrivialHandler api) => HasTrivialHandler (MakesFederatedCall comp name :> api) where - trivialHandler name _ = trivialHandler @api name - instance (HasTrivialHandler api) => HasTrivialHandler (ReqBody cs a :> api) where trivialHandler name _ = trivialHandler @api name @@ -111,24 +100,3 @@ instance PartialAPI (Named (name :: Symbol) endpoint :<|> api) (Named name h) where mkHandler h = h :<|> mkHandler @api EmptyAPI - --------------------------------------------------------------------------------- --- The remote backend type - -data BackendReachability = BackendReachable | BackendUnreachable - deriving (Eq, Ord) - -data Backend = Backend - { bReachable :: BackendReachability, - bUsers :: Nat - } - deriving (Eq, Ord) - -rbReachable :: Remote Backend -> BackendReachability -rbReachable = bReachable . tUnqualified - -participating :: Remote Backend -> [a] -> [a] -participating rb users = - if rbReachable rb == BackendReachable - then users - else [] diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index dcb01c32c56..a8a5e74d4d4 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -252,6 +252,9 @@ postMLSConvOk = do qcid <- assertConv rsp RegularConv (Just alice) qalice [] (Just nameMaxSize) Nothing checkConvCreateEvent (qUnqualified qcid) wsA +-- @SF.Separation @TSFI.RESTfulAPI @S2 +-- +-- This test verifies that a user must be a member of an MLS conversation in order to send messages to it. testSenderNotInConversation :: TestM () testSenderNotInConversation = do -- create users @@ -279,6 +282,8 @@ testSenderNotInConversation = do liftIO $ Wai.label err @?= "no-conversation" +-- @END + testAddUserWithBundle :: TestM () testAddUserWithBundle = do [alice, bob] <- createAndConnectUsers [Nothing, Nothing] @@ -665,6 +670,10 @@ testLocalToRemoteNonMember = do const (Just "no-conversation-member") === fmap Wai.label . responseJsonError +-- @SF.Separation @TSFI.RESTfulAPI @S2 +-- +-- This test verifies that only the members of an MLS conversation are allowed +-- to join via external commit. testExternalCommitNotMember :: TestM () testExternalCommitNotMember = do [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) @@ -683,6 +692,8 @@ testExternalCommitNotMember = do localPostCommitBundle (mpSender mp) bundle !!! const 404 === statusCode +-- @END + testExternalCommitSameClient :: TestM () testExternalCommitSameClient = do [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) diff --git a/services/galley/test/integration/API/MLS/Mocks.hs b/services/galley/test/integration/API/MLS/Mocks.hs index 49165e64bc7..58c988cb390 100644 --- a/services/galley/test/integration/API/MLS/Mocks.hs +++ b/services/galley/test/integration/API/MLS/Mocks.hs @@ -19,13 +19,11 @@ module API.MLS.Mocks ( receiveCommitMock, receiveCommitMockByDomain, messageSentMock, - messageSentMockByDomain, welcomeMock, welcomeMockByDomain, sendMessageMock, claimKeyPackagesMock, queryGroupStateMock, - deleteMLSConvMock, ) where @@ -65,12 +63,6 @@ receiveCommitMockByDomain clients = do messageSentMock :: Mock LByteString messageSentMock = "on-mls-message-sent" ~> RemoteMLSMessageOk -messageSentMockByDomain :: [Domain] -> Mock LByteString -messageSentMockByDomain reachables = do - domain <- frTargetDomain <$> getRequest - guard (domain `elem` reachables) - messageSentMock - welcomeMock :: Mock LByteString welcomeMock = "mls-welcome" ~> MLSWelcomeSent @@ -97,9 +89,3 @@ queryGroupStateMock gs qusr = do if uid == qUnqualified qusr then GetGroupInfoResponseState (Base64ByteString gs) else GetGroupInfoResponseError ConvNotFound - -deleteMLSConvMock :: Mock LByteString -deleteMLSConvMock = - asum - [ "on-conversation-updated" ~> EmptyResponse - ] diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index 978f7ab4d14..fd0f371a2d2 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -757,43 +757,6 @@ readWelcome fp = runMaybeT $ do guard $ fileSize stat > 0 liftIO $ BS.readFile fp -createRemoveCommit :: (HasCallStack) => ClientIdentity -> [ClientIdentity] -> MLSTest MessagePackage -createRemoveCommit cid targets = do - bd <- State.gets mlsBaseDir - welcomeFile <- liftIO $ emptyTempFile bd "welcome" - pgsFile <- liftIO $ emptyTempFile bd "pgs" - - g <- getClientGroupState cid - - let groupStateMap = Map.fromList (readGroupState g) - let indices = map (fromMaybe (error "could not find target") . flip Map.lookup groupStateMap) targets - commit <- - mlscli - cid - ( [ "member", - "remove", - "--group", - "", - "--group-out", - "", - "--welcome-out", - welcomeFile, - "--group-info-out", - pgsFile - ] - <> map show indices - ) - Nothing - welcome <- liftIO $ readWelcome welcomeFile - pgs <- liftIO $ BS.readFile pgsFile - pure - MessagePackage - { mpSender = cid, - mpMessage = commit, - mpWelcome = welcome, - mpGroupInfo = Just pgs - } - createExternalAddProposal :: (HasCallStack) => ClientIdentity -> MLSTest MessagePackage createExternalAddProposal joiner = do groupId <- diff --git a/services/galley/test/integration/API/SQS.hs b/services/galley/test/integration/API/SQS.hs index 2057433b150..ccf45732c90 100644 --- a/services/galley/test/integration/API/SQS.hs +++ b/services/galley/test/integration/API/SQS.hs @@ -111,9 +111,6 @@ tUpdate expectedCount uids l (Just e) = liftIO $ do (Set.fromList $ billingUserIds) tUpdate _ _ l Nothing = liftIO $ assertFailure $ l <> ": Expected 1 TeamUpdate, got nothing" -updateMatcher :: TeamId -> TeamEvent -> Bool -updateMatcher tid e = e ^. eventType == E.TeamEvent'TEAM_UPDATE && decodeIdFromBS (e ^. teamId) == tid - assertTeamUpdate :: (HasCallStack) => String -> TeamId -> Int32 -> [UserId] -> TestM () assertTeamUpdate l tid c uids = assertIfWatcher l (\e -> e ^. eventType == E.TeamEvent'TEAM_UPDATE && decodeIdFromBS (e ^. teamId) == tid) $ tUpdate c uids diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index cad9536576d..cc49154eddb 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -31,13 +31,11 @@ import API.Util qualified as Util import API.Util.TeamFeature qualified as Util import Bilge hiding (head, timeout) import Bilge.Assert -import Control.Arrow ((>>>)) import Control.Lens hiding ((#), (.=)) import Control.Monad.Catch import Data.Aeson hiding (json) import Data.ByteString.Conversion import Data.Code qualified as Code -import Data.Csv (FromNamedRecord (..), decodeByName) import Data.Currency qualified as Currency import Data.Default import Data.Id @@ -46,8 +44,7 @@ import Data.LegalHold qualified as LH import Data.List.NonEmpty (NonEmpty ((:|))) import Data.List1 hiding (head) import Data.List1 qualified as List1 -import Data.Map qualified as Map -import Data.Misc (HttpsUrl, PlainTextPassword6, mkHttpsUrl, plainTextPassword6) +import Data.Misc import Data.Qualified import Data.Range import Data.Set qualified as Set @@ -56,8 +53,6 @@ import Data.Text.Ascii (AsciiChars (validate)) import Data.UUID qualified as UUID import Data.UUID.Util qualified as UUID import Data.UUID.V1 qualified as UUID -import Data.Vector qualified as V -import GHC.TypeLits (KnownSymbol) import Galley.Env qualified as Galley import Galley.Options (featureFlags, maxConvSize, maxFanoutSize, settings) import Galley.Types.Conversations.Roles @@ -66,7 +61,6 @@ import Imports import Network.HTTP.Types.Status (status403) import Network.Wai.Utilities.Error qualified as Error import Network.Wai.Utilities.Error qualified as Wai -import SAML2.WebSSO.Types qualified as SAML import Test.Tasty import Test.Tasty.Cannon (TimeoutUnit (..), (#)) import Test.Tasty.Cannon qualified as WS @@ -83,19 +77,15 @@ import Wire.API.Internal.Notification hiding (target) import Wire.API.Routes.Internal.Galley.TeamsIntra as TeamsIntra import Wire.API.Routes.Version import Wire.API.Team -import Wire.API.Team.Export (TeamExportUser (..)) -import Wire.API.Team.Feature qualified as Public +import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.API.Team.Member qualified as Member -import Wire.API.Team.Member qualified as TM import Wire.API.Team.Member qualified as Teams import Wire.API.Team.Permission as P import Wire.API.Team.Role import Wire.API.Team.SearchVisibility import Wire.API.User qualified as Public import Wire.API.User qualified as U -import Wire.API.User.Client qualified as C -import Wire.API.User.Client.Prekey qualified as PC tests :: IO TestSetup -> TestTree tests s = @@ -105,11 +95,6 @@ tests s = test s "create binding team with currency" testCreateBindingTeamWithCurrency, testGroup "List Team Members" $ [ test s "a member should be able to list their team" testListTeamMembersDefaultLimit, - let numMembers = 5 - in test - s - ("admins should be able to get a csv stream with their team (" <> show numMembers <> " members)") - (testListTeamMembersCsv numMembers), test s "the list should be limited to the number requested (hard truncation is not tested here)" testListTeamMembersTruncated, test s "pagination" testListTeamMembersPagination ], @@ -233,79 +218,6 @@ testListTeamMembersDefaultLimit = do "member list indicates that there are no more members" (listFromServer ^. teamMemberListType == ListComplete) --- | for ad-hoc load-testing, set @numMembers@ to, say, 10k and see what --- happens. but please don't give that number to our ci! :) --- for additional tests of the CSV download particularly with SCIM users, please refer to 'Test.Spar.Scim.UserSpec' -testListTeamMembersCsv :: (HasCallStack) => Int -> TestM () -testListTeamMembersCsv numMembers = do - let teamSize = numMembers + 1 - - (owner, tid, mbs) <- Util.createBindingTeamWithNMembersWithHandles True numMembers - let numClientMappings = Map.fromList $ (owner : mbs) `zip` (cycle [1, 2, 3] :: [Int]) - addClients numClientMappings - resp <- Util.getTeamMembersCsv owner tid - let rbody = fromMaybe (error "no body") . responseBody $ resp - usersInCsv <- either (error "could not decode csv") pure (decodeCSV @TeamExportUser rbody) - liftIO $ do - assertEqual "total number of team members" teamSize (length usersInCsv) - assertEqual "owners in team" 1 (countOn tExportRole (Just RoleOwner) usersInCsv) - assertEqual "members in team" numMembers (countOn tExportRole (Just RoleMember) usersInCsv) - - do - let someUsersInCsv = take 50 usersInCsv - someHandles = tExportHandle <$> someUsersInCsv - users <- Util.getUsersByHandle (catMaybes someHandles) - mbrs <- view teamMembers <$> Util.bulkGetTeamMembers owner tid (U.userId <$> users) - - let check :: (Eq a) => String -> (TeamExportUser -> Maybe a) -> UserId -> Maybe a -> IO () - check msg getTeamExportUserAttr uid userAttr = do - assertBool msg (isJust userAttr) - assertEqual (msg <> ": " <> show uid) 1 (countOn getTeamExportUserAttr userAttr usersInCsv) - - liftIO . forM_ (zip users mbrs) $ \(user, mbr) -> do - assertEqual "user/member id match" (U.userId user) (mbr ^. TM.userId) - check "tExportDisplayName" (Just . tExportDisplayName) (U.userId user) (Just $ U.userDisplayName user) - check "tExportEmail" tExportEmail (U.userId user) (U.userEmail user) - - liftIO . forM_ (zip3 someUsersInCsv users mbrs) $ \(export, user, mbr) -> do - -- FUTUREWORK: there are a lot of cases we don't cover here (manual invitation, saml, other roles, ...). - assertEqual ("tExportDisplayName: " <> show (U.userId user)) (U.userDisplayName user) (tExportDisplayName export) - assertEqual ("tExportHandle: " <> show (U.userId user)) (U.userHandle user) (tExportHandle export) - assertEqual ("tExportEmail: " <> show (U.userId user)) (U.userEmail user) (tExportEmail export) - assertEqual ("tExportRole: " <> show (U.userId user)) (permissionsRole $ view permissions mbr) (tExportRole export) - assertEqual ("tExportCreatedOn: " <> show (U.userId user)) (snd <$> view invitation mbr) (tExportCreatedOn export) - assertEqual ("tExportInvitedBy: " <> show (U.userId user)) Nothing (tExportInvitedBy export) - assertEqual ("tExportIdpIssuer: " <> show (U.userId user)) (userToIdPIssuer user) (tExportIdpIssuer export) - assertEqual ("tExportManagedBy: " <> show (U.userId user)) (U.userManagedBy user) (tExportManagedBy export) - assertEqual ("tExportUserId: " <> show (U.userId user)) (U.userId user) (tExportUserId export) - assertEqual "tExportNumDevices: " (Map.findWithDefault (-1) (U.userId user) numClientMappings) (tExportNumDevices export) - where - userToIdPIssuer :: (HasCallStack) => U.User -> Maybe HttpsUrl - userToIdPIssuer usr = case (U.userIdentity >=> U.ssoIdentity) usr of - Just (U.UserSSOId (SAML.UserRef (SAML.Issuer issuer) _)) -> either (const $ error "shouldn't happen") Just $ mkHttpsUrl issuer - Just _ -> Nothing - Nothing -> Nothing - - decodeCSV :: (FromNamedRecord a) => LByteString -> Either String [a] - decodeCSV bstr = decodeByName bstr <&> (snd >>> V.toList) - - countOn :: (Eq b) => (a -> b) -> b -> [a] -> Int - countOn prop val xs = sum $ fmap (bool 0 1 . (== val) . prop) xs - - addClients :: Map.Map UserId Int -> TestM () - addClients xs = forM_ (Map.toList xs) addClientForUser - - addClientForUser :: (UserId, Int) -> TestM () - addClientForUser (uid, n) = forM_ [0 .. (n - 1)] (addClient uid) - - addClient :: UserId -> Int -> TestM () - addClient uid i = do - brig <- viewBrig - post (brig . paths ["i", "clients", toByteString' uid] . contentJson . json (newClient (someLastPrekeys !! i)) . queryItem "skip_reauth" "true") !!! const 201 === statusCode - - newClient :: PC.LastPrekey -> C.NewClient - newClient lpk = C.newClient C.PermanentClientType lpk - testListTeamMembersPagination :: TestM () testListTeamMembersPagination = do (owner, tid, _) <- Util.createBindingTeamWithNMembers 18 @@ -396,11 +308,10 @@ testEnableSSOPerTeam = do owner <- Util.randomUser tid <- Util.createBindingTeamInternal "foo" owner assertTeamActivate "create team" tid - let check :: (HasCallStack) => String -> Public.FeatureStatus -> TestM () + let check :: (HasCallStack) => String -> FeatureStatus -> TestM () check msg enabledness = do - status :: Public.WithStatusNoLock Public.SSOConfig <- responseJsonUnsafe <$> (getSSOEnabledInternal tid (getSSOEnabledInternal tid TestM () putSSOEnabledInternalCheckNotImplemented = do g <- viewGalley @@ -409,28 +320,27 @@ testEnableSSOPerTeam = do <$> put ( g . paths ["i", "teams", toByteString' tid, "features", "sso"] - . json (Public.WithStatusNoLock Public.FeatureStatusDisabled Public.SSOConfig Public.FeatureTTLUnlimited) + . json (Feature FeatureStatusDisabled SSOConfig) ) liftIO $ do assertEqual "bad status" status403 (Wai.code waierr) assertEqual "bad label" "not-implemented" (Wai.label waierr) - featureSSO <- view (tsGConf . settings . featureFlags . flagSSO) + featureSSO <- view (tsGConf . settings . featureFlags . to npProject) case featureSSO of - FeatureSSOEnabledByDefault -> check "Teams should start with SSO enabled" Public.FeatureStatusEnabled - FeatureSSODisabledByDefault -> check "Teams should start with SSO disabled" Public.FeatureStatusDisabled - putSSOEnabledInternal tid Public.FeatureStatusEnabled - check "Calling 'putEnabled True' should enable SSO" Public.FeatureStatusEnabled + FeatureSSOEnabledByDefault -> check "Teams should start with SSO enabled" FeatureStatusEnabled + FeatureSSODisabledByDefault -> check "Teams should start with SSO disabled" FeatureStatusDisabled + putSSOEnabledInternal tid FeatureStatusEnabled + check "Calling 'putEnabled True' should enable SSO" FeatureStatusEnabled putSSOEnabledInternalCheckNotImplemented testEnableTeamSearchVisibilityPerTeam :: TestM () testEnableTeamSearchVisibilityPerTeam = do (tid, owner, member : _) <- Util.createBindingTeamWithMembers 2 - let check :: String -> Public.FeatureStatus -> TestM () + let check :: String -> FeatureStatus -> TestM () check msg enabledness = do - status :: Public.WithStatusNoLock Public.SearchVisibilityAvailableConfig <- responseJsonUnsafe <$> (Util.getTeamFeatureInternal @Public.SearchVisibilityAvailableConfig tid (Util.getTeamFeatureInternal @SearchVisibilityAvailableConfig tid TeamId -> Public.LockStatus -> TestM () +-- @END + +setFeatureLockStatus :: forall cfg. (IsFeatureConfig cfg) => TeamId -> LockStatus -> TestM () setFeatureLockStatus tid status = do g <- viewGalley - put (g . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg, toByteString' status]) !!! const 200 === statusCode + put (g . paths ["i", "teams", toByteString' tid, "features", featureNameBS @cfg, toByteString' status]) !!! const 200 === statusCode generateVerificationCode :: Public.SendVerificationCode -> TestM () generateVerificationCode req = do @@ -1122,11 +1041,11 @@ generateVerificationCode req = do let js = RequestBodyLBS $ encode req post (brig . paths ["verification-code", "send"] . contentJson . body js) !!! const 200 === statusCode -setTeamSndFactorPasswordChallenge :: TeamId -> Public.FeatureStatus -> TestM () +setTeamSndFactorPasswordChallenge :: TeamId -> FeatureStatus -> TestM () setTeamSndFactorPasswordChallenge tid status = do g <- viewGalley - let js = RequestBodyLBS $ encode $ Public.WithStatusNoLock status Public.SndFactorPasswordChallengeConfig Public.FeatureTTLUnlimited - put (g . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @Public.SndFactorPasswordChallengeConfig] . contentJson . body js) !!! const 200 === statusCode + let js = RequestBodyLBS $ encode $ Feature status SndFactorPasswordChallengeConfig + put (g . paths ["i", "teams", toByteString' tid, "features", featureNameBS @SndFactorPasswordChallengeConfig] . contentJson . body js) !!! const 200 === statusCode getVerificationCode :: UserId -> Public.VerificationAction -> TestM Code.Value getVerificationCode uid action = do @@ -1388,6 +1307,7 @@ testBillingInLargeTeam = do assertTeamUpdate ("delete fanoutLimit + 3rd billing member: " <> show ownerFanoutPlusThree) team (fanoutLimit + 2) (allOwnersBeforeFanoutLimit <> [ownerFanoutPlusTwo]) refreshIndex +-- | @SF.Management @TSFI.RESTfulAPI @S2 -- This test covers: -- Promotion, demotion of team roles. -- Demotion by superior roles is allowed. @@ -1454,6 +1374,8 @@ testUpdateTeamMember = do e ^. eventTeam @?= tid e ^. eventData @?= EdMemberUpdate uid mPerm +-- @END + testUpdateTeamStatus :: TestM () testUpdateTeamStatus = do g <- viewGalley @@ -1729,11 +1651,11 @@ newTeamMember' perms uid = Member.mkTeamMember uid perms Nothing LH.defUserLegal -- and with different kinds of internal checks, it's quite tedious to do so. getSSOEnabledInternal :: (HasCallStack) => TeamId -> TestM ResponseLBS -getSSOEnabledInternal = Util.getTeamFeatureInternal @Public.SSOConfig +getSSOEnabledInternal = Util.getTeamFeatureInternal @SSOConfig -putSSOEnabledInternal :: (HasCallStack) => TeamId -> Public.FeatureStatus -> TestM () +putSSOEnabledInternal :: (HasCallStack) => TeamId -> FeatureStatus -> TestM () putSSOEnabledInternal tid statusValue = - void $ Util.putTeamFeatureInternal @Public.SSOConfig expect2xx tid (Public.WithStatusNoLock statusValue Public.SSOConfig Public.FeatureTTLUnlimited) + void $ Util.putTeamFeatureInternal @SSOConfig expect2xx tid (Feature statusValue SSOConfig) getSearchVisibility :: (HasCallStack) => (Request -> Request) -> UserId -> TeamId -> (MonadHttp m) => m ResponseLBS getSearchVisibility g uid tid = do diff --git a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs index 0ed8319d99e..507cfaacdcf 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs @@ -212,7 +212,7 @@ testApproveLegalHoldDevice = do WS.bracketRN cannon [owner, member, member, member2, outsideContact, stranger] $ \[ows, mws, mws', member2Ws, outsideContactWs, strangerWs] -> withDummyTestServiceForTeam' owner tid $ \_ chan -> do requestLegalHoldDevice owner member tid !!! testResponse 201 Nothing - liftIO . assertMatchJSON chan $ \(RequestNewLegalHoldClient userId' teamId') -> do + liftIO . assertMatchJSON chan $ \(RequestNewLegalHoldClientV0 userId' teamId') -> do assertEqual "userId == member" userId' member assertEqual "teamId == tid" teamId' tid -- Only the user themself can approve adding a LH device @@ -236,7 +236,7 @@ testApproveLegalHoldDevice = do userStatus let pluck = \case Ev.ClientAdded eClient -> do - clientId eClient @?= someClientId + eClient.clientId @?= someClientId clientType eClient @?= LegalHoldClientType clientClass eClient @?= Just LegalHoldClient _ -> assertBool "Unexpected event" False @@ -315,7 +315,7 @@ testDisableLegalHoldForUser = do approveLegalHoldDevice (Just defPassword) member member tid !!! testResponse 200 Nothing assertNotification mws $ \case Ev.ClientAdded client -> do - clientId client @?= someClientId + client.clientId @?= someClientId clientType client @?= LegalHoldClientType clientClass client @?= Just LegalHoldClient _ -> assertBool "Unexpected event" False @@ -502,14 +502,12 @@ testEnablePerTeam = do member <- randomUser addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing do - status :: Public.WithStatusNoLock Public.LegalholdConfig <- responseJsonUnsafe <$> (getEnabled tid (getEnabled tid (getEnabled tid (getEnabled tid do grantConsent tid member requestLegalHoldDevice owner member tid !!! const 201 === statusCode @@ -519,9 +517,8 @@ testEnablePerTeam = do liftIO $ assertEqual "User legal hold status should be enabled" UserLegalHoldEnabled status do putEnabled tid Public.FeatureStatusDisabled -- disable again - status :: Public.WithStatusNoLock Public.LegalholdConfig <- responseJsonUnsafe <$> (getEnabled tid (getEnabled tid (getEnabled tid (getEnabled tid (getEnabled tid (getEnabled tid >> Set.unions >>> Set.toList >>> head - >>> clientId + >>> (.clientId) withDummyTestServiceForTeam' legalholder tid $ \_ _chan -> do grantConsent tid legalholder @@ -662,17 +657,17 @@ testOldClientsBlockDeviceHandshake = do legalholderLHDevice <- doEnableLH legalholder legalholder _legalholder2LHDevice <- doEnableLH legalholder legalholder2 - let caps = Set.singleton Client.ClientSupportsLegalholdImplicitConsent + let caps = Client.ClientCapabilityList $ Set.singleton Client.ClientSupportsLegalholdImplicitConsent legalholderClient <- do clnt <- randomClientWithCaps legalholder (someLastPrekeys !! 1) (Just caps) - ensureClientCaps legalholder clnt (Client.ClientCapabilityList caps) + ensureClientCaps legalholder clnt caps pure clnt legalholder2Client <- do clnt <- randomClient legalholder2 (someLastPrekeys !! 3) -- this another way to do it (instead of providing caps during client creation). ensureClientCaps legalholder2 clnt (Client.ClientCapabilityList mempty) upgradeClientToLH legalholder2 clnt - ensureClientCaps legalholder2 clnt (Client.ClientCapabilityList caps) + ensureClientCaps legalholder2 clnt caps pure clnt grantConsent tid2 peer connectUsers peer (List1.list1 legalholder [legalholder2]) @@ -731,7 +726,7 @@ testClaimKeys testcase = do >>> Set.unions >>> Set.toList >>> head - >>> clientId + >>> (.clientId) let makePeerClient :: TestM () makePeerClient = case testcase of diff --git a/services/galley/test/integration/API/Teams/LegalHold/Util.hs b/services/galley/test/integration/API/Teams/LegalHold/Util.hs index 6fd3eee176b..921ebad99cd 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/Util.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/Util.hs @@ -54,6 +54,7 @@ import Test.Tasty.Runners import TestSetup import Wire.API.Internal.Notification (ntfPayload) import Wire.API.Provider.Service +import Wire.API.Team.Feature import Wire.API.Team.Feature qualified as Public import Wire.API.Team.LegalHold import Wire.API.Team.LegalHold.External @@ -256,7 +257,7 @@ putEnabledM' g extra tid enabled = do put $ g . paths ["i", "teams", toByteString' tid, "features", "legalhold"] - . json (Public.WithStatusNoLock enabled Public.LegalholdConfig Public.FeatureTTLUnlimited) + . json (Public.Feature enabled Public.LegalholdConfig) . extra postSettings :: (HasCallStack) => UserId -> TeamId -> NewLegalHoldService -> TestM ResponseLBS @@ -529,18 +530,6 @@ putLHWhitelistTeam' g tid = do . paths ["i", "legalhold", "whitelisted-teams", toByteString' tid] ) -_deleteLHWhitelistTeam :: (HasCallStack) => TeamId -> TestM ResponseLBS -_deleteLHWhitelistTeam tid = do - galleyCall <- viewGalley - deleteLHWhitelistTeam' galleyCall tid - -deleteLHWhitelistTeam' :: (HasCallStack, MonadHttp m) => GalleyR -> TeamId -> m ResponseLBS -deleteLHWhitelistTeam' g tid = do - delete - ( g - . paths ["i", "legalhold", "whitelisted-teams", toByteString' tid] - ) - errWith :: (HasCallStack, Typeable a, FromJSON a) => Int -> (a -> Bool) -> ResponseLBS -> TestM () errWith wantStatus wantBody rsp = liftIO $ do assertEqual "" wantStatus (statusCode rsp) @@ -559,13 +548,13 @@ testOnlyIfLhWhitelisted :: IO TestSetup -> TestName -> TestM () -> TestTree testOnlyIfLhWhitelisted setupAction name testAction = do singleTest name $ LHTest FeatureLegalHoldWhitelistTeamsAndImplicitConsent setupAction testAction -data LHTest = LHTest FeatureLegalHold (IO TestSetup) (TestM ()) +data LHTest = LHTest (FeatureDefaults LegalholdConfig) (IO TestSetup) (TestM ()) instance IsTest LHTest where run :: OptionSet -> LHTest -> (Progress -> IO ()) -> IO Result run _ (LHTest expectedFlag setupAction testAction) _ = do setup <- setupAction - let featureLegalHold = setup ^. tsGConf . settings . featureFlags . flagLegalHold + let featureLegalHold = setup ^. tsGConf . settings . featureFlags . to npProject if featureLegalHold == expectedFlag then do hunitResult <- try $ void . flip runReaderT setup . runTestM $ testAction diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index c7b157051ae..07909cbbbcd 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -20,38 +20,31 @@ module API.Util where -import API.Federation.Util import API.SQS qualified as SQS import Bilge hiding (timeout) import Bilge.Assert import Bilge.TestSession import Control.Applicative import Control.Concurrent.Async -import Control.Exception (throw) import Control.Lens hiding (from, to, uncons, (#), (.=)) import Control.Monad.Catch (MonadCatch, MonadMask) import Control.Monad.Codensity (lowerCodensity) -import Control.Monad.Except (ExceptT, runExceptT) import Control.Retry (constantDelay, exponentialBackoff, limitRetries, retrying) import Data.Aeson hiding (json) import Data.Aeson qualified as A import Data.Aeson.Lens (key, _String) import Data.ByteString qualified as BS -import Data.ByteString.Base64.URL qualified as B64U import Data.ByteString.Char8 qualified as B8 import Data.ByteString.Char8 qualified as C import Data.ByteString.Conversion import Data.ByteString.Lazy qualified as Lazy -import Data.CaseInsensitive qualified as CI import Data.Code qualified as Code import Data.Currency qualified as Currency import Data.Default import Data.Domain -import Data.Handle qualified as Handle import Data.HashMap.Strict qualified as HashMap import Data.Id import Data.Json.Util hiding ((#)) -import Data.Kind import Data.LegalHold (defUserLegalHoldStatus) import Data.List.NonEmpty (NonEmpty) import Data.List1 as List1 @@ -60,25 +53,21 @@ import Data.Map.Strict qualified as Map import Data.Misc import Data.ProtoLens qualified as Protolens import Data.ProtocolBuffers (encodeMessage) -import Data.Qualified hiding (isLocal) +import Data.Qualified import Data.Range import Data.Serialize (runPut) import Data.Set qualified as Set -import Data.Singletons import Data.String.Conversions import Data.Text qualified as Text import Data.Text.Encoding qualified as T import Data.Text.Encoding qualified as Text -import Data.Text.Lazy.Encoding qualified as LT import Data.Time (getCurrentTime) import Data.Tuple.Extra import Data.UUID qualified as UUID import Data.UUID.V4 import Federator.MockServer import Federator.MockServer qualified as Mock -import GHC.TypeLits (KnownSymbol) import GHC.TypeNats -import Galley.Intra.User (chunkify) import Galley.Options qualified as Opts import Galley.Run qualified as Run import Galley.Types.Conversations.One2One @@ -86,17 +75,12 @@ import Galley.Types.UserList import Imports import Network.HTTP.Client qualified as HTTP import Network.HTTP.Media.MediaType -import Network.HTTP.Types qualified as HTTP import Network.URI (pathSegments) -import Network.Wai (defaultRequest) -import Network.Wai qualified as Wai -import Network.Wai.Test qualified as Wai import Network.Wai.Utilities.MockServer (withMockServer) import Servant import System.Exit import System.Process import System.Random -import Test.QuickCheck qualified as Q import Test.Tasty.Cannon (TimeoutUnit (..), (#)) import Test.Tasty.Cannon qualified as WS import Test.Tasty.HUnit @@ -108,14 +92,12 @@ import Web.Cookie import Wire.API.Connection import Wire.API.Conversation import Wire.API.Conversation qualified as Conv -import Wire.API.Conversation.Action import Wire.API.Conversation.Code hiding (Value) import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Conversation.Typing import Wire.API.Event.Conversation import Wire.API.Event.Conversation qualified as Conv -import Wire.API.Event.Federation qualified as Fed import Wire.API.Event.LeaveReason import Wire.API.Event.Team import Wire.API.Event.Team qualified as TE @@ -123,7 +105,6 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Brig import Wire.API.Federation.API.Common import Wire.API.Federation.API.Galley -import Wire.API.Federation.Domain (originDomainHeaderName) import Wire.API.Internal.Notification hiding (target) import Wire.API.MLS.LeafNode import Wire.API.MLS.Message @@ -134,12 +115,10 @@ import Wire.API.Message import Wire.API.Message.Proto qualified as Proto import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Internal.Galley.ConversationsIntra -import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Multi import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Routes.MultiTablePaging import Wire.API.Routes.Version import Wire.API.Team -import Wire.API.Team.Feature import Wire.API.Team.Invitation import Wire.API.Team.Member hiding (userId) import Wire.API.Team.Member qualified as Team @@ -198,7 +177,7 @@ createBindingTeam = do createBindingTeam' :: (HasCallStack) => TestM (User, TeamId) createBindingTeam' = do - owner <- randomTeamCreator' + owner <- randomTeamCreator teams <- getTeams (User.userId owner) [] team <- assertOne $ view teamListTeams teams let tid = view teamId team @@ -294,7 +273,7 @@ createBindingTeamInternalNoActivate :: (HasCallStack) => Text -> UserId -> TestM createBindingTeamInternalNoActivate name owner = do g <- viewGalley tid <- randomId - let nt = BindingNewTeam $ newNewTeam (unsafeRange name) DefaultIcon + let nt = newNewTeam (unsafeRange name) DefaultIcon _ <- put (g . paths ["/i/teams", toByteString' tid] . zUser owner . zConn "conn" . zType "access" . json nt) TeamId -> TestM TeamData -getTeamInternal tid = do - g <- viewGalley - r <- get (g . paths ["i/teams", toByteString' tid]) UserId -> TeamId -> TestM Team getTeam usr tid = do g <- viewGalley @@ -329,14 +302,6 @@ getTeamMembers usr tid = do r <- get (g . paths ["teams", toByteString' tid, "members"] . zUser usr) UserId -> TeamId -> TestM ResponseLBS -getTeamMembersCsv usr tid = do - g <- viewGalley - get (g . accept "text/csv" . paths ["teams", toByteString' tid, "members/csv"] . zUser usr) UserId -> TeamId -> Int -> TestM TeamMemberList getTeamMembersTruncated usr tid n = do g <- viewGalley @@ -428,14 +393,6 @@ getTeamMemberInternal tid mid = do r <- get (g . paths ["i", "teams", toByteString' tid, "members", toByteString' mid]) UserId -> TeamId -> UserId -> Permissions -> Maybe (UserId, UTCTimeMillis) -> TestM () -addTeamMember usr tid muid mperms mmbinv = do - g <- viewGalley - let payload = json (mkNewTeamMember muid mperms mmbinv) - post (g . paths ["teams", toByteString' tid, "members"] . zUser usr . zConn "conn" . payload) - !!! const 200 - === statusCode - -- | FUTUREWORK: do not use this, it's broken!! use 'addUserToTeam' instead! https://wearezeta.atlassian.net/browse/SQSERVICES-471 addTeamMemberInternal :: (HasCallStack) => TeamId -> UserId -> Permissions -> Maybe (UserId, UTCTimeMillis) -> TestM () addTeamMemberInternal tid muid mperms mmbinv = addTeamMemberInternal' tid muid mperms mmbinv !!! const 200 === statusCode @@ -458,7 +415,7 @@ addUserToTeamWithRole role inviter tid = do (inv, rsp2) <- addUserToTeamWithRole' role inviter tid let invitee :: User = responseJsonUnsafe rsp2 inviteeId = User.userId invitee - let invmeta = Just (inviter, inCreatedAt inv) + let invmeta = Just (inviter, inv.createdAt) mem <- getTeamMember inviter tid inviteeId liftIO $ assertEqual "Member has no/wrong invitation metadata" invmeta (mem ^. Team.invitation) let zuid = parseSetCookie <$> getHeader "Set-Cookie" rsp2 @@ -469,10 +426,10 @@ addUserToTeamWithRole' :: (HasCallStack) => Maybe Role -> UserId -> TeamId -> Te addUserToTeamWithRole' role inviter tid = do brig <- viewBrig inviteeEmail <- randomEmail - let invite = InvitationRequest Nothing role Nothing inviteeEmail Nothing + let invite = InvitationRequest Nothing role Nothing inviteeEmail invResponse <- postInvitation tid inviter invite inv <- responseJsonError invResponse - inviteeCode <- getInvitationCode tid (inInvitation inv) + inviteeCode <- getInvitationCode tid inv.invitationId r <- post ( brig @@ -502,7 +459,7 @@ makeOwner owner mem tid = do !!! const 200 === statusCode -acceptInviteBody :: Email -> InvitationCode -> RequestBody +acceptInviteBody :: EmailAddress -> InvitationCode -> RequestBody acceptInviteBody email code = RequestBodyLBS . encode $ object @@ -694,7 +651,7 @@ createOne2OneTeamConv u1 u2 n tid = do g <- viewGalley let conv = NewConv [u2] [] (n >>= checked) mempty Nothing (Just $ ConvTeamInfo tid) Nothing Nothing roleNameWireAdmin BaseProtocolProteusTag - post $ g . path "/conversations/one2one" . zUser u1 . zConn "conn" . zType "access" . json conv + post $ g . path "/one2one-conversations" . zUser u1 . zConn "conn" . zType "access" . json conv postConv :: UserId -> @@ -731,26 +688,6 @@ postConvQualified u c n = do . zType "access" . json n -postConvWithRemoteUsersGeneric :: - (HasCallStack) => - Mock LByteString -> - UserId -> - Maybe ClientId -> - NewConv -> - TestM (Response (Maybe LByteString)) -postConvWithRemoteUsersGeneric m u c n = do - let mock = - ("get-not-fully-connected-backends" ~> NonConnectedBackends mempty) - <|> m - fmap fst $ - withTempMockFederator' mock $ - postConvQualified u c n {newConvName = setName (newConvName n)} - Maybe (Range n m Text) -> Maybe (Range n m Text) - setName Nothing = checked "federated gossip" - setName x = x - postConvWithRemoteUsers :: (HasCallStack) => UserId -> @@ -821,7 +758,7 @@ postO2OConv :: UserId -> UserId -> Maybe Text -> TestM ResponseLBS postO2OConv u1 u2 n = do g <- viewGalley let conv = NewConv [u2] [] (n >>= checked) mempty Nothing Nothing Nothing Nothing roleNameWireAdmin BaseProtocolProteusTag - post $ g . path "/conversations/one2one" . zUser u1 . zConn "conn" . zType "access" . json conv + post $ g . path "/one2one-conversations" . zUser u1 . zConn "conn" . zType "access" . json conv postConnectConv :: UserId -> UserId -> Text -> Text -> Maybe Text -> TestM ResponseLBS postConnectConv a b name msg email = do @@ -1033,15 +970,6 @@ getConvs u cids = do . zConn "conn" . json (ListConversations (unsafeRange cids)) -getConvClients :: (HasCallStack) => GroupId -> TestM ClientList -getConvClients gid = do - g <- viewGalley - responseJsonError - =<< get - ( g - . paths ["i", "group", B64U.encode $ unGroupId gid] - ) - getAllConvs :: (HasCallStack) => UserId -> TestM [Conversation] getAllConvs u = do g <- viewGalley @@ -1297,7 +1225,8 @@ putOtherMemberQualified from to m c = do putOtherMember :: UserId -> UserId -> OtherMemberUpdate -> ConvId -> TestM ResponseLBS putOtherMember from to m c = do - g <- viewGalley + -- this endpoint was removed in v7 + g <- fmap (addPrefixAtVersion V6 .) (view tsUnversionedGalley) put $ g . paths ["conversations", toByteString' c, "members", toByteString' to] @@ -1409,15 +1338,6 @@ postJoinCodeConv' mPw u j = do -- `json (JoinConversationByCode j Nothing)` and `json j` are equivalent, using the latter to test backwards compatibility . (if isJust mPw then json (JoinConversationByCode j mPw) else json j) -deleteFederation :: - (MonadHttp m, HasGalley m, MonadIO m) => - Domain -> - m ResponseLBS -deleteFederation dom = do - g <- viewGalley - delete $ - g . paths ["/i/federation", toByteString' dom] - putQualifiedAccessUpdate :: (MonadHttp m, HasGalley m, MonadIO m) => UserId -> @@ -1581,15 +1501,6 @@ registerRemoteConv convId originUser name othMembers = do protocol = ProtocolProteus } -getFeatureStatusMulti :: forall cfg. (KnownSymbol (FeatureSymbol cfg)) => Multi.TeamFeatureNoConfigMultiRequest -> TestM ResponseLBS -getFeatureStatusMulti req = do - g <- viewGalley - post - ( g - . paths ["i", "features-multi-teams", featureNameBS @cfg] - . json req - ) - ------------------------------------------------------------------------------- -- Common Assertions @@ -1771,10 +1682,6 @@ assertMLSMessageEvent qcs u message e = do evtFrom e @?= u evtData e @?= EdMLSMessage message --- | This assumes the default role name -wsAssertMemberJoin :: (HasCallStack) => Qualified ConvId -> Qualified UserId -> [Qualified UserId] -> Notification -> IO () -wsAssertMemberJoin conv usr new = wsAssertMemberJoinWithRole conv usr new roleNameWireAdmin - wsAssertMemberJoinWithRole :: (HasCallStack) => Qualified ConvId -> Qualified UserId -> [Qualified UserId] -> RoleName -> Notification -> IO () wsAssertMemberJoinWithRole conv usr new role n = do let e = List1.head (WS.unpackPayload n) @@ -1788,23 +1695,6 @@ assertJoinEvent conv usr new role e = do evtFrom e @?= usr fmap (sort . mMembers) (evtData e ^? _EdMembersJoin) @?= Just (sort (fmap (`SimpleMember` role) new)) -wsAssertFederationDeleted :: - (HasCallStack) => - Domain -> - Notification -> - IO () -wsAssertFederationDeleted dom n = do - ntfTransient n @?= False - assertFederationDeletedEvent dom $ List1.head (WS.unpackPayload n) - -assertFederationDeletedEvent :: - Domain -> - Fed.Event -> - IO () -assertFederationDeletedEvent dom e = do - Fed._eventType e @?= Fed.FederationDelete - Fed._eventDomain e @?= dom - -- FUTUREWORK: See if this one can be implemented in terms of: -- -- checkConvMemberLeaveEvent :: HasCallStack => Qualified ConvId -> Qualified UserId -> WS.WebSocket -> TestM () @@ -1893,29 +1783,6 @@ assertNoMsg ws f = do Left _ -> pure () -- expected Right _ -> assertFailure "Unexpected message" -assertRemoveUpdate :: (MonadIO m, HasCallStack) => FederatedRequest -> Qualified ConvId -> Qualified UserId -> [UserId] -> Qualified UserId -> m () -assertRemoveUpdate req qconvId remover alreadyPresentUsers victim = liftIO $ do - frRPC req @?= "on-conversation-updated" - frOriginDomain req @?= qDomain qconvId - cu <- assertJust $ decode (frBody req) - cuOrigUserId cu @?= remover - cuConvId cu @?= qUnqualified qconvId - sort (cuAlreadyPresentUsers cu) @?= sort alreadyPresentUsers - cuAction cu - @?= SomeConversationAction - (sing @'ConversationRemoveMembersTag) - (ConversationRemoveMembers (pure victim) EdReasonRemoved) - -assertLeaveUpdate :: (MonadIO m, HasCallStack) => FederatedRequest -> Qualified ConvId -> Qualified UserId -> [UserId] -> m () -assertLeaveUpdate req qconvId remover alreadyPresentUsers = liftIO $ do - frRPC req @?= "on-conversation-updated" - frOriginDomain req @?= qDomain qconvId - cu <- assertJust $ decode (frBody req) - cuOrigUserId cu @?= remover - cuConvId cu @?= qUnqualified qconvId - sort (cuAlreadyPresentUsers cu) @?= sort alreadyPresentUsers - cuAction cu @?= SomeConversationAction (sing @'ConversationLeaveTag) () - ------------------------------------------------------------------------------- -- Helpers @@ -1949,33 +1816,18 @@ decodeConvId = qUnqualified . decodeQualifiedConvId decodeQualifiedConvId :: (HasCallStack) => Response (Maybe Lazy.ByteString) -> Qualified ConvId decodeQualifiedConvId = cnvQualifiedId . responseJsonUnsafe -decodeConvList :: (HasCallStack) => Response (Maybe Lazy.ByteString) -> [Conversation] -decodeConvList = convList . responseJsonUnsafeWithMsg "conversations" - decodeConvIdList :: (HasCallStack) => Response (Maybe Lazy.ByteString) -> [ConvId] decodeConvIdList = convList . responseJsonUnsafeWithMsg "conversation-ids" -decodeQualifiedConvIdList :: Response (Maybe Lazy.ByteString) -> Either String [Qualified ConvId] -decodeQualifiedConvIdList = fmap mtpResults . responseJsonEither @ConvIdsPage - zUser :: UserId -> Request -> Request zUser = header "Z-User" . toByteString' -zBot :: UserId -> Request -> Request -zBot = header "Z-Bot" . toByteString' - zClient :: ClientId -> Request -> Request zClient = header "Z-Client" . toByteString' zConn :: ByteString -> Request -> Request zConn = header "Z-Connection" -zProvider :: ProviderId -> Request -> Request -zProvider = header "Z-Provider" . toByteString' - -zConv :: ConvId -> Request -> Request -zConv = header "Z-Conversation" . toByteString' - zType :: ByteString -> Request -> Request zType = header "Z-Type" @@ -2068,16 +1920,6 @@ postConnection from to = do RequestBodyLBS . encode $ ConnectionRequest to (unsafeRange "some conv name") -postConnectionQualified :: UserId -> Qualified UserId -> TestM ResponseLBS -postConnectionQualified from (Qualified toUser toDomain) = do - brig <- viewBrig - post $ - brig - . paths ["connections", toByteString' toDomain, toByteString' toUser] - . contentJson - . zUser from - . zConn "conn" - -- | A copy of 'putConnection' from Brig integration tests. putConnection :: UserId -> UserId -> Relation -> TestM ResponseLBS putConnection from to r = do @@ -2144,11 +1986,8 @@ randomQualifiedUser = randomUser' False True True randomQualifiedId :: (MonadIO m) => Domain -> m (Qualified (Id a)) randomQualifiedId domain = Qualified <$> randomId <*> pure domain -randomTeamCreator :: (HasCallStack) => TestM UserId -randomTeamCreator = qUnqualified <$> randomUser' True True True - -randomTeamCreator' :: (HasCallStack) => TestM User -randomTeamCreator' = randomUser'' True True True +randomTeamCreator :: (HasCallStack) => TestM User +randomTeamCreator = randomUser'' True True True randomUser' :: (HasCallStack) => Bool -> Bool -> Bool -> TestM (Qualified UserId) randomUser' isCreator hasPassword hasEmail = userQualifiedId <$> randomUser'' isCreator hasPassword hasEmail @@ -2165,7 +2004,7 @@ randomUserProfile' isCreator hasPassword hasEmail = do ["name" .= fromEmail e] <> ["password" .= defPassword | hasPassword] <> ["email" .= fromEmail e | hasEmail] - <> ["team" .= BindingNewTeam (newNewTeam (unsafeRange "teamName") DefaultIcon) | isCreator] + <> ["team" .= newNewTeam (unsafeRange "teamName") DefaultIcon | isCreator] responseJsonUnsafe <$> (post (b . path "/i/users" . json p) TestM UserId @@ -2180,7 +2019,7 @@ ephemeralUser = do randomClient :: (HasCallStack) => UserId -> LastPrekey -> TestM ClientId randomClient uid lk = randomClientWithCaps uid lk Nothing -randomClientWithCaps :: (HasCallStack) => UserId -> LastPrekey -> Maybe (Set Client.ClientCapability) -> TestM ClientId +randomClientWithCaps :: (HasCallStack) => UserId -> LastPrekey -> Maybe ClientCapabilityList -> TestM ClientId randomClientWithCaps uid lk caps = do b <- viewBrig resp <- @@ -2263,36 +2102,6 @@ deleteClient u c pw = do [ "password" .= pw ] --- TODO: Refactor, as used also in brig -isUserDeleted :: (HasCallStack) => UserId -> TestM Bool -isUserDeleted u = do - b <- viewBrig - r <- - get (b . paths ["i", "users", toByteString' u, "status"]) - error $ "getStatus: failed to parse response: " ++ show r - Just j -> do - let st = maybeFromJSON =<< j ^? key "status" - let decoded = fromMaybe (error $ "getStatus: failed to decode status" ++ show j) st - pure $ decoded == Deleted - where - maybeFromJSON :: (FromJSON a) => Value -> Maybe a - maybeFromJSON v = case fromJSON v of - Success a -> Just a - _ -> Nothing - -isMember :: UserId -> ConvId -> TestM Bool -isMember usr cnv = do - g <- viewGalley - res <- - get $ - g - . paths ["i", "conversations", toByteString' cnv, "members", toByteString' usr] - . expect2xx - pure $ isJust (responseJsonMaybe @Member res) - randomUserWithClient :: LastPrekey -> TestM (UserId, ClientId) randomUserWithClient lk = do (u, c) <- randomUserWithClientQualified lk @@ -2304,9 +2113,6 @@ randomUserWithClientQualified lk = do c <- randomClient (qUnqualified u) lk pure (u, c) -newNonce :: TestM (Id ()) -newNonce = randomId - fromBS :: (HasCallStack, FromByteString a, MonadIO m) => ByteString -> m a fromBS bs = case fromByteString bs of @@ -2399,16 +2205,13 @@ otrRecipients = . fmap Map.fromList . foldr ((uncurry Map.insert . fmap pure) . (\(a, b, c) -> (a, (b, c)))) mempty -genRandom :: (Q.Arbitrary a, MonadIO m) => m a -genRandom = liftIO . Q.generate $ Q.arbitrary - defPassword :: PlainTextPassword6 defPassword = plainTextPassword6Unsafe "topsecretdefaultpassword" -randomEmail :: (MonadIO m) => m Email +randomEmail :: (MonadIO m) => m EmailAddress randomEmail = do uid <- liftIO nextRandom - pure $ Email ("success+" <> UUID.toText uid) "simulator.amazonses.com" + pure $ unsafeEmailAddress ("success+" <> UUID.toASCIIBytes uid) "simulator.amazonses.com" selfConv :: UserId -> ConvId selfConv u = Id (toUUID u) @@ -2479,9 +2282,9 @@ mkProteusConv :: UserId -> RoleName -> [OtherMember] -> - RemoteConversation + RemoteConversationV2 mkProteusConv cnvId creator selfRole otherMembers = - RemoteConversation + RemoteConversationV2 cnvId ( ConversationMetadata RegularConv @@ -2577,33 +2380,6 @@ deleteTeam owner tid = do !!! do const 202 === statusCode --- (Duplicate of 'Galley.Intra.User.getUsers'.) -getUsersByUid :: [UserId] -> TestM [User] -getUsersByUid = getUsersBy "ids" - -getUsersByHandle :: [Handle.Handle] -> TestM [User] -getUsersByHandle = getUsersBy "handles" - -getUsersBy :: forall uidsOrHandles. (ToByteString uidsOrHandles) => ByteString -> [uidsOrHandles] -> TestM [User] -getUsersBy keyName = chunkify $ \keys -> do - brig <- viewBrig - let users = BS.intercalate "," $ toByteString' <$> keys - res <- - get - ( brig - . path "/i/users" - . queryItem keyName users - . expect2xx - ) - let accounts = fromJust $ responseJsonMaybe @[UserAccount] res - pure $ fmap accountUser accounts - -getUserProfile :: UserId -> UserId -> TestM UserProfile -getUserProfile zusr uid = do - brig <- view tsUnversionedBrig - res <- get (brig . zUser zusr . paths ["v1", "users", toByteString' uid]) - responseJsonError res - upgradeClientToLH :: (HasCallStack) => UserId -> ClientId -> TestM () upgradeClientToLH zusr cid = putCapabilities zusr cid [ClientSupportsLegalholdImplicitConsent] @@ -2616,7 +2392,7 @@ putCapabilities zusr cid caps = do ( brig . zUser zusr . paths ["clients", toByteString' cid] - . json defUpdateClient {updateClientCapabilities = Just (Set.fromList caps)} + . json defUpdateClient {updateClientCapabilities = Just $ ClientCapabilityList $ Set.fromList caps} . expect2xx ) @@ -2654,6 +2430,7 @@ mkProfile quid name = { profileQualifiedId = quid, profileName = name, profilePict = noPict, + profileTextStatus = Nothing, profileAssets = mempty, profileAccentId = defaultAccentId, profileDeleted = False, @@ -2687,51 +2464,11 @@ withTempMockFederator' resp action = do $ \mockPort -> do withSettingsOverrides (\opts -> opts & Opts.federator ?~ Endpoint "127.0.0.1" (fromIntegral mockPort)) action --- Starts a servant Application in Network.Wai.Test session and runs the --- FederatedRequest against it. -makeFedRequestToServant :: - forall (api :: Type). - (HasServer api '[]) => - Domain -> - Server api -> - FederatedRequest -> - IO LByteString -makeFedRequestToServant originDomain server fedRequest = do - sresp <- Wai.runSession session app - let status = Wai.simpleStatus sresp - bdy = Wai.simpleBody sresp - if HTTP.statusIsSuccessful status - then pure bdy - else throw (Mock.MockErrorResponse status (LT.decodeUtf8 bdy)) - where - app :: Application - app = serve (Proxy @api) server - - session :: Wai.Session Wai.SResponse - session = do - Wai.srequest - ( Wai.SRequest - ( defaultRequest - { Wai.requestMethod = HTTP.methodPost, - Wai.pathInfo = [frRPC fedRequest], - Wai.requestHeaders = - [ (CI.mk "Content-Type", "application/json"), - (CI.mk "Accept", "application/json"), - (originDomainHeaderName, cs . domainText $ originDomain) - ] - } - ) - (frBody fedRequest) - ) - assertRight :: (MonadIO m, Show a, HasCallStack) => Either a b -> m b assertRight = \case Left e -> liftIO $ assertFailure $ "Expected Right, got Left: " <> show e Right x -> pure x -assertRightT :: (MonadIO m, Show a, HasCallStack) => ExceptT a m b -> m b -assertRightT = assertRight <=< runExceptT - -- | Run a probe several times, until a "good" value materializes or until patience runs out -- (after ~2secs). -- If all retries were unsuccessful, 'aFewTimes' will return the last obtained value, even @@ -2750,14 +2487,6 @@ aFewTimesAssertBool msg good action = do result <- aFewTimes action good liftIO $ assertBool msg (good result) -checkUserUpdateEvent :: (HasCallStack) => UserId -> WS.WebSocket -> TestM () -checkUserUpdateEvent uid w = WS.assertMatch_ checkTimeout w $ \notif -> do - let j = Object $ List1.head (ntfPayload notif) - let etype = j ^? key "type" . _String - let euser = j ^?! key "user" ^? key "id" . _String - etype @?= Just "user.update" - euser @?= Just (UUID.toText (toUUID uid)) - checkUserDeleteEvent :: (HasCallStack) => UserId -> WS.Timeout -> WS.WebSocket -> TestM () checkUserDeleteEvent uid timeout_ w = WS.assertMatch_ timeout_ w $ \notif -> do let j = Object $ List1.head (ntfPayload notif) @@ -2863,10 +2592,6 @@ mockedFederatedBrigResponse users = do guardComponent Brig mockReply [mkProfile mem (Name name) | (mem, name) <- users] -fedRequestsForDomain :: (HasCallStack) => Domain -> Component -> [FederatedRequest] -> [FederatedRequest] -fedRequestsForDomain domain component = - filter $ \req -> frTargetDomain req == domain && frComponent req == component - parseFedRequest :: (FromJSON a) => FederatedRequest -> Either String a parseFedRequest fr = eitherDecode (frBody fr) @@ -2878,14 +2603,6 @@ assertTwo :: (HasCallStack, Show a) => [a] -> (a, a) assertTwo [a, b] = (a, b) assertTwo xs = error $ "Expected two elements, found " <> show xs -assertThree :: (HasCallStack, Show a) => [a] -> (a, a, a) -assertThree [a1, a2, a3] = (a1, a2, a3) -assertThree xs = error $ "Expected three elements, found " <> show xs - -assertNone :: (HasCallStack, MonadIO m, Show a) => [a] -> m () -assertNone [] = pure () -assertNone xs = liftIO . error $ "Expected exactly no elements, found " <> show xs - assertJust :: (HasCallStack, MonadIO m) => Maybe a -> m a assertJust (Just a) = pure a assertJust Nothing = liftIO $ error "Expected Just, got Nothing" @@ -2917,16 +2634,10 @@ generateRemoteAndConvIdWithDomain :: Domain -> Bool -> Local UserId -> TestM (Re generateRemoteAndConvIdWithDomain remoteDomain shouldBeLocal lUserId = do other <- Qualified <$> randomId <*> pure remoteDomain let convId = one2OneConvId BaseProtocolProteusTag (tUntagged lUserId) other - isLocal = tDomain lUserId == qDomain convId - if shouldBeLocal == isLocal + if shouldBeLocal == isLocal lUserId convId then pure (qTagUnsafe other, convId) else generateRemoteAndConvIdWithDomain remoteDomain shouldBeLocal lUserId -matchFedRequest :: Domain -> Text -> FederatedRequest -> Bool -matchFedRequest domain reqpath req = - frTargetDomain req == domain - && frRPC req == reqpath - spawn :: (HasCallStack) => CreateProcess -> Maybe ByteString -> IO ByteString spawn cp minput = do (mout, ex) <- withCreateProcess @@ -3036,27 +2747,3 @@ createAndConnectUsers domains = do (False, True) -> connectWithRemoteUser (qUnqualified b) a (False, False) -> pure () pure users - -putConversationProtocol :: (MonadIO m, MonadHttp m, HasGalley m, HasCallStack) => UserId -> ClientId -> Qualified ConvId -> ProtocolTag -> m ResponseLBS -putConversationProtocol uid client (Qualified conv domain) protocol = do - galley <- viewGalley - put - ( galley - . paths ["conversations", toByteString' domain, toByteString' conv, "protocol"] - . zUser uid - . zConn "conn" - . zClient client - . Bilge.json (object ["protocol" .= protocol]) - ) - -assertMixedProtocol :: (MonadIO m, HasCallStack) => Conversation -> m ConversationMLSData -assertMixedProtocol conv = do - case cnvProtocol conv of - ProtocolMixed mlsData -> pure mlsData - _ -> liftIO $ assertFailure "Unexpected protocol" - -connectBackend :: UserId -> Remote Backend -> TestM [Qualified UserId] -connectBackend usr (tDomain &&& bUsers . tUnqualified -> (d, c)) = do - users <- replicateM (fromIntegral c) (randomQualifiedId d) - mapM_ (connectWithRemoteUser usr) users - pure users diff --git a/services/galley/test/integration/API/Util/TeamFeature.hs b/services/galley/test/integration/API/Util/TeamFeature.hs index 749ea934531..cb8b7422a35 100644 --- a/services/galley/test/integration/API/Util/TeamFeature.hs +++ b/services/galley/test/integration/API/Util/TeamFeature.hs @@ -24,146 +24,35 @@ module API.Util.TeamFeature where import API.Util (HasGalley (viewGalley), zUser) import API.Util qualified as Util import Bilge -import Control.Lens ((.~), (^?)) -import Control.Monad.Catch (MonadThrow) -import Data.Aeson (FromJSON, Result (Success), ToJSON, Value, fromJSON) -import Data.Aeson.Key qualified as Key -import Data.Aeson.Lens +import Control.Lens ((%~)) import Data.ByteString.Conversion (toByteString') import Data.Id (ConvId, TeamId, UserId) -import Data.Schema -import GHC.TypeLits (KnownSymbol) import Galley.Options (featureFlags, settings) import Galley.Types.Teams import Imports -import Test.Tasty.HUnit (assertBool, assertFailure, (@?=)) import TestSetup import Wire.API.Team.Feature -import Wire.API.Team.Feature qualified as Public -withCustomSearchFeature :: FeatureTeamSearchVisibilityAvailability -> TestM () -> TestM () +withCustomSearchFeature :: FeatureDefaults SearchVisibilityAvailableConfig -> TestM () -> TestM () withCustomSearchFeature flag action = do - Util.withSettingsOverrides (\opts -> opts & settings . featureFlags . flagTeamSearchVisibility .~ flag) action + Util.withSettingsOverrides + ( \opts -> + opts & settings . featureFlags %~ npUpdate @SearchVisibilityAvailableConfig flag + ) + action putTeamSearchVisibilityAvailableInternal :: (HasCallStack) => TeamId -> - Public.FeatureStatus -> + FeatureStatus -> (MonadIO m, MonadHttp m, HasGalley m) => m () putTeamSearchVisibilityAvailableInternal tid statusValue = void $ putTeamFeatureInternal - @Public.SearchVisibilityAvailableConfig + @SearchVisibilityAvailableConfig expect2xx tid - (Public.WithStatusNoLock statusValue Public.SearchVisibilityAvailableConfig Public.FeatureTTLUnlimited) - -getTeamFeatureInternal :: - forall cfg m. - (HasGalley m, MonadIO m, MonadHttp m, KnownSymbol (Public.FeatureSymbol cfg)) => - TeamId -> - m ResponseLBS -getTeamFeatureInternal tid = do - g <- viewGalley - get $ - g - . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] - -getTeamFeature :: - forall cfg m. - (HasGalley m, MonadIO m, MonadHttp m, HasCallStack, KnownSymbol (Public.FeatureSymbol cfg)) => - UserId -> - TeamId -> - m ResponseLBS -getTeamFeature uid tid = do - galley <- viewGalley - get $ - galley - . paths ["teams", toByteString' tid, "features", Public.featureNameBS @cfg] - . zUser uid - -getAllTeamFeatures :: - (HasCallStack, HasGalley m, MonadIO m, MonadHttp m) => - UserId -> - TeamId -> - m ResponseLBS -getAllTeamFeatures uid tid = do - g <- viewGalley - get $ - g - . paths ["teams", toByteString' tid, "features"] - . zUser uid - -getTeamFeatureFromAll :: - forall cfg m. - ( HasCallStack, - MonadThrow m, - HasGalley m, - MonadIO m, - MonadHttp m, - KnownSymbol (Public.FeatureSymbol cfg), - FromJSON (Public.WithStatus cfg) - ) => - UserId -> - TeamId -> - m (Public.WithStatus cfg) -getTeamFeatureFromAll uid tid = do - response :: Value <- responseJsonError =<< getAllTeamFeatures uid tid - let status = response ^? key (Key.fromText (Public.featureName @cfg)) - maybe (error "getting all features failed") pure (status >>= fromResult . fromJSON) - where - fromResult :: Result a -> Maybe a - fromResult (Success b) = Just b - fromResult _ = Nothing - -getAllFeatureConfigs :: - (HasCallStack, HasGalley m, Monad m, MonadHttp m) => - UserId -> - m ResponseLBS -getAllFeatureConfigs uid = do - g <- viewGalley - get $ - g - . paths ["feature-configs"] - . zUser uid - -getFeatureConfig :: - forall cfg m. - ( HasCallStack, - MonadThrow m, - HasGalley m, - MonadHttp m, - KnownSymbol (Public.FeatureSymbol cfg), - FromJSON (Public.WithStatus cfg) - ) => - UserId -> - m (Public.WithStatus cfg) -getFeatureConfig uid = do - response :: Value <- responseJsonError =<< getAllFeatureConfigs uid - let status = response ^? key (Key.fromText (Public.featureName @cfg)) - maybe (error "getting all feature configs failed") pure (status >>= fromResult . fromJSON) - where - fromResult :: Result a -> Maybe a - fromResult (Success b) = Just b - fromResult _ = Nothing - -putTeamFeature :: - forall cfg. - ( HasCallStack, - KnownSymbol (Public.FeatureSymbol cfg), - ToJSON (Public.WithStatusNoLock cfg) - ) => - UserId -> - TeamId -> - Public.WithStatusNoLock cfg -> - TestM ResponseLBS -putTeamFeature uid tid status = do - galley <- viewGalley - put $ - galley - . paths ["teams", toByteString' tid, "features", Public.featureNameBS @cfg] - . json status - . zUser uid + (Feature statusValue SearchVisibilityAvailableConfig) putTeamFeatureInternal :: forall cfg m. @@ -171,65 +60,34 @@ putTeamFeatureInternal :: HasGalley m, MonadHttp m, HasCallStack, - KnownSymbol (Public.FeatureSymbol cfg), - ToJSON (Public.WithStatusNoLock cfg) + IsFeatureConfig cfg ) => (Request -> Request) -> TeamId -> - Public.WithStatusNoLock cfg -> + Feature cfg -> m ResponseLBS putTeamFeatureInternal reqmod tid status = do galley <- viewGalley put $ galley - . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] + . paths ["i", "teams", toByteString' tid, "features", featureNameBS @cfg] . json status . reqmod -setLockStatusInternal :: +putTeamFeature :: forall cfg. - ( HasCallStack, - KnownSymbol (Public.FeatureSymbol cfg) - ) => - (Request -> Request) -> + (HasCallStack, IsFeatureConfig cfg) => + UserId -> TeamId -> - Public.LockStatus -> + Feature cfg -> TestM ResponseLBS -setLockStatusInternal reqmod tid lockStatus = do +putTeamFeature uid tid status = do galley <- viewGalley put $ galley - . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg, toByteString' lockStatus] - . reqmod - -patchTeamFeatureInternal :: - forall cfg. - ( HasCallStack, - KnownSymbol (Public.FeatureSymbol cfg), - ToSchema cfg - ) => - TeamId -> - Public.WithStatusPatch cfg -> - TestM ResponseLBS -patchTeamFeatureInternal = patchTeamFeatureInternalWithMod id - -patchTeamFeatureInternalWithMod :: - forall cfg. - ( HasCallStack, - KnownSymbol (Public.FeatureSymbol cfg), - ToSchema cfg - ) => - (Request -> Request) -> - TeamId -> - Public.WithStatusPatch cfg -> - TestM ResponseLBS -patchTeamFeatureInternalWithMod reqmod tid reqBody = do - galley <- viewGalley - patch $ - galley - . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] - . json reqBody - . reqmod + . paths ["teams", toByteString' tid, "features", featureNameBS @cfg] + . json status + . zUser uid getGuestLinkStatus :: (HasCallStack) => @@ -240,46 +98,29 @@ getGuestLinkStatus :: getGuestLinkStatus galley u cid = get $ galley - . paths ["conversations", toByteString' cid, "features", Public.featureNameBS @Public.GuestLinksConfig] + . paths ["conversations", toByteString' cid, "features", featureNameBS @GuestLinksConfig] . zUser u -checkTeamFeatureAllEndpoints :: - forall cfg. - ( HasCallStack, - IsFeatureConfig cfg, - ToSchema cfg, - Typeable cfg, - Eq cfg, - Show cfg, - KnownSymbol (FeatureSymbol cfg) - ) => - UserId -> +getTeamFeatureInternal :: + forall cfg m. + (HasGalley m, MonadIO m, MonadHttp m, IsFeatureConfig cfg) => TeamId -> - WithStatus cfg -> - TestM () -checkTeamFeatureAllEndpoints uid tid expected = do - compareLeniently $ responseJsonUnsafe <$> getTeamFeatureInternal @cfg tid - compareLeniently $ responseJsonUnsafe <$> getTeamFeature @cfg uid tid - compareLeniently $ getTeamFeatureFromAll @cfg uid tid - compareLeniently $ getFeatureConfig uid - where - compareLeniently :: TestM (WithStatus cfg) -> TestM () - compareLeniently receive = do - received <- receive - liftIO $ do - wsStatus received @?= wsStatus expected - wsLockStatus received @?= wsLockStatus expected - wsConfig received @?= wsConfig expected - checkTtl (wsTTL received) (wsTTL expected) + m ResponseLBS +getTeamFeatureInternal tid = do + g <- viewGalley + get $ + g + . paths ["i", "teams", toByteString' tid, "features", featureNameBS @cfg] - checkTtl :: FeatureTTL -> FeatureTTL -> IO () - checkTtl (FeatureTTLSeconds actualTtl) (FeatureTTLSeconds expectedTtl) = - assertBool - ("expected the actual TTL to be greater than 0 and equal to or no more than 2 seconds less than " <> show expectedTtl <> ", but it was " <> show actualTtl) - ( actualTtl > 0 - && actualTtl <= expectedTtl - && abs (fromIntegral @Word @Int actualTtl - fromIntegral @Word @Int expectedTtl) <= 2 - ) - checkTtl FeatureTTLUnlimited FeatureTTLUnlimited = pure () - checkTtl FeatureTTLUnlimited _ = assertFailure "expected the actual TTL to be unlimited, but it was limited" - checkTtl _ FeatureTTLUnlimited = assertFailure "expected the actual TTL to be limited, but it was unlimited" +getTeamFeature :: + forall cfg m. + (HasGalley m, MonadIO m, MonadHttp m, HasCallStack, IsFeatureConfig cfg) => + UserId -> + TeamId -> + m ResponseLBS +getTeamFeature uid tid = do + galley <- viewGalley + get $ + galley + . paths ["teams", toByteString' tid, "features", featureNameBS @cfg] + . zUser uid diff --git a/services/galley/test/integration/Federation.hs b/services/galley/test/integration/Federation.hs index 666c557ad7d..bfa44d53dfc 100644 --- a/services/galley/test/integration/Federation.hs +++ b/services/galley/test/integration/Federation.hs @@ -1,15 +1,9 @@ -{-# LANGUAGE RecordWildCards #-} - module Federation where -import Cassandra qualified as C import Control.Lens ((^.)) -import Control.Monad.Catch -import Data.ByteString qualified as LBS import Data.Domain import Data.Id import Data.Qualified -import Data.Set qualified as Set import Data.UUID qualified as UUID import Galley.API.Util import Galley.App @@ -19,17 +13,9 @@ import Galley.Types.Conversations.Members (LocalMember (..), RemoteMember (..), import Imports import Test.Tasty.HUnit import TestSetup -import UnliftIO.Retry import Wire.API.Conversation -import Wire.API.Conversation qualified as Public import Wire.API.Conversation.Protocol (Protocol (..)) import Wire.API.Conversation.Role (roleNameWireMember) -import Wire.API.Routes.FederationDomainConfig -import Wire.API.Routes.MultiTablePaging -import Wire.API.Routes.MultiTablePaging qualified as Public - -x3 :: RetryPolicy -x3 = limitRetries 3 <> exponentialBackoff 100000 isConvMemberLTests :: TestM () isConvMemberLTests = do @@ -59,17 +45,3 @@ isConvMemberLTests = do liftIO $ assertBool "Remote UserId" $ isConvMemberL lconv rUserId liftIO $ assertBool "Qualified UserId (local)" $ isConvMemberL lconv $ tUntagged lUserId liftIO $ assertBool "Qualified UserId (remote)" $ isConvMemberL lconv $ tUntagged rUserId - -fromFedList :: FederationDomainConfigs -> Set Domain -fromFedList = Set.fromList . fmap domain . remotes - -constHandlers :: (MonadIO m) => [RetryStatus -> Handler m Bool] -constHandlers = [const $ Handler $ (\(_ :: SomeException) -> pure True)] - -pageToConvIdPage :: Public.LocalOrRemoteTable -> C.PageWithState (Qualified ConvId) -> Public.ConvIdsPage -pageToConvIdPage table page@C.PageWithState {..} = - Public.MultiTablePage - { mtpResults = pwsResults, - mtpHasMore = C.pwsHasMore page, - mtpPagingState = Public.ConversationPagingState table (LBS.toStrict . C.unPagingState <$> pwsState) - } diff --git a/services/galley/test/integration/Run.hs b/services/galley/test/integration/Run.hs index a5d212c39b4..54547697b7c 100644 --- a/services/galley/test/integration/Run.hs +++ b/services/galley/test/integration/Run.hs @@ -97,7 +97,7 @@ main = withOpenSSL $ runTests go ] getOpts gFile iFile = do m <- newManager tlsManagerSettings {managerResponseTimeout = responseTimeoutMicro 300000000} - let local p = Endpoint {_host = "127.0.0.1", _port = p} + let local p = Endpoint {host = "127.0.0.1", port = p} gConf <- handleParseError =<< decodeFileEither gFile iConf <- handleParseError =<< decodeFileEither iFile -- FUTUREWORK: we don't support process env setup any more, so both gconf and iConf diff --git a/services/galley/test/integration/TestSetup.hs b/services/galley/test/integration/TestSetup.hs index a6b9ba84f52..66b975c6c81 100644 --- a/services/galley/test/integration/TestSetup.hs +++ b/services/galley/test/integration/TestSetup.hs @@ -48,7 +48,7 @@ where import Bilge (Manager, MonadHttp (..), Request, withResponse) import Cassandra qualified as Cql -import Control.Lens (makeLenses, view, (^.)) +import Control.Lens (makeLenses, view) import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow) import Data.Aeson import Data.ByteString.Conversion @@ -155,8 +155,8 @@ runFedClient (FedClient mgr ep) domain = where servantClientMToHttp :: Domain -> Servant.ClientM a -> m a servantClientMToHttp originDomain action = liftIO $ do - let h = Text.unpack $ ep ^. host - p = fromInteger . toInteger $ ep ^. port + let h = Text.unpack ep.host + p = fromInteger $ toInteger ep.port baseUrl = Servant.BaseUrl Servant.Http h p "/federation" clientEnv = Servant.ClientEnv mgr baseUrl Nothing (makeClientRequest originDomain) eitherRes <- Servant.runClientM action clientEnv diff --git a/services/galley/test/resources/ecdsa_secp256r1_sha256.pem b/services/galley/test/resources/backendA/ecdsa_secp256r1_sha256.pem similarity index 100% rename from services/galley/test/resources/ecdsa_secp256r1_sha256.pem rename to services/galley/test/resources/backendA/ecdsa_secp256r1_sha256.pem diff --git a/services/galley/test/resources/ecdsa_secp384r1_sha384.pem b/services/galley/test/resources/backendA/ecdsa_secp384r1_sha384.pem similarity index 100% rename from services/galley/test/resources/ecdsa_secp384r1_sha384.pem rename to services/galley/test/resources/backendA/ecdsa_secp384r1_sha384.pem diff --git a/services/galley/test/resources/ecdsa_secp521r1_sha512.pem b/services/galley/test/resources/backendA/ecdsa_secp521r1_sha512.pem similarity index 100% rename from services/galley/test/resources/ecdsa_secp521r1_sha512.pem rename to services/galley/test/resources/backendA/ecdsa_secp521r1_sha512.pem diff --git a/services/galley/test/resources/ed25519.pem b/services/galley/test/resources/backendA/ed25519.pem similarity index 100% rename from services/galley/test/resources/ed25519.pem rename to services/galley/test/resources/backendA/ed25519.pem diff --git a/services/galley/test/resources/backendB/ecdsa_secp256r1_sha256.pem b/services/galley/test/resources/backendB/ecdsa_secp256r1_sha256.pem new file mode 100644 index 00000000000..260cdfe0e3a --- /dev/null +++ b/services/galley/test/resources/backendB/ecdsa_secp256r1_sha256.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgCUxypWAvn5V0pRz7 +DGYAhCmwZAkDW7Kid0CvsaLwutahRANCAARFmzfgJgQBGVCEJRB1WYGm2J0167aw +YRG7cSb74vHuaHaKio0c24n7o/daaBNOZMaBCi30lhL1YEzLI0wVJF51 +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/backendB/ecdsa_secp384r1_sha384.pem b/services/galley/test/resources/backendB/ecdsa_secp384r1_sha384.pem new file mode 100644 index 00000000000..8d4ede9322f --- /dev/null +++ b/services/galley/test/resources/backendB/ecdsa_secp384r1_sha384.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAfG49ge824AADZtVDW +JwWuJj/MoULnpNB1K3G4iRRvODb5E5yH7myhKSb3oeHnKaShZANiAARm+FFL69DT +Qk3tAVFQBP7ND7eu1Oq4VYyxcmynJj/NFIpCCgOs28AcKo6adqXOJizgeGf2/W4P +x+7Vi+Ir0TyIMpWBqo61G2jMDKMF23Yw/85tO1NdcL00As7kLF54nso= +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/backendB/ecdsa_secp521r1_sha512.pem b/services/galley/test/resources/backendB/ecdsa_secp521r1_sha512.pem new file mode 100644 index 00000000000..4dc9d6cb35e --- /dev/null +++ b/services/galley/test/resources/backendB/ecdsa_secp521r1_sha512.pem @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIACsFBjEdDV0Xb6AZM +g8fu/CLS+Tcd6MHQLEZ/G1abf7EIPTr8nCLvJqDEGVtgzqzqiW/ej8YOJLmIX7Xb +WLsPCr6hgYkDgYYABAG4C1/gxxaXR+8y/L3PGd45A3Rc54dpWVUvSWD7M50msW0c +Gs5gzLSMobNGUFeLfjzB0BbSdFALqawAsmBDi/LqrACkHUvBjUp4DfjkJwWdJWwz +/goHs+u1GB9MWVqsFmNUVgHjPDTbC4npMgVXWLELo3O9IzDAuvM2KbTjKo3djd/u +yw== +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/backendB/ed25519.pem b/services/galley/test/resources/backendB/ed25519.pem new file mode 100644 index 00000000000..14ca43a284b --- /dev/null +++ b/services/galley/test/resources/backendB/ed25519.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIO5vqw3LGpDiQ9AVsLuNp9BoWqXX+dat9PDYtcSgefcr +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend1/ecdsa_secp256r1_sha256.pem b/services/galley/test/resources/dynBackend1/ecdsa_secp256r1_sha256.pem new file mode 100644 index 00000000000..4aa899a53c1 --- /dev/null +++ b/services/galley/test/resources/dynBackend1/ecdsa_secp256r1_sha256.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgX3aqY4POOuFTDjI6 +rNyuCUARS0karqX6omz+ZHLBpmChRANCAAQBr9wUSSDssSVQylUfrIRoN6uNxJHu +/IfMnvieXDhS42a/R59G0YnZ+43Z0OHiclfIMYvMT4UzwmKNqF8rKGTh +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend1/ecdsa_secp384r1_sha384.pem b/services/galley/test/resources/dynBackend1/ecdsa_secp384r1_sha384.pem new file mode 100644 index 00000000000..d234e3c8372 --- /dev/null +++ b/services/galley/test/resources/dynBackend1/ecdsa_secp384r1_sha384.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBEDdTqi5zs5Zk/guXe +tbonA/9ZACWuoREM0sFPtIj3Sm0oOtRr7XkzHybAzKcTQwqhZANiAARyFcyRUpNM +b8XTSYk9AoVNxQjSFc5ENr99G/WJj6PPXinSo5ixazYbBGXt8N/jSr5U7JJHNQOp +/j0ZE2Ba/ARTU6bBySW5cuPn45o56c7aAv8743wc73Vvx57JOgCZxeY= +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend1/ecdsa_secp521r1_sha512.pem b/services/galley/test/resources/dynBackend1/ecdsa_secp521r1_sha512.pem new file mode 100644 index 00000000000..eabcd72c8ba --- /dev/null +++ b/services/galley/test/resources/dynBackend1/ecdsa_secp521r1_sha512.pem @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIB+aX/6XEvPsiiRep9 +yk9IzN7PWTOGXVB0/8MlvPJZEN/xhmaQQhhs+nCZ5PNo+/03bTe6ge4k+oxnjzLO +m4ST2fyhgYkDgYYABAFpStisSMDJyecgrzj/xyAoCVo1rMq5PqhgpaQ8uiR2Auwn +dVLk4RdC7Zqxx1j6gKy0YihlUeHQt7gr4/+6Q3muBwCt2IghBqyZL0by9A1LKRvS +vUJxwv1Iu8Sl2uljRP62QuE9ETOiH8BnCA+GRmwOUFZnxH/NLvT/OQSClUFQydVK +mQ== +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend1/ed25519.pem b/services/galley/test/resources/dynBackend1/ed25519.pem new file mode 100644 index 00000000000..5fa82d186bb --- /dev/null +++ b/services/galley/test/resources/dynBackend1/ed25519.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEICBjsYFFgSL4mLwbQKlP1eF/AHyb+1z4Vz6kDr1lMG3/ +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend2/ecdsa_secp256r1_sha256.pem b/services/galley/test/resources/dynBackend2/ecdsa_secp256r1_sha256.pem new file mode 100644 index 00000000000..d71d16694f6 --- /dev/null +++ b/services/galley/test/resources/dynBackend2/ecdsa_secp256r1_sha256.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEfaYkGCkcUbE/EhE +BMGZU9RKCyvgi/UESXvhQFgSje+hRANCAATnGRIEOs7xR64zD7yB2zbmwkgHL35o +scFaK0P+zSQ+Q6St4j+cb9+igjn8aFddVJiuWnMPsceLg9KueVXgNSWW +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend2/ecdsa_secp384r1_sha384.pem b/services/galley/test/resources/dynBackend2/ecdsa_secp384r1_sha384.pem new file mode 100644 index 00000000000..271c34cab68 --- /dev/null +++ b/services/galley/test/resources/dynBackend2/ecdsa_secp384r1_sha384.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAo8PEN99hC+GQ1L5eO +eQWP9w2/qL/gVOwzb48HloaDWkZlhTlth7N8v0KMkoyh3EmhZANiAARF8+nQz+cX +/fGTX9wrz+kn+mZHV0+Kh2j3n8bNcPp94L3MR/8t1qqQvoAvGFLUZULF7H7cU5st +HBiHxbVbz76Mqol9ZPbwJzCmcI+WAf1qPIiOjOWkceZM/YznUP/RM6Q= +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend2/ecdsa_secp521r1_sha512.pem b/services/galley/test/resources/dynBackend2/ecdsa_secp521r1_sha512.pem new file mode 100644 index 00000000000..95a40e376e3 --- /dev/null +++ b/services/galley/test/resources/dynBackend2/ecdsa_secp521r1_sha512.pem @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAbXN6cs+k71Pd3+gJ +hYTa5yMmmcitnp5myCOoKp3bvULH6naFQkg4PrMH3cnpgPcVaj6ZK+6m0qnlLg+x +PXQ9pQChgYkDgYYABAA6GUqOxi4NHNTSC+tphkagMqygFkrEzLHudYmwk2OwEDhG +cuf+ICuS+FmepZuMAI5QGqMNHLXttH0JXSHoob9eZAEWy8b0xOdj1GZHdNdk4aXL +tvwuzsZpeBKpMNwnTwggcJsepbBzXPANSyCnDyoCmKaHAQwtca2KFlwBB3uMRHve +2Q== +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend2/ed25519.pem b/services/galley/test/resources/dynBackend2/ed25519.pem new file mode 100644 index 00000000000..c73ba496405 --- /dev/null +++ b/services/galley/test/resources/dynBackend2/ed25519.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIBhKAk3pknMLukvifvnT6ujsMvlLKTa/1IjXS2lD5XTz +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend3/ecdsa_secp256r1_sha256.pem b/services/galley/test/resources/dynBackend3/ecdsa_secp256r1_sha256.pem new file mode 100644 index 00000000000..b3dbe2f828d --- /dev/null +++ b/services/galley/test/resources/dynBackend3/ecdsa_secp256r1_sha256.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgNhdHUB18rQWrJU4r +epN5CxOREiAYzKsGNuZ6//q5gbuhRANCAAQUvVXIJuKB81w1HBwgriR3UJ/Dy0se +u88O3pO+X28Uon/LczW9K2hzU5HwR+JF5NiKJTi3QQBzeaQTc1ybljf4 +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend3/ecdsa_secp384r1_sha384.pem b/services/galley/test/resources/dynBackend3/ecdsa_secp384r1_sha384.pem new file mode 100644 index 00000000000..9606a469ff5 --- /dev/null +++ b/services/galley/test/resources/dynBackend3/ecdsa_secp384r1_sha384.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDClNF0H/IIY4Z8WPOs1 +G4JonwQYqNuYnWgRGiDWvhXyXTI+uuhoN0PM9juzYuaJsoKhZANiAARB1q6sJd82 +MMI/4vJQbJBSnzOQWKNr8K+tsNOAGKuFGBBX1kOGsPiUJyEq64nnT97MJRfc/MW2 +A9gjrbMlM4fNoF3V6bqykfMDRBmOQ8oWovNFiH9Csbz46sjAsQKSw9g= +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend3/ecdsa_secp521r1_sha512.pem b/services/galley/test/resources/dynBackend3/ecdsa_secp521r1_sha512.pem new file mode 100644 index 00000000000..00229707a10 --- /dev/null +++ b/services/galley/test/resources/dynBackend3/ecdsa_secp521r1_sha512.pem @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAewieMnAzbnPuliiT +3Qj7v4zoxV308YQ2hDa17AWFEEg4ujeTHfRGZLjyMyh+fGnDVegBzJfdHfuExCcj +2RtSvp2hgYkDgYYABADr9KSC4esqdqkAoQcDvZa9mbVgow0+P/BS8Cj2Q5dnh0cj +2+p+F7cIEnvJJ7AY5heizlAyTxSv5U4Zx4Iein6A8QBqD7B0I0bkY2/ucVS1th0Z +9QAanTIqwxQ3HgJccHctI/M0QPgimOLYSmHpDmFlMSgtjKrJ6nOWgY1D2ev8l4n7 +xQ== +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend3/ed25519.pem b/services/galley/test/resources/dynBackend3/ed25519.pem new file mode 100644 index 00000000000..55dc2d42ff8 --- /dev/null +++ b/services/galley/test/resources/dynBackend3/ed25519.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIF7bh0Ix5rSrpGVYJCn/sZZJO46pPsF8yPO46wDcB6bF +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/generate_keys.sh b/services/galley/test/resources/generate_keys.sh new file mode 100755 index 00000000000..5b40e8db369 --- /dev/null +++ b/services/galley/test/resources/generate_keys.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +openssl genpkey -algorithm ed25519 > ed25519.pem +openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:P-256 > ecdsa_secp256r1_sha256.pem +openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:P-384 > ecdsa_secp384r1_sha384.pem +openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:P-521 > ecdsa_secp521r1_sha512.pem diff --git a/services/galley/test/unit/Test/Galley/Mapping.hs b/services/galley/test/unit/Test/Galley/Mapping.hs index c18bb63f903..b52bf67fd92 100644 --- a/services/galley/test/unit/Test/Galley/Mapping.hs +++ b/services/galley/test/unit/Test/Galley/Mapping.hs @@ -41,7 +41,7 @@ import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Federation.API.Galley ( RemoteConvMembers (..), - RemoteConversation (..), + RemoteConversationV2 (..), ) import Wire.Sem.Logger qualified as P diff --git a/services/gundeck/default.nix b/services/gundeck/default.nix index b925700365e..7629d0712c5 100644 --- a/services/gundeck/default.nix +++ b/services/gundeck/default.nix @@ -28,8 +28,9 @@ , extra , foldl , gitignoreSource -, gundeck-types , hedis +, hs-opentelemetry-instrumentation-wai +, hs-opentelemetry-sdk , HsOpenSSL , http-client , http-client-tls @@ -59,6 +60,7 @@ , safe , safe-exceptions , scientific +, servant , servant-server , string-conversions , tagged @@ -78,11 +80,10 @@ , wai , wai-extra , wai-middleware-gunzip -, wai-predicates -, wai-routing , wai-utilities , websockets , wire-api +, wire-otel , yaml }: mkDerivation { @@ -112,8 +113,9 @@ mkDerivation { extended extra foldl - gundeck-types hedis + hs-opentelemetry-instrumentation-wai + hs-opentelemetry-sdk http-client http-client-tls http-types @@ -130,6 +132,7 @@ mkDerivation { resourcet retry safe-exceptions + servant servant-server text time @@ -143,10 +146,9 @@ mkDerivation { wai wai-extra wai-middleware-gunzip - wai-predicates - wai-routing wai-utilities wire-api + wire-otel yaml ]; executableHaskellDepends = [ @@ -162,7 +164,6 @@ mkDerivation { containers exceptions extended - gundeck-types HsOpenSSL http-client http-client-tls @@ -200,11 +201,9 @@ mkDerivation { bytestring-conversion containers exceptions - gundeck-types HsOpenSSL imports lens - metrics-wai MonadRandom mtl multiset @@ -218,17 +217,14 @@ mkDerivation { tasty-hunit tasty-quickcheck text - time tinylog types-common - wai-utilities wire-api ]; benchmarkHaskellDepends = [ amazonka base criterion - gundeck-types HsOpenSSL imports lens @@ -236,6 +232,7 @@ mkDerivation { text types-common uuid + wire-api ]; description = "Push Notification Hub"; license = lib.licenses.agpl3Only; diff --git a/services/gundeck/gundeck.cabal b/services/gundeck/gundeck.cabal index e2150a6251c..19ce66fb3e2 100644 --- a/services/gundeck/gundeck.cabal +++ b/services/gundeck/gundeck.cabal @@ -18,7 +18,6 @@ flag static library -- cabal-fmt: expand src exposed-modules: - Gundeck.API Gundeck.API.Internal Gundeck.API.Public Gundeck.Aws @@ -66,6 +65,7 @@ library default-extensions: AllowAmbiguousTypes BangPatterns + BlockArguments ConstraintKinds DataKinds DefaultSignatures @@ -111,62 +111,63 @@ library -Wredundant-constraints -Wunused-packages build-depends: - , aeson >=2.0.1.0 - , amazonka >=2 - , amazonka-core >=2 - , amazonka-sns >=2 - , amazonka-sqs >=2 - , async >=2.0 - , attoparsec >=0.10 - , auto-update >=0.1 - , base >=4.7 && <5 - , bilge >=0.21 - , bytestring >=0.9 - , bytestring-conversion >=0.2 - , cassandra-util >=0.16.2 - , containers >=0.5 + , aeson >=2.0.1.0 + , amazonka >=2 + , amazonka-core >=2 + , amazonka-sns >=2 + , amazonka-sqs >=2 + , async >=2.0 + , attoparsec >=0.10 + , auto-update >=0.1 + , base >=4.7 && <5 + , bilge >=0.21 + , bytestring >=0.9 + , bytestring-conversion >=0.2 + , cassandra-util >=0.16.2 + , containers >=0.5 , crypton-x509-store - , errors >=2.0 - , exceptions >=0.4 + , errors >=2.0 + , exceptions >=0.4 , extended - , extra >=1.1 + , extra >=1.1 , foldl - , gundeck-types >=1.0 - , hedis >=0.14.0 - , http-client >=0.7 - , http-client-tls >=0.3 - , http-types >=0.8 + , hedis >=0.14.0 + , hs-opentelemetry-instrumentation-wai + , hs-opentelemetry-sdk + , http-client >=0.7 + , http-client-tls >=0.3 + , http-types >=0.8 , imports - , lens >=4.4 - , lens-aeson >=1.0 - , metrics-core >=0.2.1 - , metrics-wai >=0.5.7 - , mtl >=2.2 - , network-uri >=2.6 + , lens >=4.4 + , lens-aeson >=1.0 + , metrics-core >=0.2.1 + , metrics-wai + , mtl >=2.2 + , network-uri >=2.6 , prometheus-client - , psqueues >=0.2.2 + , psqueues >=0.2.2 , raw-strings-qq - , resourcet >=1.1 - , retry >=0.5 + , resourcet >=1.1 + , retry >=0.5 , safe-exceptions + , servant , servant-server - , text >=1.1 - , time >=1.4 - , tinylog >=0.10 - , tls >=1.7.0 - , types-common >=0.16 + , text >=1.1 + , time >=1.4 + , tinylog >=0.10 + , tls >=1.7.0 + , types-common >=0.16 , types-common-aws - , unliftio >=0.2 - , unordered-containers >=0.2 - , uuid >=1.3 - , wai >=3.2 - , wai-extra >=3.0 - , wai-middleware-gunzip >=0.0.2 - , wai-predicates >=0.8 - , wai-routing >=0.12 - , wai-utilities >=0.16 + , unliftio >=0.2 + , unordered-containers >=0.2 + , uuid >=1.3 + , wai >=3.2 + , wai-extra >=3.0 + , wai-middleware-gunzip >=0.0.2 + , wai-utilities >=0.16 , wire-api - , yaml >=0.8 + , wire-otel + , yaml >=0.8 default-language: GHC2021 @@ -293,7 +294,7 @@ executable gundeck-integration build-depends: , aeson , async - , base >=4 && <5 + , base >=4 && <5 , base16-bytestring >=0.1 , bilge , bytestring @@ -302,7 +303,6 @@ executable gundeck-integration , containers , exceptions , gundeck - , gundeck-types , HsOpenSSL , http-client , http-client-tls @@ -324,7 +324,7 @@ executable gundeck-integration , tinylog , types-common , uuid - , wai-utilities + , wai-utilities >=0.16 , websockets >=0.8 , wire-api , yaml @@ -538,11 +538,9 @@ test-suite gundeck-tests , containers , exceptions , gundeck - , gundeck-types , HsOpenSSL , imports , lens - , metrics-wai , MonadRandom , mtl , multiset @@ -556,10 +554,8 @@ test-suite gundeck-tests , tasty-hunit , tasty-quickcheck , text - , time , tinylog , types-common - , wai-utilities , wire-api default-language: GHC2021 @@ -621,7 +617,6 @@ benchmark gundeck-bench , base , criterion , gundeck - , gundeck-types , HsOpenSSL , imports , lens @@ -629,5 +624,6 @@ benchmark gundeck-bench , text , types-common , uuid + , wire-api default-language: GHC2021 diff --git a/services/gundeck/gundeck.integration.yaml b/services/gundeck/gundeck.integration.yaml index 6c4c2ca748a..adf2914f6aa 100644 --- a/services/gundeck/gundeck.integration.yaml +++ b/services/gundeck/gundeck.integration.yaml @@ -37,7 +37,6 @@ aws: settings: httpPoolSize: 1024 notificationTTL: 24192200 - bulkPush: true perNativePushConcurrency: 32 sqsThrottleMillis: 1000 maxConcurrentNativePushes: diff --git a/services/gundeck/migrate-data/src/Gundeck/DataMigration.hs b/services/gundeck/migrate-data/src/Gundeck/DataMigration.hs index 7bb99bb3f26..a1b20641cd8 100644 --- a/services/gundeck/migrate-data/src/Gundeck/DataMigration.hs +++ b/services/gundeck/migrate-data/src/Gundeck/DataMigration.hs @@ -40,10 +40,10 @@ data CassandraSettings = CassandraSettings toCassandraOpts :: CassandraSettings -> CassandraOpts toCassandraOpts cas = CassandraOpts - { _endpoint = Endpoint (Text.pack (cas.cHost)) (cas.cPort), - _keyspace = C.unKeyspace (cas.cKeyspace), - _filterNodesByDatacentre = Nothing, - _tlsCa = cas.cTlsCa + { endpoint = Endpoint (Text.pack (cas.cHost)) (cas.cPort), + keyspace = C.unKeyspace (cas.cKeyspace), + filterNodesByDatacentre = Nothing, + tlsCa = cas.cTlsCa } cassandraSettingsParser :: Parser CassandraSettings diff --git a/services/gundeck/src/Gundeck/API/Internal.hs b/services/gundeck/src/Gundeck/API/Internal.hs index 357d49bfe83..f0dfabe1d19 100644 --- a/services/gundeck/src/Gundeck/API/Internal.hs +++ b/services/gundeck/src/Gundeck/API/Internal.hs @@ -16,7 +16,8 @@ -- with this program. If not, see . module Gundeck.API.Internal - ( sitemap, + ( type InternalAPI, + servantSitemap, ) where @@ -29,63 +30,37 @@ import Gundeck.Presence qualified as Presence import Gundeck.Push qualified as Push import Gundeck.Push.Data qualified as PushTok import Gundeck.Push.Native.Types qualified as PushTok -import Imports hiding (head) -import Network.Wai -import Network.Wai.Predicate hiding (setStatus) -import Network.Wai.Routing hiding (route) -import Network.Wai.Utilities +import Imports +import Servant import Wire.API.Push.Token qualified as PushTok - -sitemap :: Routes a Gundeck () -sitemap = do - head "/i/status" (continue $ const (pure empty)) true - get "/i/status" (continue $ const (pure empty)) true - - -- Push API ----------------------------------------------------------- - - post "/i/push/v2" (continue pushH) $ - request .&. accept "application" "json" - - -- Presence API ---------------------------------------------------------- - - get "/i/presences/:uid" (continue Presence.list) $ - param "uid" .&. accept "application" "json" - - get "/i/presences" (continue Presence.listAll) $ - param "ids" .&. accept "application" "json" - - post "/i/presences" (continue Presence.add) $ - request .&. accept "application" "json" - - delete "/i/presences/:uid/devices/:did/cannons/:cannon" (continue Presence.remove) $ - param "uid" .&. param "did" .&. param "cannon" - - -- User-Client API ------------------------------------------------------- - - delete "/i/clients/:cid" (continue unregisterClientH) $ - header "Z-User" .&. param "cid" - - delete "/i/user" (continue removeUserH) $ - header "Z-User" - - get "/i/push-tokens/:uid" (continue getPushTokensH) $ - param "uid" - -type JSON = Media "application" "json" - -pushH :: Request ::: JSON -> Gundeck Response -pushH (req ::: _) = do - ps <- fromJsonBody (JsonRequest req) - empty <$ Push.push ps - -unregisterClientH :: UserId ::: ClientId -> Gundeck Response -unregisterClientH (uid ::: cid) = empty <$ Client.unregister uid cid - -removeUserH :: UserId -> Gundeck Response -removeUserH uid = empty <$ Client.removeUser uid - -getPushTokensH :: UserId -> Gundeck Response -getPushTokensH = fmap json . getPushTokens - -getPushTokens :: UserId -> Gundeck PushTok.PushTokenList -getPushTokens uid = PushTok.PushTokenList <$> (view PushTok.addrPushToken <$$> PushTok.lookup uid Cassandra.All) +import Wire.API.Push.V2 +import Wire.API.Routes.Internal.Gundeck +import Wire.API.Routes.Named + +servantSitemap :: ServerT InternalAPI Gundeck +servantSitemap = + Named @"i-status" statusH + :<|> Named @"i-push" pushH + :<|> ( Named @"i-presences-get-for-users" Presence.listAllH + :<|> Named @"i-presences-get-for-user" Presence.listH + :<|> Named @"i-presences-post" Presence.addH + :<|> Named @"i-presences-delete" Presence.removeH + ) + :<|> Named @"i-clients-delete" unregisterClientH + :<|> Named @"i-user-delete" removeUserH + :<|> Named @"i-push-tokens-get" getPushTokensH + +statusH :: (Applicative m) => m NoContent +statusH = pure NoContent + +pushH :: [Push] -> Gundeck NoContent +pushH ps = NoContent <$ Push.push ps + +unregisterClientH :: UserId -> ClientId -> Gundeck NoContent +unregisterClientH uid cid = NoContent <$ Client.unregister uid cid + +removeUserH :: UserId -> Gundeck NoContent +removeUserH uid = NoContent <$ Client.removeUser uid + +getPushTokensH :: UserId -> Gundeck PushTok.PushTokenList +getPushTokensH uid = PushTok.PushTokenList <$> (view PushTok.addrPushToken <$$> PushTok.lookup uid Cassandra.All) diff --git a/services/gundeck/src/Gundeck/Aws.hs b/services/gundeck/src/Gundeck/Aws.hs index 944a9d213bf..71014205f40 100644 --- a/services/gundeck/src/Gundeck/Aws.hs +++ b/services/gundeck/src/Gundeck/Aws.hs @@ -46,7 +46,6 @@ module Gundeck.Aws Attributes, AWS.Seconds (..), publish, - timeToLive, -- * Feedback listen, @@ -84,8 +83,6 @@ import Gundeck.Aws.Sns import Gundeck.Instances () import Gundeck.Options (Opts) import Gundeck.Options qualified as O -import Gundeck.Types.Push hiding (token) -import Gundeck.Types.Push qualified as Push import Imports import Network.HTTP.Client import Network.HTTP.Types @@ -95,6 +92,8 @@ import System.Logger.Class import UnliftIO.Async import UnliftIO.Exception import Util.Options +import Wire.API.Push.V2 hiding (token) +import Wire.API.Push.V2 qualified as Push data Error where EndpointNotFound :: EndpointArn -> Error @@ -383,26 +382,6 @@ newtype Attributes = Attributes -- -- cf. http://docs.aws.amazon.com/sns/latest/dg/sns-ttl.html -timeToLive :: Transport -> AWS.Seconds -> Attributes -timeToLive t s = Attributes (Endo (ttlAttr s)) - where - ttlAttr n - | n == 0 = setTTL (ttlNow t) - | otherwise = setTTL (toText n) - setTTL v = - let ty = SNS.newMessageAttributeValue "String" - in Map.insert (ttlKey t) (ty & SNS.messageAttributeValue_stringValue ?~ v) - ttlNow GCM = "0" - ttlNow APNS = "0" - ttlNow APNSSandbox = "0" - ttlNow APNSVoIP = "15" -- See note [VoIP TTLs] - ttlNow APNSVoIPSandbox = "15" -- See note [VoIP TTLs] - ttlKey GCM = "AWS.SNS.MOBILE.GCM.TTL" - ttlKey APNS = "AWS.SNS.MOBILE.APNS.TTL" - ttlKey APNSSandbox = "AWS.SNS.MOBILE.APNS_SANDBOX.TTL" - ttlKey APNSVoIP = "AWS.SNS.MOBILE.APNS_VOIP.TTL" - ttlKey APNSVoIPSandbox = "AWS.SNS.MOBILE.APNS_VOIP_SANDBOX.TTL" - publish :: EndpointArn -> LT.Text -> Attributes -> Amazon (Either PublishError ()) publish arn txt attrs = do -- TODO: Make amazonka accept a lazy text or bytestring. diff --git a/services/gundeck/src/Gundeck/Aws/Arn.hs b/services/gundeck/src/Gundeck/Aws/Arn.hs index 0ff914c5d57..6723ba223af 100644 --- a/services/gundeck/src/Gundeck/Aws/Arn.hs +++ b/services/gundeck/src/Gundeck/Aws/Arn.hs @@ -57,8 +57,8 @@ import Control.Foldl qualified as Foldl import Control.Lens import Data.Attoparsec.Text import Data.Text qualified as Text -import Gundeck.Types (AppName (..), Transport (..)) import Imports +import Wire.API.Push.V2 (AppName (..), Transport (..)) newtype ArnEnv = ArnEnv {arnEnvText :: Text} deriving (Show, ToText, FromJSON) diff --git a/services/gundeck/src/Gundeck/Env.hs b/services/gundeck/src/Gundeck/Env.hs index 8fc8b78abaf..2397005c68a 100644 --- a/services/gundeck/src/Gundeck/Env.hs +++ b/services/gundeck/src/Gundeck/Env.hs @@ -27,6 +27,7 @@ import Control.Concurrent.Async (Async) import Control.Lens (makeLenses, (^.)) import Control.Retry (capDelay, exponentialBackoff) import Data.ByteString.Char8 qualified as BSChar8 +import Data.Id import Data.Misc (Milliseconds (..)) import Data.Text qualified as Text import Data.Time.Clock @@ -62,9 +63,6 @@ data Env = Env makeLenses ''Env -schemaVersion :: Int32 -schemaVersion = 7 - createEnv :: Opts -> IO ([Async ()], Env) createEnv o = do l <- Logger.mkLogger (o ^. logLevel) (o ^. logNetStrings) (o ^. logFormat) @@ -103,7 +101,7 @@ createEnv o = do { updateAction = Ms . round . (* 1000) <$> getPOSIXTime } mtbs <- mkThreadBudgetState `mapM` (o ^. settings . maxConcurrentNativePushes) - pure $! (rThread : rAdditionalThreads,) $! Env (RequestId "N/A") o l n p r rAdditional a io mtbs + pure $! (rThread : rAdditionalThreads,) $! Env (RequestId defRequestId) o l n p r rAdditional a io mtbs reqIdMsg :: RequestId -> Logger.Msg -> Logger.Msg reqIdMsg = ("request" Logger..=) . unRequestId diff --git a/services/gundeck/src/Gundeck/Instances.hs b/services/gundeck/src/Gundeck/Instances.hs index 83ab2a692b4..ee225e82aed 100644 --- a/services/gundeck/src/Gundeck/Instances.hs +++ b/services/gundeck/src/Gundeck/Instances.hs @@ -31,8 +31,8 @@ import Data.Id import Data.Text.Encoding qualified as Text import Data.UUID qualified as Uuid import Gundeck.Aws.Arn (EndpointArn) -import Gundeck.Types import Imports +import Wire.API.Push.V2 instance Cql Transport where ctype = Tagged IntColumn diff --git a/services/gundeck/src/Gundeck/Monad.hs b/services/gundeck/src/Gundeck/Monad.hs index 5320f725501..6d4147ea70a 100644 --- a/services/gundeck/src/Gundeck/Monad.hs +++ b/services/gundeck/src/Gundeck/Monad.hs @@ -31,8 +31,6 @@ module Gundeck.Monad Gundeck, runDirect, runGundeck, - fromJsonBody, - ifNothing, posixTime, -- * Select which redis to target @@ -45,11 +43,9 @@ import Bilge hiding (Request, header, options, statusCode) import Bilge.RPC import Cassandra import Control.Concurrent.Async (AsyncCancelled) -import Control.Error import Control.Exception (throwIO) import Control.Lens (view, (.~), (^.)) import Control.Monad.Catch hiding (tryJust) -import Data.Aeson (FromJSON) import Data.Misc (Milliseconds (..)) import Data.UUID as UUID import Data.UUID.V4 as UUID @@ -57,9 +53,7 @@ import Database.Redis qualified as Redis import Gundeck.Env import Gundeck.Redis qualified as Redis import Imports -import Network.HTTP.Types import Network.Wai -import Network.Wai.Utilities import Prometheus import System.Logger qualified as Log import System.Logger qualified as Logger @@ -202,14 +196,6 @@ lookupReqId l r = case lookup requestIdName (requestHeaders r) of ~~ msg (val "generated a new request id for local request") pure localRid -fromJsonBody :: (FromJSON a) => JsonRequest a -> Gundeck a -fromJsonBody r = exceptT (throwM . mkError status400 "bad-request") pure (parseBody r) -{-# INLINE fromJsonBody #-} - -ifNothing :: Error -> Maybe a -> Gundeck a -ifNothing e = maybe (throwM e) pure -{-# INLINE ifNothing #-} - posixTime :: Gundeck Milliseconds posixTime = view time >>= liftIO {-# INLINE posixTime #-} diff --git a/services/gundeck/src/Gundeck/Options.hs b/services/gundeck/src/Gundeck/Options.hs index f5882a2a708..5f67081e178 100644 --- a/services/gundeck/src/Gundeck/Options.hs +++ b/services/gundeck/src/Gundeck/Options.hs @@ -57,9 +57,6 @@ data Settings = Settings _httpPoolSize :: !Int, -- | TTL (seconds) of stored notifications _notificationTTL :: !NotificationTTL, - -- | Use this option to group push notifications and send them in bulk to Cannon, instead - -- of in individual requests - _bulkPush :: !Bool, -- | Maximum number of concurrent threads calling SNS. _maxConcurrentNativePushes :: !(Maybe MaxConcurrentNativePushes), -- | Maximum number of parallel requests to SNS and cassandra diff --git a/services/gundeck/src/Gundeck/Presence.hs b/services/gundeck/src/Gundeck/Presence.hs index 4c626fe35ee..aa8fb778095 100644 --- a/services/gundeck/src/Gundeck/Presence.hs +++ b/services/gundeck/src/Gundeck/Presence.hs @@ -16,42 +16,32 @@ -- with this program. If not, see . module Gundeck.Presence - ( list, - listAll, - add, - remove, + ( listH, + listAllH, + addH, + removeH, ) where -import Data.ByteString.Conversion +import Data.CommaSeparatedList import Data.Id -import Data.Predicate import Gundeck.Monad import Gundeck.Presence.Data qualified as Data -import Gundeck.Types -import Gundeck.Util import Imports -import Network.HTTP.Types -import Network.Wai (Request, Response) -import Network.Wai.Utilities +import Servant.API hiding (URI) +import Wire.API.CannonId +import Wire.API.Presence -list :: UserId ::: JSON -> Gundeck Response -list (uid ::: _) = setStatus status200 . json <$> runWithDefaultRedis (Data.list uid) +listH :: UserId -> Gundeck [Presence] +listH = runWithDefaultRedis . Data.list -listAll :: List UserId ::: JSON -> Gundeck Response -listAll (uids ::: _) = - setStatus status200 . json . concat - <$> runWithDefaultRedis (Data.listAll (fromList uids)) +listAllH :: CommaSeparatedList UserId -> Gundeck [Presence] +listAllH uids = concat <$> runWithDefaultRedis (Data.listAll (fromCommaSeparatedList uids)) -add :: Request ::: JSON -> Gundeck Response -add (req ::: _) = do - p <- fromJsonBody (JsonRequest req) +addH :: Presence -> Gundeck (Headers '[Header "Location" URI] NoContent) +addH p = do Data.add p - pure $ - ( setStatus status201 - . addHeader hLocation (toByteString' (resource p)) - ) - empty + pure (addHeader (resource p) NoContent) -remove :: UserId ::: ConnId ::: CannonId -> Gundeck Response -remove _ = pure (empty & setStatus status204) +removeH :: UserId -> ConnId -> CannonId -> Gundeck NoContent +removeH _ _ _ = pure NoContent diff --git a/services/gundeck/src/Gundeck/Presence/Data.hs b/services/gundeck/src/Gundeck/Presence/Data.hs index bfe1773ba9c..6173ace303d 100644 --- a/services/gundeck/src/Gundeck/Presence/Data.hs +++ b/services/gundeck/src/Gundeck/Presence/Data.hs @@ -35,10 +35,10 @@ import Data.List.NonEmpty qualified as NonEmpty import Data.Misc (Milliseconds) import Database.Redis import Gundeck.Monad (Gundeck, posixTime, runWithAdditionalRedis) -import Gundeck.Types import Gundeck.Util.Redis import Imports import System.Logger.Class (MonadLogger) +import Wire.API.Presence -- Note [Migration] --------------------------------------------------------- -- diff --git a/services/gundeck/src/Gundeck/Push.hs b/services/gundeck/src/Gundeck/Push.hs index 6f3bcbcf684..098223a5547 100644 --- a/services/gundeck/src/Gundeck/Push.hs +++ b/services/gundeck/src/Gundeck/Push.hs @@ -23,15 +23,12 @@ module Gundeck.Push deleteToken, -- (for testing) pushAll, - pushAny, MonadPushAll (..), MonadNativeTargets (..), MonadMapAsync (..), - MonadPushAny (..), ) where -import Control.Arrow ((&&&)) import Control.Error import Control.Exception (ErrorCall (ErrorCall)) import Control.Lens (to, view, (.~), (^.)) @@ -43,7 +40,6 @@ import Data.List.Extra qualified as List import Data.List1 (List1, list1) import Data.Map qualified as Map import Data.Range -import Data.Sequence qualified as Seq import Data.Set qualified as Set import Data.Text qualified as Text import Data.UUID qualified as UUID @@ -60,8 +56,6 @@ import Gundeck.Push.Native qualified as Native import Gundeck.Push.Native.Types import Gundeck.Push.Websocket qualified as Web import Gundeck.ThreadBudget -import Gundeck.Types -import Gundeck.Types.Presence qualified as Presence import Gundeck.Util import Imports import Network.HTTP.Types @@ -69,20 +63,16 @@ import Network.Wai.Utilities import System.Logger.Class (msg, val, (+++), (.=), (~~)) import System.Logger.Class qualified as Log import Wire.API.Internal.Notification +import Wire.API.Presence (Presence (..)) +import Wire.API.Presence qualified as Presence import Wire.API.Push.Token qualified as Public +import Wire.API.Push.V2 push :: [Push] -> Gundeck () push ps = do - bulk :: Bool <- view (options . settings . bulkPush) - rs <- - if bulk - then (Right <$> pushAll ps) `catch` (pure . Left . Seq.singleton) - else pushAny ps - case rs of - Right () -> pure () - Left exs -> do - forM_ exs $ Log.err . msg . (val "Push failed: " +++) . show - throwM (mkError status500 "server-error" "Server Error") + pushAll ps `catch` \(ex :: SomeException) -> do + Log.err $ msg (val "Push failed") . Log.field "error" (displayException ex) + throwM (mkError status500 "server-error" "Server Error") -- | Abstract over all effects in 'pushAll' (for unit testing). class (MonadThrow m) => MonadPushAll m where @@ -133,59 +123,6 @@ instance MonadMapAsync Gundeck where Nothing -> mapAsync f l Just chunkSize -> concat <$> mapM (mapAsync f) (List.chunksOf chunkSize l) --- | Abstract over all effects in 'pushAny' (for unit testing). -class (MonadPushAll m, MonadNativeTargets m, MonadMapAsync m) => MonadPushAny m where - mpyPush :: - Notification -> - List1 NotificationTarget -> - Maybe UserId -> - Maybe ConnId -> - Set ConnId -> - m [Presence] - -instance MonadPushAny Gundeck where - mpyPush = Web.push - --- | Send individual HTTP requests to cannon for every device and notification. --- --- REFACTOR: This should go away in the future, once 'pushAll' has been proven to always do the same --- thing. also check what types this removal would make unnecessary. -pushAny :: - forall m. - (MonadPushAny m) => - [Push] -> - m (Either (Seq.Seq SomeException) ()) -pushAny ps = collectErrors <$> mntgtMapAsync pushAny' ps - where - collectErrors :: [Either SomeException ()] -> Either (Seq.Seq SomeException) () - collectErrors = runAllE . foldMap (AllE . fmapL Seq.singleton) - -pushAny' :: - forall m. - (MonadPushAny m) => - Push -> - m () -pushAny' p = do - i <- mpaMkNotificationId - let pload = p ^. pushPayload - let notif = Notification i (p ^. pushTransient) pload - let rcps = fromRange (p ^. pushRecipients) - let uniq = uncurry list1 $ head &&& tail $ toList rcps - let tgts = mkTarget <$> uniq - unless (p ^. pushTransient) $ - mpaStreamAdd i tgts pload =<< mpaNotificationTTL - mpaForkIO $ do - alreadySent <- mpyPush notif tgts (p ^. pushOrigin) (p ^. pushOriginConnection) (p ^. pushConnections) - unless (p ^. pushTransient) $ - mpaPushNative notif (p ^. pushNativePriority) =<< nativeTargets p (nativeTargetsRecipients p) alreadySent - where - mkTarget :: Recipient -> NotificationTarget - mkTarget r = - target (r ^. recipientId) - & targetClients .~ case r ^. recipientClients of - RecipientClientsAll -> [] - RecipientClientsSome cs -> toList cs - -- | Construct and send a single bulk push request to the client. Write the 'Notification's from -- the request to C*. Trigger native pushes for all delivery failures notifications. pushAll :: (MonadPushAll m, MonadNativeTargets m, MonadMapAsync m) => [Push] -> m () diff --git a/services/gundeck/src/Gundeck/Push/Data.hs b/services/gundeck/src/Gundeck/Push/Data.hs index 5c3fc33cd34..01adce9b11d 100644 --- a/services/gundeck/src/Gundeck/Push/Data.hs +++ b/services/gundeck/src/Gundeck/Push/Data.hs @@ -30,10 +30,10 @@ import Data.ByteString.Conversion import Data.Id (ClientId, ConnId, UserId) import Gundeck.Instances () import Gundeck.Push.Native.Types -import Gundeck.Types hiding (token) import Imports hiding (lookup) import System.Logger.Class (MonadLogger, field, msg, val, (~~)) import System.Logger.Class qualified as Log +import Wire.API.Push.V2 hiding (token) lookup :: (MonadClient m, MonadLogger m) => UserId -> Consistency -> m [Address] lookup u c = foldM mk [] =<< retry x1 (query q (params c (Identity u))) diff --git a/services/gundeck/src/Gundeck/Push/Native.hs b/services/gundeck/src/Gundeck/Push/Native.hs index 0b9c6660eb4..3353568de84 100644 --- a/services/gundeck/src/Gundeck/Push/Native.hs +++ b/services/gundeck/src/Gundeck/Push/Native.hs @@ -39,14 +39,15 @@ import Gundeck.Options import Gundeck.Push.Data qualified as Data import Gundeck.Push.Native.Serialise import Gundeck.Push.Native.Types as Types -import Gundeck.Types import Gundeck.Util import Imports import Prometheus qualified as Prom import System.Logger.Class (MonadLogger, field, msg, val, (.=), (~~)) import System.Logger.Class qualified as Log import UnliftIO (handleAny, mapConcurrently, pooledMapConcurrentlyN_) +import Wire.API.Event.Gundeck import Wire.API.Internal.Notification +import Wire.API.Push.V2 push :: NativePush -> [Address] -> Gundeck () push _ [] = pure () diff --git a/services/gundeck/src/Gundeck/Push/Native/Serialise.hs b/services/gundeck/src/Gundeck/Push/Native/Serialise.hs index 648a888f834..6be13d4d80b 100644 --- a/services/gundeck/src/Gundeck/Push/Native/Serialise.hs +++ b/services/gundeck/src/Gundeck/Push/Native/Serialise.hs @@ -32,8 +32,8 @@ import Data.Text.Encoding (encodeUtf8) import Data.Text.Lazy qualified as LT import Data.Text.Lazy.Builder qualified as LTB import Gundeck.Push.Native.Types -import Gundeck.Types import Imports +import Wire.API.Push.V2 serialise :: (HasCallStack) => NativePush -> UserId -> Transport -> Either Failure LT.Text serialise (NativePush nid prio _aps) uid transport = do diff --git a/services/gundeck/src/Gundeck/Push/Native/Types.hs b/services/gundeck/src/Gundeck/Push/Native/Types.hs index d191bfb0459..cf28f90ebbb 100644 --- a/services/gundeck/src/Gundeck/Push/Native/Types.hs +++ b/services/gundeck/src/Gundeck/Push/Native/Types.hs @@ -29,7 +29,6 @@ module Gundeck.Push.Native.Types addrEndpoint, addrConn, addrClient, - addrEqualClient, addrPushToken, -- * Re-Exports @@ -42,12 +41,12 @@ module Gundeck.Push.Native.Types ) where -import Control.Lens (Lens', makeLenses, view, (^.)) +import Control.Lens (Lens', makeLenses, (^.)) import Data.Id (ClientId, ConnId, UserId) import Gundeck.Aws.Arn -import Gundeck.Types import Imports import Wire.API.Internal.Notification +import Wire.API.Push.V2 -- | Native push address information of a device. data Address = Address @@ -72,11 +71,6 @@ addrToken = addrPushToken . token addrClient :: Lens' Address ClientId addrClient = addrPushToken . tokenClient -addrEqualClient :: Address -> Address -> Bool -addrEqualClient a a' = - view addrConn a == view addrConn a' - || view addrClient a == view addrClient a' - instance Show Address where show a = showString "Address" diff --git a/services/gundeck/src/Gundeck/Push/Websocket.hs b/services/gundeck/src/Gundeck/Push/Websocket.hs index 2a6ff64e406..97600d2720d 100644 --- a/services/gundeck/src/Gundeck/Push/Websocket.hs +++ b/services/gundeck/src/Gundeck/Push/Websocket.hs @@ -41,7 +41,6 @@ import Data.Set qualified as Set import Data.Time.Clock.POSIX import Gundeck.Monad import Gundeck.Presence.Data qualified as Presence -import Gundeck.Types.Presence import Gundeck.Util import Imports import Network.HTTP.Client (HttpExceptionContent (..)) @@ -54,6 +53,7 @@ import System.Logger.Class qualified as Log import UnliftIO (handleAny, mapConcurrently) import Wire.API.Internal.BulkPush import Wire.API.Internal.Notification +import Wire.API.Presence class (Monad m, MonadThrow m, Log.MonadLogger m) => MonadBulkPush m where mbpBulkSend :: URI -> BulkPushRequest -> m (URI, Either SomeException BulkPushResponse) @@ -165,7 +165,7 @@ bulkSend uri req = (uri,) <$> ((Right <$> bulkSend' uri req) `catch` (pure . Lef bulkSend' :: forall m. - ( MonadIO m, + ( MonadUnliftIO m, MonadMask m, HasRequestId m, MonadHttp m, diff --git a/services/gundeck/src/Gundeck/React.hs b/services/gundeck/src/Gundeck/React.hs index 9ffdf521cca..098eefa1288 100644 --- a/services/gundeck/src/Gundeck/React.hs +++ b/services/gundeck/src/Gundeck/React.hs @@ -41,12 +41,13 @@ import Gundeck.Options (notificationTTL, settings) import Gundeck.Push.Data qualified as Push import Gundeck.Push.Native.Types import Gundeck.Push.Websocket qualified as Web -import Gundeck.Types import Gundeck.Util import Imports import System.Logger.Class (Msg, msg, val, (+++), (.=), (~~)) import System.Logger.Class qualified as Log +import Wire.API.Event.Gundeck import Wire.API.Internal.Notification +import Wire.API.Push.V2 onEvent :: Event -> Gundeck () onEvent ev = case ev ^. evType of diff --git a/services/gundeck/src/Gundeck/Run.hs b/services/gundeck/src/Gundeck/Run.hs index 4780f1142a9..b978b9d6b13 100644 --- a/services/gundeck/src/Gundeck/Run.hs +++ b/services/gundeck/src/Gundeck/Run.hs @@ -27,25 +27,29 @@ import Control.Exception (finally) import Control.Lens ((.~), (^.)) import Control.Monad.Extra import Data.Metrics.AWS (gaugeTokenRemaing) -import Data.Metrics.Middleware.Prometheus (waiPrometheusMiddleware) +import Data.Metrics.Servant qualified as Metrics import Data.Proxy (Proxy (Proxy)) import Data.Text (unpack) import Database.Redis qualified as Redis -import Gundeck.API (sitemap) -import Gundeck.API.Public (servantSitemap) +import Gundeck.API.Internal as Internal (InternalAPI, servantSitemap) +import Gundeck.API.Public as Public (servantSitemap) import Gundeck.Aws qualified as Aws import Gundeck.Env import Gundeck.Env qualified as Env import Gundeck.Monad import Gundeck.Options hiding (host, port) import Gundeck.React +import Gundeck.Schema.Run (lastSchemaVersion) import Gundeck.ThreadBudget -import Imports hiding (head) +import Imports import Network.Wai as Wai import Network.Wai.Middleware.Gunzip qualified as GZip import Network.Wai.Middleware.Gzip qualified as GZip import Network.Wai.Utilities.Request import Network.Wai.Utilities.Server hiding (serverPort) +import OpenTelemetry.Instrumentation.Wai (newOpenTelemetryWaiMiddleware) +import OpenTelemetry.Trace (defaultSpanArguments, inSpan, kind) +import OpenTelemetry.Trace qualified as Otel import Servant (Handler (Handler), (:<|>) (..)) import Servant qualified import System.Logger qualified as Log @@ -54,22 +58,23 @@ import Util.Options import Wire.API.Routes.Public.Gundeck (GundeckAPI) import Wire.API.Routes.Version import Wire.API.Routes.Version.Wai +import Wire.OpenTelemetry run :: Opts -> IO () -run o = do +run o = withTracer \tracer -> do (rThreads, e) <- createEnv o runClient (e ^. cstate) $ - versionCheck schemaVersion + versionCheck lastSchemaVersion let l = e ^. applog - s <- newSettings $ defaultServer (unpack $ o ^. gundeck . host) (o ^. gundeck . port) l + s <- newSettings $ defaultServer (unpack . host $ o ^. gundeck) (port $ o ^. gundeck) l let throttleMillis = fromMaybe defSqsThrottleMillis $ o ^. (settings . sqsThrottleMillis) lst <- Async.async $ Aws.execute (e ^. awsEnv) (Aws.listen throttleMillis (runDirect e . onEvent)) wtbs <- forM (e ^. threadBudgetState) $ \tbs -> Async.async $ runDirect e $ watchThreadBudgetState tbs 10 wCollectAuth <- Async.async (collectAuthMetrics (Aws._awsEnv (Env._awsEnv e))) - let app = middleware e $ mkApp e - runSettingsWithShutdown s app Nothing `finally` do + app <- middleware e <*> pure (mkApp e) + inSpan tracer "gundeck" defaultSpanArguments {kind = Otel.Server} (runSettingsWithShutdown s app Nothing) `finally` do Log.info l $ Log.msg (Log.val "Shutting down ...") shutdown (e ^. cstate) Async.cancel lst @@ -80,32 +85,26 @@ run o = do whenJust (e ^. rstateAdditionalWrite) $ (=<<) Redis.disconnect . takeMVar Log.close (e ^. applog) where - middleware :: Env -> Middleware - middleware e = - versionMiddleware (foldMap expandVersionExp (o ^. settings . disabledAPIVersions)) - . requestIdMiddleware (e ^. applog) defaultRequestIdHeaderName - . waiPrometheusMiddleware sitemap - . GZip.gunzip - . GZip.gzip GZip.def - . catchErrors (e ^. applog) defaultRequestIdHeaderName - -type CombinedAPI = GundeckAPI :<|> Servant.Raw + middleware :: Env -> IO Middleware + middleware e = do + otelMiddleWare <- newOpenTelemetryWaiMiddleware + pure $ + versionMiddleware (foldMap expandVersionExp (o ^. settings . disabledAPIVersions)) + . otelMiddleWare + . requestIdMiddleware (e ^. applog) defaultRequestIdHeaderName + . Metrics.servantPrometheusMiddleware (Proxy @(GundeckAPI :<|> InternalAPI)) + . GZip.gunzip + . GZip.gzip GZip.def + . catchErrors (e ^. applog) defaultRequestIdHeaderName mkApp :: Env -> Wai.Application mkApp env0 req cont = do let rid = getRequestId defaultRequestIdHeaderName req env = reqId .~ rid $ env0 - Servant.serve - (Proxy @CombinedAPI) - (servantSitemap' env :<|> Servant.Tagged (runGundeckWithRoutes env)) - req - cont - where - runGundeckWithRoutes :: Env -> Wai.Application - runGundeckWithRoutes e r k = runGundeck e r (route (compile sitemap) r k) + Servant.serve (Proxy @(GundeckAPI :<|> InternalAPI)) (servantSitemap' env) req cont -servantSitemap' :: Env -> Servant.Server GundeckAPI -servantSitemap' env = Servant.hoistServer (Proxy @GundeckAPI) toServantHandler servantSitemap +servantSitemap' :: Env -> Servant.Server (GundeckAPI :<|> InternalAPI) +servantSitemap' env = Servant.hoistServer (Proxy @(GundeckAPI :<|> InternalAPI)) toServantHandler (Public.servantSitemap :<|> Internal.servantSitemap) where toServantHandler :: Gundeck a -> Handler a toServantHandler m = Handler . ExceptT $ Right <$> runDirect env m diff --git a/services/gundeck/src/Gundeck/Util.hs b/services/gundeck/src/Gundeck/Util.hs index 9b210881463..831ae3955af 100644 --- a/services/gundeck/src/Gundeck/Util.hs +++ b/services/gundeck/src/Gundeck/Util.hs @@ -23,13 +23,10 @@ import Data.Id import Data.UUID.V1 import Imports import Network.HTTP.Types.Status -import Network.Wai.Predicate.MediaType (Media) import Network.Wai.Utilities import UnliftIO (async, waitCatch) import Wire.API.Internal.Notification -type JSON = Media "application" "json" - -- | 'Data.UUID.V1.nextUUID' is sometimes unsuccessful, so we try a few times. mkNotificationId :: (MonadIO m, MonadThrow m) => m NotificationId mkNotificationId = do @@ -47,8 +44,3 @@ mapAsync :: m (t (Either SomeException b)) mapAsync f = mapM waitCatch <=< mapM (async . f) {-# INLINE mapAsync #-} - -maybeEqual :: (Eq a) => Maybe a -> Maybe a -> Bool -maybeEqual (Just x) (Just y) = x == y -maybeEqual _ _ = False -{-# INLINE maybeEqual #-} diff --git a/services/gundeck/src/Gundeck/Util/Redis.hs b/services/gundeck/src/Gundeck/Util/Redis.hs index 891505c39ae..d125d04baca 100644 --- a/services/gundeck/src/Gundeck/Util/Redis.hs +++ b/services/gundeck/src/Gundeck/Util/Redis.hs @@ -29,9 +29,6 @@ import System.Logger.Message retry :: (MonadIO m, MonadMask m, MonadLogger m) => RetryPolicyM m -> m a -> m a retry x = recovering x handlers . const -x1 :: RetryPolicy -x1 = limitRetries 1 <> exponentialBackoff 100000 - x3 :: RetryPolicy x3 = limitRetries 3 <> exponentialBackoff 100000 diff --git a/services/gundeck/test/bench/Main.hs b/services/gundeck/test/bench/Main.hs index 79fd1d6a9a7..c18d2b26932 100644 --- a/services/gundeck/test/bench/Main.hs +++ b/services/gundeck/test/bench/Main.hs @@ -33,10 +33,10 @@ import Gundeck.Options import Gundeck.Push.Native.Serialise import Gundeck.Push.Native.Types import Gundeck.ThreadBudget.Internal -import Gundeck.Types.Push import Imports import OpenSSL (withOpenSSL) import System.Random (randomRIO) +import Wire.API.Push.V2 main :: IO () main = withOpenSSL $ do diff --git a/services/gundeck/test/integration/API.hs b/services/gundeck/test/integration/API.hs index e5e3a3cdbcc..5db3cbb086c 100644 --- a/services/gundeck/test/integration/API.hs +++ b/services/gundeck/test/integration/API.hs @@ -50,10 +50,8 @@ import Data.Set qualified as Set import Data.Text.Encoding qualified as T import Data.UUID qualified as UUID import Data.UUID.V4 -import Gundeck.Options hiding (bulkPush) +import Gundeck.Options import Gundeck.Options qualified as O -import Gundeck.Types -import Gundeck.Types.Common qualified import Imports import Network.HTTP.Client qualified as Http import Network.URI (parseURI) @@ -65,7 +63,10 @@ import Test.Tasty import Test.Tasty.HUnit import TestSetup import Util (runRedisProxy, withEnvOverrides, withSettingsOverrides) +import Wire.API.Event.Gundeck import Wire.API.Internal.Notification +import Wire.API.Presence +import Wire.API.Push.V2 import Prelude qualified tests :: IO TestSetup -> TestTree @@ -871,7 +872,7 @@ testRedisMigration :: TestM () testRedisMigration = do uid <- randomUser con <- randomConnId - cannonURI <- Gundeck.Types.Common.parse "http://cannon.example" + cannonURI <- Wire.API.Presence.parse "http://cannon.example" let presence = Presence uid con cannonURI Nothing 1 "" redis2 <- view tsRedis2 diff --git a/services/gundeck/test/unit/Aws/Arn.hs b/services/gundeck/test/unit/Aws/Arn.hs index 9d20bfaeec0..dde384d6b35 100644 --- a/services/gundeck/test/unit/Aws/Arn.hs +++ b/services/gundeck/test/unit/Aws/Arn.hs @@ -3,10 +3,10 @@ module Aws.Arn where import Amazonka.Data.Text import Control.Lens import Gundeck.Aws.Arn -import Gundeck.Types import Imports import Test.Tasty import Test.Tasty.HUnit +import Wire.API.Push.V2 tests :: TestTree tests = diff --git a/services/gundeck/test/unit/Json.hs b/services/gundeck/test/unit/Json.hs index b83dbf006be..b84b925c4f1 100644 --- a/services/gundeck/test/unit/Json.hs +++ b/services/gundeck/test/unit/Json.hs @@ -22,13 +22,13 @@ import Data.Aeson import Data.Aeson.KeyMap (fromList) import Data.Id import Data.List1 -import Gundeck.Types.Push import Imports import Test.Tasty import Test.Tasty.HUnit import Test.Tasty.QuickCheck import Wire.API.Internal.BulkPush import Wire.API.Internal.Notification +import Wire.API.Push.V2 tests :: TestTree tests = diff --git a/services/gundeck/test/unit/Main.hs b/services/gundeck/test/unit/Main.hs index 332418beb38..826e49f401f 100644 --- a/services/gundeck/test/unit/Main.hs +++ b/services/gundeck/test/unit/Main.hs @@ -21,19 +21,14 @@ module Main where import Aws.Arn qualified -import Data.Metrics.Test (pathsConsistencyCheck) -import Data.Metrics.WaiRoute (treeToPaths) import DelayQueue qualified -import Gundeck.API qualified import Imports import Json qualified import Native qualified -import Network.Wai.Utilities.Server (compile) import OpenSSL (withOpenSSL) import ParseExistsError qualified import Push qualified import Test.Tasty -import Test.Tasty.HUnit import ThreadBudget qualified main :: IO () @@ -41,12 +36,7 @@ main = withOpenSSL . defaultMain $ testGroup "Main" - [ testCase "sitemap" $ - assertEqual - "inconcistent sitemap" - mempty - (pathsConsistencyCheck . treeToPaths . compile $ Gundeck.API.sitemap), - DelayQueue.tests, + [ DelayQueue.tests, Json.tests, Native.tests, Push.tests, diff --git a/services/gundeck/test/unit/MockGundeck.hs b/services/gundeck/test/unit/MockGundeck.hs index d662a62aa10..10bc5806bb6 100644 --- a/services/gundeck/test/unit/MockGundeck.hs +++ b/services/gundeck/test/unit/MockGundeck.hs @@ -67,7 +67,6 @@ import Gundeck.Options import Gundeck.Push import Gundeck.Push.Native as Native import Gundeck.Push.Websocket as Web -import Gundeck.Types hiding (recipient) import Imports import Network.URI qualified as URI import System.Logger.Class as Log hiding (trace) @@ -75,6 +74,8 @@ import Test.QuickCheck as QC import Test.QuickCheck.Instances () import Wire.API.Internal.BulkPush import Wire.API.Internal.Notification +import Wire.API.Presence +import Wire.API.Push.V2 hiding (recipient) ---------------------------------------------------------------------- -- env @@ -438,9 +439,6 @@ instance MonadMapAsync MockGundeck where mntgtPerPushConcurrency = pure Nothing -- (unbounded) mntgtMapAsync f xs = Right <$$> mapM f xs -- (no concurrency) -instance MonadPushAny MockGundeck where - mpyPush = mockOldSimpleWebPush - instance MonadBulkPush MockGundeck where mbpBulkSend = mockBulkSend mbpDeleteAllPresences _ = pure () -- FUTUREWORK: test presence deletion logic @@ -643,53 +641,6 @@ mockBulkSend uri notifs = do BulkPushResponse [(ntfId ntif, trgt, getstatus trgt) | (ntif, trgt) <- flat] -mockOldSimpleWebPush :: - (HasCallStack, m ~ MockGundeck) => - Notification -> - List1 NotificationTarget -> - Maybe UserId -> - Maybe ConnId -> - Set ConnId -> - m [Presence] -mockOldSimpleWebPush notif tgts _senderid mconnid connWhitelist = do - env <- ask - getstatus <- mkWSStatus - let clients :: [(UserId, ClientId)] - clients = - -- reformat - fmap (\(PushTarget uid connid) -> (uid, clientIdFromConnId connid)) - -- drop all broken web sockets - . filter ((== PushStatusOk) . getstatus) - -- do not push to sending device - . filter ((/= mconnid) . Just . ptConnId) - -- reformat - . mconcat - . fmap - ( ( \tgt -> - PushTarget (tgt ^. targetUser) - . fakeConnId - <$> (tgt ^. targetClients) - ) - -- apply filters - . connWhitelistSieve - . emptyMeansFullHack - ) - $ toList tgts - connWhitelistSieve :: NotificationTarget -> NotificationTarget - connWhitelistSieve = - if null connWhitelist - then id - else targetClients %~ filter ((`elem` connWhitelist) . fakeConnId) - emptyMeansFullHack :: NotificationTarget -> NotificationTarget - emptyMeansFullHack tgt = - tgt - & targetClients %~ \case - [] -> clientIdsOfUser env (tgt ^. targetUser) - same@(_ : _) -> same - forM_ clients $ \(userid, clientid) -> do - msWSQueue %= deliver (userid, clientid) (ntfPayload notif) - pure $ uncurry fakePresence <$> clients - ---------------------------------------------------------------------- -- helpers diff --git a/services/gundeck/test/unit/Native.hs b/services/gundeck/test/unit/Native.hs index 2e525f7cf1f..5b0d4daf1b9 100644 --- a/services/gundeck/test/unit/Native.hs +++ b/services/gundeck/test/unit/Native.hs @@ -28,11 +28,11 @@ import Data.Text.Encoding qualified as T import Data.Text.Lazy.Encoding qualified as LT import Gundeck.Push.Native.Serialise import Gundeck.Push.Native.Types -import Gundeck.Types.Push import Imports import Test.Tasty import Test.Tasty.QuickCheck import Wire.API.Internal.Notification +import Wire.API.Push.V2 tests :: TestTree tests = diff --git a/services/gundeck/test/unit/ParseExistsError.hs b/services/gundeck/test/unit/ParseExistsError.hs index 1c370be3eed..02ae7fd1408 100644 --- a/services/gundeck/test/unit/ParseExistsError.hs +++ b/services/gundeck/test/unit/ParseExistsError.hs @@ -3,10 +3,10 @@ module ParseExistsError where import Amazonka.Types import Gundeck.Aws import Gundeck.Aws.Arn -import Gundeck.Types.Push.V2 (Transport (APNS)) import Imports import Test.Tasty import Test.Tasty.HUnit +import Wire.API.Push.V2 (Transport (APNS)) tests :: TestTree tests = diff --git a/services/gundeck/test/unit/Push.hs b/services/gundeck/test/unit/Push.hs index 3214c72bdca..69c4b777cb6 100644 --- a/services/gundeck/test/unit/Push.hs +++ b/services/gundeck/test/unit/Push.hs @@ -21,9 +21,8 @@ module Push where import Data.Aeson qualified as Aeson -import Gundeck.Push (pushAll, pushAny) +import Gundeck.Push (pushAll) import Gundeck.Push.Websocket as Web (bulkPush) -import Gundeck.Types import Imports import MockGundeck import Test.QuickCheck @@ -31,6 +30,8 @@ import Test.QuickCheck.Instances () import Test.Tasty import Test.Tasty.QuickCheck import Wire.API.Internal.Notification +import Wire.API.Presence +import Wire.API.Push.V2 tests :: TestTree tests = @@ -82,11 +83,8 @@ pushAllProp env (Pretty pushes) = where ((), realst) = runMockGundeck env (pushAll pushes) ((), mockst) = runMockGundeck env (mockPushAll pushes) - (errs, oldst) = runMockGundeck env (pushAny pushes) props = [ (Aeson.eitherDecode . Aeson.encode) pushes === Right pushes, (Aeson.eitherDecode . Aeson.encode) env === Right env, - counterexample "real vs. mock:" $ realst === mockst, - counterexample "real vs. old:" $ realst === oldst, - counterexample "old errors:" $ isRight errs === True + counterexample "real vs. mock:" $ realst === mockst ] diff --git a/services/gundeck/test/unit/ThreadBudget.hs b/services/gundeck/test/unit/ThreadBudget.hs index 7715d8c8a7c..727bae4a8a7 100644 --- a/services/gundeck/test/unit/ThreadBudget.hs +++ b/services/gundeck/test/unit/ThreadBudget.hs @@ -30,7 +30,6 @@ import Control.Concurrent.Async import Control.Lens import Control.Monad.Catch (MonadCatch, catch) import Data.String.Conversions -import Data.Time import GHC.Generics import Gundeck.Options import Gundeck.ThreadBudget.Internal @@ -53,19 +52,12 @@ newtype NumberOfThreads = NumberOfThreads {fromNumberOfThreads :: Int} -- | 'microseconds' determines how long one unit lasts. there is a trade-off of fast -- vs. robust in this whole setup. this type is supposed to help us find a good sweet spot. +-- +-- There is also `Milliseconds` (with small `s` after `Milli`) in "Data.Misc". maybe this +-- should be cleaned up... newtype MilliSeconds = MilliSeconds {fromMilliSeconds :: Int} deriving (Eq, Ord, Show, Generic, ToExpr) --- toMillisecondsCeiling 0.03 == MilliSeconds 30 --- toMillisecondsCeiling 0.003 == MilliSeconds 3 --- toMillisecondsCeiling 0.0003 == MilliSeconds 1 --- toMillisecondsCeiling 0.0000003 == MilliSeconds 1 -toMillisecondsCeiling :: NominalDiffTime -> MilliSeconds -toMillisecondsCeiling = MilliSeconds . ceiling . (* 1000) . toRational - -milliSecondsToNominalDiffTime :: MilliSeconds -> NominalDiffTime -milliSecondsToNominalDiffTime = fromRational . (/ 1000) . toRational . fromMilliSeconds - instance Arbitrary NumberOfThreads where arbitrary = NumberOfThreads <$> choose (1, 30) shrink (NumberOfThreads n) = NumberOfThreads <$> filter (> 0) (shrink n) @@ -112,9 +104,6 @@ instance LC.MonadLogger (ReaderT LogHistory IO) where delayms :: (MonadCatch m, MonadIO m) => MilliSeconds -> m () delayms = delay' . (* 1000) . fromMilliSeconds -delayndt :: (MonadCatch m, MonadIO m) => NominalDiffTime -> m () -delayndt = delay' . round . (* 1000) . (* 1000) . toRational - delay' :: (MonadCatch m, MonadIO m) => Int -> m () delay' microsecs = threadDelay microsecs `catch` \AsyncCancelled -> pure () @@ -146,14 +135,15 @@ tests = "thread budgets" [ -- flaky test case as described in https://wearezeta.atlassian.net/browse/BE-527 -- testCase "unit test" testThreadBudgets, - testProperty "qc stm (sequential)" propSequential + testProperty "qc stm (sequential)" propSequential, + testCase "thread buckets" testThreadBudgets ] ---------------------------------------------------------------------- -- deterministic unit test -_testThreadBudgets :: Assertion -_testThreadBudgets = do +testThreadBudgets :: Assertion +testThreadBudgets = do let timeUnits n = MilliSeconds $ lengthOfTimeUnit * n lengthOfTimeUnit = 5 -- if you make this larger, the test will run more slowly, and be -- less likely to have timing issues. if you make it too small, some of the calls to diff --git a/services/integration.yaml b/services/integration.yaml index b33259f873a..174d6db1477 100644 --- a/services/integration.yaml +++ b/services/integration.yaml @@ -129,12 +129,30 @@ dynamicBackends: dynamic-backend-1: domain: d1.example.com federatorExternalPort: 10098 + mlsPrivateKeyPaths: + removal: + ed25519: "test/resources/dynBackend1/ed25519.pem" + ecdsa_secp256r1_sha256: "test/resources/dynBackend1/ecdsa_secp256r1_sha256.pem" + ecdsa_secp384r1_sha384: "test/resources/dynBackend1/ecdsa_secp384r1_sha384.pem" + ecdsa_secp521r1_sha512: "test/resources/dynBackend1/ecdsa_secp521r1_sha512.pem" dynamic-backend-2: domain: d2.example.com federatorExternalPort: 11098 + mlsPrivateKeyPaths: + removal: + ed25519: "test/resources/dynBackend2/ed25519.pem" + ecdsa_secp256r1_sha256: "test/resources/dynBackend2/ecdsa_secp256r1_sha256.pem" + ecdsa_secp384r1_sha384: "test/resources/dynBackend2/ecdsa_secp384r1_sha384.pem" + ecdsa_secp521r1_sha512: "test/resources/dynBackend2/ecdsa_secp521r1_sha512.pem" dynamic-backend-3: domain: d3.example.com federatorExternalPort: 12098 + mlsPrivateKeyPaths: + removal: + ed25519: "test/resources/dynBackend3/ed25519.pem" + ecdsa_secp256r1_sha256: "test/resources/dynBackend3/ecdsa_secp256r1_sha256.pem" + ecdsa_secp384r1_sha384: "test/resources/dynBackend3/ecdsa_secp384r1_sha384.pem" + ecdsa_secp521r1_sha512: "test/resources/dynBackend3/ecdsa_secp521r1_sha512.pem" rabbitmq: host: localhost @@ -183,4 +201,43 @@ federation-v0: host: 127.0.0.1 port: 21091 +federation-v1: + originDomain: federation-v1.example.com + brig: + host: 127.0.0.1 + port: 22082 + cannon: + host: 127.0.0.1 + port: 22083 + cargohold: + host: 127.0.0.1 + port: 22084 + federatorInternal: + host: 127.0.0.1 + port: 22097 + federatorExternal: + host: 127.0.0.1 + port: 22098 + galley: + host: 127.0.0.1 + port: 22085 + gundeck: + host: 127.0.0.1 + port: 22086 + nginz: + host: 127.0.0.1 + port: 22080 + spar: + host: 127.0.0.1 + port: 22088 + proxy: + host: 127.0.0.1 + port: 22087 + backgroundWorker: + host: 127.0.0.1 + port: 22089 + stern: + host: 127.0.0.1 + port: 22091 + integrationTestHostName: "localhost" diff --git a/services/nginz/integration-test/conf/nginz/integration-ca-key.pem b/services/nginz/integration-test/conf/nginz/integration-ca-key.pem index 812d4ddc4a1..1017ec966b5 100644 --- a/services/nginz/integration-test/conf/nginz/integration-ca-key.pem +++ b/services/nginz/integration-test/conf/nginz/integration-ca-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCUJVDizZney5e4 -rJHEp7L/cXMf/k5zMKrapCte8OePyraPjSSQJA9S/l+RJEj2exQyqrdmxn6BE1hp -5aR/87FE1KXIZX+iX8OkyTno0Hq+RUDSwrUEpziHIZQBSsxcYRkr3Dq0VogQD401 -SJL/XogjV3CeTxPUpPPm5p9s7c+y1R3L+Oj5k//ooUcSidNU+QrF1j1BzmUEXxry -mZshn/okwboXMTD2Ap12+/zN62AkFy/BUGYmqmyFTKjUaTAVZAKlN5diZ0q5TSIy -IejaqMI6IPEGBriM+n8cFpMOhFOWQcByK9qzmpAp0KCnl1agSXVmTUTF9Vbanoez -vcw6FuP9AgMBAAECggEACB7IgXoMEFiAAz0gS1N23gYRraQCmFFHWC8t+mkBhFHz -8kfmBGmZlm6/fcTro+kIqSNO5LkGF5ygGMPf4ayRn6h5QtP/bD7MCkUGYdLFm5bP -sA3AntXspQmL44s+SuT+nHcYl6hzkk/L6WsGNa2wkCFbmK3UdDArd1FWVUHuw8pR -2s2V1KpVR6/3Wdw86l3khcDbY3CHimenmGSxxjFPixHMpcni3cTPdnULo+vZT3fh -MMmsRMwQvcZXNFtUjzwelx+/e0MB+AyoEYPaKa+afKKQBxlVmldrn9q/m3++fkiT -PWLg4yNcG+M+78vldoJb3kHANYCNxn438LDUrgNvAQKBgQDGNoSjC6Zmt7OwmO0H -kszLTbzbtNBmV5aFNRtopSL5H/DcMpq1MUXxsCpEK8cRHlbDLaEV/lrADjFN7KNg -Hvy0B77iiHGLm2rB6psZpSafapFjFC24q0VKS95Z6UyTIUiajIj2aYEPz0HOrgFC -lw2Ba7VTV2OxWUegVLoxbaV2/QKBgQC/VhGUf53klmi2XEfh5X+CtvH6v5P48VyZ -8P8e4PcZVBvgAbuMPMT+EW6+46J73GMJ2ISs0kDZEge0k+RRzUVqWvUlBWV9nt04 -BUGZT//w8bqD8Dfo1TeRwiLYuYMUNWaAdYvs0nt49dFpX5hyd+KUB+A5v1QbjTSY -PQT3yscxAQKBgQCE4DteigrNRU0ikAImV5UOnViD+NzUHtd7CTUMm9esJmtzUkFA -Qn3fHffXp3lV0n7bbRVWByOTKHCJCqAjaeKCVcbzWgC0VEXnJX1AXeRcbjZ0syxL -ZhWXTvEKWUnKQD/Jy3htqCCrFofJJAEYQOb+4dO2wRjF5VIM+3+ubxDDiQKBgFIn -tqy4jydTneqPfR312OZbf1NXZ0YA/O3smN69YdwyTTXGCK2SelNNUOwN+fqNCslz -eqRqMwYBw+U5i1PEfAXKwHAA/S8PQ5WGTEB0JUVjxd5ZCuiihJXFcgj0vt+yfiyy -TD6HshSiGCTSszaTW2qMZy7khEzAONEVgkiTfSwBAoGAb48KvxQtxW+2RXkNWzMv -D7DyHm9jTTcTARTf7WtY0KMWQa//MPWofieD6KdzRd65lea2Z8wX5vcPVIEUp803 -zQrZMeLTcAQjsTsSP3qBWBi8F/Vd3JKc++F9+7dNfMEhN/fElxDqFrMbXeWtn/Xr -meIImb/2qCWt45/YjQGL8Do= +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCZHfooV9E6ekRu +FHvZEBb0xxctgy/KwvLveeocte//bE8iHDJEzrWTHsPrmvmazc19cdVj+AU+UWUA +P8MuoqBKT+QVRcXvbHZlamGsA/Quflyk3pBBRd4CNDCBmeFbptoXVDh08A2rQIHY +qrAcLgsRzObXqyLc2UrK5GBZQISEpjPQrFBTkfta6qTDVkUA+c82dEpOR3sjNf/b +B75K6tuK6iqpINP7kTkxzMna5rkgKxubSqpHW9s6CcDrPLKJq2kcwdSTpuf4v96Q +k+Yga+YH/HixrW9hrl4Jp0yNdvUdmvi24n6kwt9YyDIP8jXxblODHVBvcBOddoIY +V2SPh6HPAgMBAAECggEAAKZ48SzrouTxX3T6JazaJsX7BOLMFCoJysHoyvXBhC3b +PtjXJHsAQxSxXlHKgkF3gEiN0HPgNH3iAO3vD/FO3vdB5Q6RIvgsnzekHI0+aegi +z+xwuRDpOsx/8ZJMEQ3qBTQCsrRw9RY8DEXao56qcLPpvbwTVNQeFjMsGZTwOyP7 +4IjnYTwt+YrH4UNZPlBMm6ph/1qukTYqdBK2dEKXwxzAvpo+JPQ6SgcKfU3uHjmN +PofzM39l3+ZVoG7WdHEcqN/0tyFz/q/pQMOveyjRFn6fa7HDBEVrOwTKETBlJyFN +nVgMCFqwL2bltu1NNs90w9+q+6VoNME9Wlk3CUuGSQKBgQDRC7jwSvtA0sWOKft6 +QdEK5s8HEP1RUuNBUOETY6jo6I3HBPLWk1W2CiUtDJxMsKdz5QCrHp//LmB9gULk +bZ4tfEXQbbBOYnlbo2cM6VTBf36znnlosHFgF+NqBiYin2cbkkOMvJTa01efvkq9 +02raQNLMP0z4GfZwnDUdNk/85QKBgQC7glD2cJ7QP7VouVYIrmk42LLBfQxOFsbx +NptMf+UID1ayG3Qf3tiRVGtaQ/DRd3uPG+Wuumy5bxwOSCF1P7UmLY+1Rpz2DBda +JGrcHfuhBxI8WineMUwqnDWMtEPwDi9/9WlAoyXbWw4MzwiHCXgPfV7iBXgrAC1/ +ULB81rVsowKBgQCLSUdBfIRqzcVqExkHfeEeZWmeKLjQvezD8XL2q1m5TnJhIC/5 +vxPGBn58xMFD7BS3COfoHLC4o5sRJNaAQ3W4kuwlk2B86eo4n+ii1rltcFjor3fv +xFjWkTQqycwRF6ro2Qz/Mgvwvg7NVkqQrtSsdbK++pJ7YTkuETbmrvCe7QKBgQCf +SXfvsgInlEdOPEtKuqbmRKet2MWwPIcp+CJ7HRZ5/1W9nbbLMCq3YoiDuL2Fo8OR +8bfu861S5YFm3H2XtdP0J7Yx31eNaP4ZdGBWtx3AUFp8bHeuqiAy/lo7OhOQhOxy +/g44e5+4NSS9Ws66sB+OwQjuZokLtm3v/qK+mkKqkwKBgH9ostvH6Iqy9BRZ0hG7 +/EhQAGB2zekJOaOnMqYElzL1PY53SyVv/7jre3OwhUQLE1O3JQGTaj48/5EPeBUC +0jFg5AUM7nD53pbCtGlaKhbBB27lCN/tGKK5OVx4nRw0uRPGiJJ3ZXQps+lE6ZTa +AtzSX1cRPfA/Ff/lB3RR9I5Y -----END PRIVATE KEY----- diff --git a/services/nginz/integration-test/conf/nginz/integration-ca.pem b/services/nginz/integration-test/conf/nginz/integration-ca.pem index 304fc892245..6a33fa9e2c3 100644 --- a/services/nginz/integration-test/conf/nginz/integration-ca.pem +++ b/services/nginz/integration-test/conf/nginz/integration-ca.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- -MIIDEzCCAfugAwIBAgIUQ35aUV70pJjvDTbfgFUj5YmchHQwDQYJKoZIhvcNAQEL -BQAwGTEXMBUGA1UEAwwOY2EuZXhhbXBsZS5jb20wHhcNMjQwNjE3MTMxNTMxWhcN -MzQwNjE1MTMxNTMxWjAZMRcwFQYDVQQDDA5jYS5leGFtcGxlLmNvbTCCASIwDQYJ -KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJQlUOLNmd7Ll7iskcSnsv9xcx/+TnMw -qtqkK17w54/Kto+NJJAkD1L+X5EkSPZ7FDKqt2bGfoETWGnlpH/zsUTUpchlf6Jf -w6TJOejQer5FQNLCtQSnOIchlAFKzFxhGSvcOrRWiBAPjTVIkv9eiCNXcJ5PE9Sk -8+bmn2ztz7LVHcv46PmT/+ihRxKJ01T5CsXWPUHOZQRfGvKZmyGf+iTBuhcxMPYC -nXb7/M3rYCQXL8FQZiaqbIVMqNRpMBVkAqU3l2JnSrlNIjIh6Nqowjog8QYGuIz6 -fxwWkw6EU5ZBwHIr2rOakCnQoKeXVqBJdWZNRMX1Vtqeh7O9zDoW4/0CAwEAAaNT -MFEwHQYDVR0OBBYEFHNgZ4nZQoNKnb0AnDkefTXxxYDqMB8GA1UdIwQYMBaAFHNg -Z4nZQoNKnb0AnDkefTXxxYDqMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL -BQADggEBAIuLuyF7m1SP6PBu29jXnfGtaGi7j0jlqfcAysn7VmAU3StgWvSatlAl -AO6MIasjSQ+ygAbfIQW6W2Wc/U+NLQq5fRVi1cnmlxH5OULOFeQZCVyux8Maq0fT -jj4mmsz62b/iiA4tyS5r+foY4v1u2siSViBJSbfYbMp/VggIimt26RNV2u/ZV6Kf -UrOxazMx1yyuqARiqoA3VOMV8Byv8SEIiteWUSYni6u7xOT4gucPORhbM1HOSQ/S -CVq95x4FeKQnbEMykHI+bpBdkoadMVtrjCbskU49mOrvl/pli9V44R8KK6C1Nv3E -VLLcoOctdw90aT3sIjaXBcZtDTE6p6g= +MIIDEzCCAfugAwIBAgIUEfjIXW9tD1WgwNHJ+kC3r6Cmv5swDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAwwOY2EuZXhhbXBsZS5jb20wHhcNMjQwOTAzMTIwMzM3WhcN +MzQwOTAxMTIwMzM3WjAZMRcwFQYDVQQDDA5jYS5leGFtcGxlLmNvbTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJkd+ihX0Tp6RG4Ue9kQFvTHFy2DL8rC +8u956hy17/9sTyIcMkTOtZMew+ua+ZrNzX1x1WP4BT5RZQA/wy6ioEpP5BVFxe9s +dmVqYawD9C5+XKTekEFF3gI0MIGZ4Vum2hdUOHTwDatAgdiqsBwuCxHM5terItzZ +SsrkYFlAhISmM9CsUFOR+1rqpMNWRQD5zzZ0Sk5HeyM1/9sHvkrq24rqKqkg0/uR +OTHMydrmuSArG5tKqkdb2zoJwOs8somraRzB1JOm5/i/3pCT5iBr5gf8eLGtb2Gu +XgmnTI129R2a+LbifqTC31jIMg/yNfFuU4MdUG9wE512ghhXZI+Hoc8CAwEAAaNT +MFEwHQYDVR0OBBYEFFqlhDsVqlH8UUKGOtCDE9xmYL3hMB8GA1UdIwQYMBaAFFql +hDsVqlH8UUKGOtCDE9xmYL3hMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBADQyh0k3xhcrNOqhvzAK1/A3TY5Hq1FE/a17Yiq8DIiLCJG/nAN60GBx +m7zuL8xoRJ4ylwIswa4z4rHj9p6M6tIbi2tTfJsbyB+FjyFRWoBmTngqNCiw7QUR +/ofSliuEu/YIjphR8LmTBvy4fVccTwXDaBPEGf2iN+DFmryLHxVpsVh3AA0uUSy0 +e2bZJLRwv1z0saC5KGHpWb6RJbAP2nRw5omcorMtP1KW8XyVESiJm7hDZAx6VLgD +k4GcEOUEq9CJs9UVAkIIDS87CfppHZEGPDK3Ufro5AhIwA3hpSJkTgzkf0TcQKIr +4E/zJSeTld4rMC26ghWodIwGRyofTP0= -----END CERTIFICATE----- diff --git a/services/nginz/integration-test/conf/nginz/integration-leaf-key.pem b/services/nginz/integration-test/conf/nginz/integration-leaf-key.pem index 1e7a83068de..e4ee0a09ab1 100644 --- a/services/nginz/integration-test/conf/nginz/integration-leaf-key.pem +++ b/services/nginz/integration-test/conf/nginz/integration-leaf-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCZjOHeUnlauuxD -WgrRnh3hj5Fs+uh9vyddMX8rSWJIbWFw4QuYzYKY8CQa3MBb6qK1uUwoJ0W1w47I -RgA5VLvGxI+T1wX8E5vljVgfT3CAXHKRB88NrT8A1urQnWpzlq5sNerL6dqgBrjG -QBmFF7NxrvjGgerC2D8+srWfpQ6Jbl9by8c3JDu+T79PM+pW9ycUgdF1AJQBTz9K -zNQ7ZTlBQvJG8WhTMKioJgQsE60oEXD0C8M5yKBBb7DrqkeZInXqCw2y7DZLWzog -D+jgoAD5/9sk3d/gGNqDibzjjwMiJnH/IqBTkZsQ9OdZZPfx5v/p062hQBlM656P -2jMpJ1xxAgMBAAECggEAS3NBjWgDP4T4EUROaqACWNKeB+nmkdt68T0gGtoNVD+D -EN9UPnpFQPdHFngAgWnzF858UIKzq1Pzdg+HjqRHPK1bS67tvua3xP1GHuR/CGPk -28T1hefqPHRen7GqHDAfdwarYBWCGv4Sjz/yCkcSIrtyfMBb5fAya5GO02pckUSK -19sl7XhkPtHJVirRkjQL29R2TCpkNNpQMjkuYLk7mox+6pNTbxgbk0cnT3eGj1pV -mlPqpwzC5GevRziE/VE/WXFLChY+8KB4fDLRqWnyvabDvQ4coaXgzwbdScJyM5hX -+Dxdfni/P2m7xAZXUyfBsr0VUzqUkJfK3WWvvAGTDQKBgQDNi3RUEjVnU/MN4aDz -iZB2VYGfo/K69xTPNEbLQWs1F4ZMpHVtUVXzTfx/xG9ug989ijEm6ncL9OsnhThn -UldSz2ojSJUxLmhgCHZGYHT72v/9rEqfT9JisWpIj44KXufUHCcl3Cozj1ae3EUp -NVhN1HphB2LsCIJvLYfLIGdBNwKBgQC/PhHQMm/MQe4pOHAbdzDrRZWdG2KSRVxp -9mmJ/aT8LOp7BDjq+Dkct6a56JGqlOTeJirMTTmCKiOiTInuB9S+K7kWJJiYg9g4 -UCiuMU+40Px/1Z4/uxRj3DSdGLXG7S6kPeADx9f9BUNpAytGqOnSnfbDiDVvQVbp -0N0+nIXDlwKBgQC2uZOXrXxGOE4pd/ySpCeF2yvZ1HDTnxWjwlBxHt4Em74rYkR2 -A0mKezjOCL4bHCaYWcKqWuOsAHYQcxEaYQv6NSOg7ESdLSlivgMPO26j+yN5yvGn -wNlCHYBjsyLNu2MSoFh5AsmNfo69uQnOwXqX7h1BJsTdGg+CcJJ4lHzWbwKBgQCD -/CRzGbwKrh3eGPNWIUaDuTxudy3qYTBMeSGReJpa5+zUBa/6imFwLldEyvttTOE/ -Z/v1j/52lPqO0mAHBSSQMsDERXGDIMsi4j+RKLsqhCEfYKCcv1JtMNam7RzXM24T -MBjgwxWPrAg/+03ssDrffuGFRQYLyH5hVCK9SW0P9QKBgQDJ1ZSto+RWxv/uOKNr -7FYeQoKpMb2IvNvnGlnYHC8KS9qRq6wUE+FtuKcdLBQP4M9Cgq71VD/dsawrhEw7 -1rAYk3OqmHxBOU5Dcb152NxYHEf53pfEfWc0x4AEVe+Jzynj2EYixRKNWwODNTEx -LKJOYd0CuWywxg6d9G7A7XbgWQ== +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDPYxemHc21P8y2 +DIDNsuUzVpnc1bj81QIQ5a2aFSVtQFr3Dhpo0XXyu0KHKR64ZelmW33Yoe2J0zwl +R66fg07whBcZspsonXmsUB5uFFF/Dcv9shBNcg67jjynXPDvJK2sC37GCar5ar/v +11v/+Iq6LNerC/OqMJlMYyFDif1BvESbnw+9hsihUT9qK5s6md5krEV1/ro0Noh7 +kBgflylEKjapfCrVMnAMgaMV55jrk5BTpoR6KCDuDmXPk3Ed304Op/krGazfGB2j +1rz3IPfKOOtIgxn8n5PDYmShVUw1R3Rps1LeEyhsH1EGuAvVx1mKhB3QGMqFtIfo +wOpkgZPdAgMBAAECggEAVMurFUB1ZkkmXj9hgPnHLoUX10xJ3ZMIy7jlkS1ZRsD8 +EK0jDj2q0OtRSet9xJ7i3nfFTojzE5obqxCSrWUmp0ATI+4789DjuZluv8quAdm1 +0U73zHq43GZNlY7yco2YN1Lh7H5yepXz0dDILLLGolYIfscdw7YoUCvuI2vt8eyY +FaMkOi8Bs32Abn+53MhaNkEOVemmwP/u2rD81IY7pXQtF+1XfSxYBipHCB7phC1g +dr6ITU9CoF3SXvjYL6uP/7W1Du/CuaDSijVZJkBfQSgbkQbBr6pAovumtpNZgZvO +bixqS4oTZqsd+Rgl3YBOpx+JbuUFL0fG+uQBv1yZbwKBgQD8n7s1w/ouUGwW+qGi +4EFTYVDFD2Rgvg3oFPHt72zreaNXSy8YWrK9Mas/Fzu5knSE3f3O0S1WvqWdt/z6 +uPDLTpg9fIWX6v+hPX09F5ekeLlUDBazBT1PQUD0qd8PiyFNB1F+ffSPTxmKScjc +hTqQCtnun6rlICalWO9VvGt6QwKBgQDSKJkjxIGLtXBaXfMQiF9dQDrMk2it5grl +w0OnpPhYvpdp+Cfi01kMUrnfHwF0v92BqeqoKZo4DwJXkrmwf5kNnftxmRqbk9gE +dJq/E/6SELyT/chtzXfxC/wTmyyxhfZUJvJUxlaZ6KP/86t8A22DJcFO2Z85iUGH +8zy2UJUnXwKBgBvSs9m+FeXX8a+uNvMrY8Z9J1os0c9d30Y6WFLuVb6xjO3mV++E +vb7co5G1S1yq5q5jjLqkiyvMn4z5YKF0kQCzTU0oU8ZhmXn2vb5mxMrWiQLauf1J +jHEYLMFFnE2n8yj6r10RHkhSW+vBKKAxBDwtFceUSkwl+FupqeJ1eBjlAoGBAMou ++LWqdZ89HSwzOobrTCPgiTELmCfFKzLE2q/MTIjEQ9NVRLo57m+mnt+DatkxRR9b +oz/JVm8cMXqi1DZza4HoPWGalDic0bPnooC18bIAnAwcmdjZVcz3ZLpQDX10jfmD +xpu8fNBxOmYhvRcADTmg9wqu3zpxTDRI1F3pxLUtAoGAGfsX4bve5cLm49Oa1p0H +kEErLMuAMIKQNVsbzVELepLYr+uwEXBCXyyoIf79ABDvUHbzxMEwgANuet/4PQzS +yB1qzFk6GDvqZ5dfPUgMUWH9wvD1qEGp6yxkyESGt8CNwnu8GI50NAeSh2/JeUIa +r/u+m2vnJjOXpJdOJ+7f6yM= -----END PRIVATE KEY----- diff --git a/services/nginz/integration-test/conf/nginz/integration-leaf.pem b/services/nginz/integration-test/conf/nginz/integration-leaf.pem index 635d332de70..abd724df6b1 100644 --- a/services/nginz/integration-test/conf/nginz/integration-leaf.pem +++ b/services/nginz/integration-test/conf/nginz/integration-leaf.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- MIIDQTCCAimgAwIBAgIBADANBgkqhkiG9w0BAQsFADAZMRcwFQYDVQQDDA5jYS5l -eGFtcGxlLmNvbTAeFw0yNDA2MTcxMzE1MzFaFw0yNDA3MTcxMzE1MzFaMAAwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCZjOHeUnlauuxDWgrRnh3hj5Fs -+uh9vyddMX8rSWJIbWFw4QuYzYKY8CQa3MBb6qK1uUwoJ0W1w47IRgA5VLvGxI+T -1wX8E5vljVgfT3CAXHKRB88NrT8A1urQnWpzlq5sNerL6dqgBrjGQBmFF7NxrvjG -gerC2D8+srWfpQ6Jbl9by8c3JDu+T79PM+pW9ycUgdF1AJQBTz9KzNQ7ZTlBQvJG -8WhTMKioJgQsE60oEXD0C8M5yKBBb7DrqkeZInXqCw2y7DZLWzogD+jgoAD5/9sk -3d/gGNqDibzjjwMiJnH/IqBTkZsQ9OdZZPfx5v/p062hQBlM656P2jMpJ1xxAgMB +eGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzhaFw0zNDA5MDExMjAzMzhaMAAwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPYxemHc21P8y2DIDNsuUzVpnc +1bj81QIQ5a2aFSVtQFr3Dhpo0XXyu0KHKR64ZelmW33Yoe2J0zwlR66fg07whBcZ +spsonXmsUB5uFFF/Dcv9shBNcg67jjynXPDvJK2sC37GCar5ar/v11v/+Iq6LNer +C/OqMJlMYyFDif1BvESbnw+9hsihUT9qK5s6md5krEV1/ro0Noh7kBgflylEKjap +fCrVMnAMgaMV55jrk5BTpoR6KCDuDmXPk3Ed304Op/krGazfGB2j1rz3IPfKOOtI +gxn8n5PDYmShVUw1R3Rps1LeEyhsH1EGuAvVx1mKhB3QGMqFtIfowOpkgZPdAgMB AAGjgawwgakwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEgGA1UdEQEB /wQ+MDyCGSouaW50ZWdyYXRpb24uZXhhbXBsZS5jb22CFGhvc3QuZG9ja2VyLmlu -dGVybmFsgglsb2NhbGhvc3QwHQYDVR0OBBYEFPowAfmLPCmdCMdSxQjsR6UQSoyH -MB8GA1UdIwQYMBaAFHNgZ4nZQoNKnb0AnDkefTXxxYDqMA0GCSqGSIb3DQEBCwUA -A4IBAQCMJwbLzUsrkQkgdGKVi/Mb5XAAV0sfkwZch1Fx0vhJI072cZSow5A2ZUHa -LScFNTPmilPKEr6MS4xIKtRQaMHInbfxSsyNViKhpzkSOKoAiJjIJ2xPKFPnbTDI -uV74nxxyf9q/p3SLQfJFk7fxbvNeLqg5bYSrMeklHj4bpMJ9fybS8/mZVc8AkTFK -fsXSu9CW1B3GF+jP3E2GrFF3Zh9MgvWjMlSYg4ljPf5FoMCUq6GmQ17hQeJFvb5h -Jqk6TcgUrp082bcVlPW17XzFwVe3n6uzvWMtwI62EztVUj98+YkBiFL3i4+OQwAU -/noc22fq20OyJtCPJY4FIK7xUcgD +dGVybmFsgglsb2NhbGhvc3QwHQYDVR0OBBYEFBkpAu3ILiU4gtEYffAU7zxGHPC6 +MB8GA1UdIwQYMBaAFFqlhDsVqlH8UUKGOtCDE9xmYL3hMA0GCSqGSIb3DQEBCwUA +A4IBAQBB1VsthdoVT9ExXkfKixotbXm6+eBgYenK1R5Qx/UX3JrlI1nF/8rKMg5e +7QfMCydSJwVEQdvnXD3ddVhUTYRActQvnJwWTyXfeiezrfDCTLu4SNpLOP7ojFlq +9ZX/E9GC0axTIUmEIy8YIC3JJ2PAlvw9qMzrsivyAgbof3NX+9XXKfwZHBwSLsO1 +Gxr9zkL+U/qww7TvyJD1LqBR0UEd9pZriorpVVFAa/JlFQX5ip1Smcd6m97nq20N +qpUIalra+K6qHxjHVwA2UxVgbO9bLFIBmp9pNvSm+5umAKkmqFnHRNAHfCy/IFGl +3fw8u9mXJ8LzUR4tiS0cVb6bwQzd -----END CERTIFICATE----- diff --git a/services/nginz/integration-test/conf/nginz/integration.conf b/services/nginz/integration-test/conf/nginz/integration.conf index baae352c92a..c89469d51ff 100644 --- a/services/nginz/integration-test/conf/nginz/integration.conf +++ b/services/nginz/integration-test/conf/nginz/integration.conf @@ -7,7 +7,7 @@ listen 8081; # port. # This port is only used for trying out nginx http2 forwarding without TLS locally and should not # be ported to any production nginz config. -listen 8090 http2; +listen 8090; ######## TLS/SSL block start ############## # @@ -15,5 +15,7 @@ listen 8090 http2; # But to also test tls forwarding, this port can be used. # This applies only locally, as for kubernetes (helm chart) based deployments, # TLS is terminated at the ingress level, not at nginz level -listen 8443 ssl http2; -listen [::]:8443 ssl http2; +listen 8443 ssl; +listen [::]:8443 ssl; + +http2 on; diff --git a/services/nginz/integration-test/conf/nginz/nginx.conf b/services/nginz/integration-test/conf/nginz/nginx.conf index 41be5df60bf..bfbc75ccc7c 100644 --- a/services/nginz/integration-test/conf/nginz/nginx.conf +++ b/services/nginz/integration-test/conf/nginz/nginx.conf @@ -155,6 +155,11 @@ http { # FUTUREWORK(federation): are any other settings # (e.g. timeouts, body size, buffers, headers,...) # useful/recommended/important-for-security?) + } + + location /api-version { + include common_response_no_zauth.conf; + proxy_pass http://brig; } # Brig Endpoints @@ -201,65 +206,75 @@ http { proxy_pass http://brig; } - location /activate { + location ~* ^(/v[0-9]+)?/activate { include common_response_no_zauth.conf; proxy_pass http://brig; } - location /login { + location ~* ^(/v[0-9]+)?/login { include common_response_no_zauth.conf; proxy_pass http://brig; } + location ~* ^(/v[0-9]+)?/teams/invitations/accept$ { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + location ~* ^(/v[0-9]+)?/teams/invitations/([^/]*)$ { include common_response_no_zauth.conf; proxy_pass http://brig; } - location /verification-code/send { + location ~* ^(/v[0-9]+)?/verification-code/send { include common_response_no_zauth.conf; proxy_pass http://brig; } ## brig authenticated endpoints - location ~* ^(/v[0-9]+)?/self$ { + location ~* ^(/v[0-9]+)?/self { include common_response_with_zauth.conf; oauth_scope self; proxy_pass http://brig; } - location /users { + location ~* ^(/v[0-9]+)?/users { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^(/v[0-9]+)?/handles { include common_response_with_zauth.conf; proxy_pass http://brig; } - location /list-users { + location ~* ^(/v[0-9]+)?/list-users { include common_response_with_zauth.conf; proxy_pass http://brig; } - location /search { + location ~* ^(/v[0-9]+)?/search { include common_response_with_zauth.conf; proxy_pass http://brig; } - location /list-connections { + location ~* ^(/v[0-9]+)?/list-connections { include common_response_with_zauth.conf; proxy_pass http://brig; } - location ~* ^/teams/([^/]+)/search$ { + location ~* ^(/v[0-9]+)?/teams/([^/]+)/search$ { include common_response_with_zauth.conf; proxy_pass http://brig; } - location ~* /teams/([^/]+)/services { + location ~* ^(/v[0-9]+)?/teams/([^/]+)/services { include common_response_with_zauth.conf; proxy_pass http://brig; } - location /connections { + location ~* ^(/v[0-9]+)?/connections { include common_response_with_zauth.conf; proxy_pass http://brig; } @@ -274,17 +289,17 @@ http { proxy_pass http://brig; } - location /properties { + location ~* ^(/v[0-9]+)?/properties { include common_response_with_zauth.conf; proxy_pass http://brig; } - location /calls/config { + location ~* ^(/v[0-9]+)?/calls/config { include common_response_with_zauth.conf; proxy_pass http://brig; } - location ~* ^/teams/([^/]*)/size$ { + location ~* ^(/v[0-9]+)?/teams/([^/]*)/size$ { include common_response_with_zauth.conf; proxy_pass http://brig; } @@ -299,39 +314,39 @@ http { proxy_pass http://brig; } - location ~* ^/oauth/clients/([^/]*)$ { + location ~* ^(/v[0-9]+)?/oauth/clients/([^/]*)$ { include common_response_with_zauth.conf; proxy_pass http://brig; } - location ~* ^/oauth/authorization/codes$ { + location ~* ^(/v[0-9]+)?/oauth/authorization/codes$ { include common_response_with_zauth.conf; proxy_pass http://brig; } - location /oauth/token { + location ~* ^(/v[0-9]+)?/oauth/token { include common_response_no_zauth.conf; proxy_pass http://brig; } - location /oauth/revoke { + location ~* ^(/v[0-9]+)?/oauth/revoke { include common_response_no_zauth.conf; proxy_pass http://brig; } - location /oauth/applications { + location ~* ^(/v[0-9]+)?/oauth/applications { include common_response_with_zauth.conf; proxy_pass http://brig; } # Cargohold Endpoints - location /assets { + location ~* ^(/v[0-9]+)?/assets { include common_response_with_zauth.conf; proxy_pass http://cargohold; } - location /bot/assets { + location ~* ^(/v[0-9]+)?/bot/assets { include common_response_with_zauth.conf; proxy_pass http://cargohold; } @@ -348,6 +363,12 @@ http { proxy_pass http://galley; } + location ~* ^(/v[0-9]+)?/one2one-conversations$ { + include common_response_with_zauth.conf; + oauth_scope conversations; + proxy_pass http://galley; + } + location ~* ^(/v[0-9]+)?/conversations$ { include common_response_with_zauth.conf; oauth_scope conversations; @@ -365,62 +386,62 @@ http { proxy_pass http://galley; } - location ~* ^/conversations/([^/]*)/otr/messages { + location ~* ^(/v[0-9]+)?/conversations/([^/]*)/otr/messages { include common_response_with_zauth.conf; proxy_pass http://galley; } - location ~* ^/conversations/([^/]*)/([^/]*)/proteus/messages { + location ~* ^(/v[0-9]+)?/conversations/([^/]*)/([^/]*)/proteus/messages { include common_response_with_zauth.conf; proxy_pass http://galley; } - location ~* ^/conversations/([^/]*)/([^/]*)/protocol { + location ~* ^(/v[0-9]+)?/conversations/([^/]*)/([^/]*)/protocol { include common_response_with_zauth.conf; proxy_pass http://galley; } - location /broadcast { + location ~* ^(/v[0-9]+)?/broadcast { include common_response_with_zauth.conf; proxy_pass http://galley; } - location /bot/conversation { + location ~* ^(/v[0-9]+)?/bot/conversation { include common_response_with_zauth.conf; proxy_pass http://galley; } - location /bot/messages { + location ~* ^(/v[0-9]+)?/bot/messages { include common_response_with_zauth.conf; proxy_pass http://galley; } - location ~* ^/teams$ { + location ~* ^(/v[0-9]+)?/teams$ { include common_response_with_zauth.conf; proxy_pass http://galley; } - location ~* ^/teams/([^/]*)$ { + location ~* ^(/v[0-9]+)?/teams/([^/]*)$ { include common_response_with_zauth.conf; proxy_pass http://galley; } - location ~* ^/teams/([^/]*)/members(.*) { + location ~* ^(/v[0-9]+)?/teams/([^/]*)/members(.*) { include common_response_with_zauth.conf; proxy_pass http://galley; } - location ~* ^/teams/([^/]*)/conversations(.*) { + location ~* ^(/v[0-9]+)?/teams/([^/]*)/conversations(.*) { include common_response_with_zauth.conf; proxy_pass http://galley; } - location ~* ^/teams/([^/]*)/features { + location ~* ^(/v[0-9]+)?/teams/([^/]*)/features { include common_response_with_zauth.conf; proxy_pass http://galley; } - location ~* ^/teams/([^/]*)/features/([^/]*) { + location ~* ^(/v[0-9]+)?/teams/([^/]*)/features/([^/]*) { include common_response_with_zauth.conf; proxy_pass http://galley; } @@ -436,22 +457,22 @@ http { proxy_pass http://galley; } - location ~* ^/teams/([^/]*)/legalhold(.*) { + location ~* ^(/v[0-9]+)?/teams/([^/]*)/legalhold(.*) { include common_response_with_zauth.conf; proxy_pass http://galley; } - location ~* ^/teams/([^/]*)/members/csv$ { + location ~* ^(/v[0-9]+)?/teams/([^/]*)/members/csv$ { include common_response_with_zauth.conf; proxy_pass http://galley; } - location /mls/welcome { + location ~* ^(/v[0-9]+)?/mls/welcome { include common_response_with_zauth.conf; proxy_pass http://galley; } - location /mls/messages { + location ~* ^(/v[0-9]+)?/mls/messages { include common_response_with_zauth.conf; proxy_pass http://galley; } @@ -468,31 +489,31 @@ http { # Gundeck Endpoints - location /push { + location ~* ^(/v[0-9]+)?/push { include common_response_with_zauth.conf; proxy_pass http://gundeck; } - location /presences { + location ~* ^(/v[0-9]+)?/presences { include common_response_with_zauth.conf; proxy_pass http://gundeck; } - location ~* ^(/v[0-9]+)?/notifications$ { + location ~* ^(/v[0-9]+)?/notifications { include common_response_with_zauth.conf; proxy_pass http://gundeck; } # Proxy Endpoints - location /proxy { + location ~* ^(/v[0-9]+)?/proxy { include common_response_with_zauth.conf; proxy_pass http://proxy; } # Cannon Endpoints - location /await { + location ~* ^(/v[0-9]+)?/await { include common_response_with_zauth.conf; proxy_pass http://cannon; @@ -503,12 +524,12 @@ http { # Spar Endpoints - location /sso { + location ~* ^(/v[0-9]+)?/sso { include common_response_no_zauth.conf; proxy_pass http://spar; } - location /identity-providers { + location ~* ^(/v[0-9]+)?/identity-providers { include common_response_with_zauth.conf; proxy_pass http://spar; } diff --git a/services/proxy/default.nix b/services/proxy/default.nix index b6205a6acee..8b689661b9c 100644 --- a/services/proxy/default.nix +++ b/services/proxy/default.nix @@ -9,6 +9,7 @@ , bytestring , case-insensitive , configurator +, errors , exceptions , extended , gitignoreSource @@ -21,6 +22,7 @@ , lib , metrics-wai , retry +, servant-server , text , tinylog , types-common @@ -46,6 +48,7 @@ mkDerivation { bytestring case-insensitive configurator + errors exceptions extended http-client @@ -56,6 +59,7 @@ mkDerivation { lens metrics-wai retry + servant-server text tinylog types-common diff --git a/services/proxy/proxy.cabal b/services/proxy/proxy.cabal index e92831949f6..5da48e93a8b 100644 --- a/services/proxy/proxy.cabal +++ b/services/proxy/proxy.cabal @@ -17,7 +17,7 @@ flag static library exposed-modules: - Proxy.API + Proxy.API.Internal Proxy.API.Public Proxy.Env Proxy.Options @@ -80,6 +80,7 @@ library , bytestring >=0.10 , case-insensitive >=1.2 , configurator >=0.3 + , errors , exceptions >=0.8 , extended , http-client >=0.7 @@ -90,6 +91,7 @@ library , lens >=4.11 , metrics-wai >=0.5 , retry >=0.7 + , servant-server , text >=1.2 , tinylog >=0.12 , types-common >=0.8 diff --git a/services/proxy/src/Proxy/API.hs b/services/proxy/src/Proxy/API.hs deleted file mode 100644 index d3ba31ca4f3..00000000000 --- a/services/proxy/src/Proxy/API.hs +++ /dev/null @@ -1,43 +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 Proxy.API - ( sitemap, - ) -where - -import Imports hiding (head) -import Network.Wai.Predicate (true) -import Network.Wai.Routing (Routes, continue, get, head) -import Network.Wai.Utilities (empty) -import Proxy.API.Public qualified as Public -import Proxy.Env (Env) -import Proxy.Proxy (Proxy) - -sitemap :: Env -> Routes a Proxy () -sitemap e = do - Public.sitemap e - routesInternal - --- | IF YOU MODIFY THIS, BE AWARE OF: --- --- >>> /libs/wire-api/src/Wire/API/Routes/Public/Proxy.hs --- >>> https://wearezeta.atlassian.net/browse/SQSERVICES-1647 -routesInternal :: Routes a Proxy () -routesInternal = do - head "/i/status" (continue $ const (pure empty)) true - get "/i/status" (continue $ const (pure empty)) true diff --git a/services/proxy/src/Proxy/API/Internal.hs b/services/proxy/src/Proxy/API/Internal.hs new file mode 100644 index 00000000000..c7128b0bfdb --- /dev/null +++ b/services/proxy/src/Proxy/API/Internal.hs @@ -0,0 +1,33 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Proxy.API.Internal + ( InternalAPI, + servantSitemap, + ) +where + +import Imports hiding (head) +import Proxy.Proxy qualified +import Servant +import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named (Named (Named)) + +type InternalAPI = Named "status" ("i" :> "status" :> MultiVerb 'GET '[Servant.JSON] '[RespondEmpty 200 "OK"] ()) + +servantSitemap :: ServerT InternalAPI Proxy.Proxy.Proxy +servantSitemap = Named @"status" (pure ()) diff --git a/services/proxy/src/Proxy/API/Public.hs b/services/proxy/src/Proxy/API/Public.hs index 03fd4b65bd1..24989369d48 100644 --- a/services/proxy/src/Proxy/API/Public.hs +++ b/services/proxy/src/Proxy/API/Public.hs @@ -16,7 +16,9 @@ -- with this program. If not, see . module Proxy.API.Public - ( sitemap, + ( PublicAPI, + servantSitemap, + waiRoutingSitemap, ) where @@ -41,18 +43,35 @@ import Network.Wai.Internal qualified as I import Network.Wai.Predicate hiding (Error, err, setStatus) import Network.Wai.Predicate.Request (getRequest) import Network.Wai.Routing hiding (path, route) +import Network.Wai.Routing qualified as Routing import Network.Wai.Utilities +import Network.Wai.Utilities.Server (compile) import Proxy.Env import Proxy.Proxy +import Servant qualified import System.Logger.Class hiding (Error, info, render) import System.Logger.Class qualified as Logger +type PublicAPI = Servant.Raw -- see https://wearezeta.atlassian.net/browse/WPB-1216 + +servantSitemap :: Env -> Servant.ServerT PublicAPI Proxy.Proxy.Proxy +servantSitemap e = Servant.Tagged app + where + app :: Application + app r k = appInProxy e r (Routing.route tree r k') + where + tree :: Tree (App Proxy) + tree = compile (waiRoutingSitemap e) + + k' :: Response -> Proxy.Proxy.Proxy ResponseReceived + k' = liftIO . k + -- | IF YOU MODIFY THIS, BE AWARE OF: -- -- >>> /libs/wire-api/src/Wire/API/Routes/Public/Proxy.hs -- >>> https://wearezeta.atlassian.net/browse/SQSERVICES-1647 -sitemap :: Env -> Routes a Proxy () -sitemap e = do +waiRoutingSitemap :: Env -> Routes a Proxy () +waiRoutingSitemap e = do get "/proxy/youtube/v3/:path" (proxy e "key" "secrets.youtube" Prefix "/youtube/v3" youtube) @@ -107,10 +126,10 @@ proxy e qparam keyname reroute path phost rq k = do then do threadDelay 5000 loop runInIO (n - 1) waiReq req - else runProxy e waiReq (k res) + else appInProxy e waiReq (k res) onUpstreamError runInIO x _ next = do void . runInIO $ Logger.warn (msg (val "gateway error") ~~ field "error" (show x)) - next (errorRs' error502) + next (errorRs error502) spotifyToken :: Request -> Proxy Response spotifyToken rq = do diff --git a/services/proxy/src/Proxy/Env.hs b/services/proxy/src/Proxy/Env.hs index d429787d1be..7b50325ed80 100644 --- a/services/proxy/src/Proxy/Env.hs +++ b/services/proxy/src/Proxy/Env.hs @@ -32,7 +32,7 @@ where import Control.Lens (makeLenses, (^.)) import Data.Configurator import Data.Configurator.Types -import Data.Id (RequestId (..)) +import Data.Id (RequestId (..), defRequestId) import Imports import Network.HTTP.Client import Network.HTTP.Client.TLS (tlsManagerSettings) @@ -62,7 +62,7 @@ createEnv o = do } let ac = AutoConfig 60 (reloadError g) (c, t) <- autoReload ac [Required $ o ^. secretsConfig] - let rid = RequestId "N/A" + let rid = RequestId defRequestId pure $! Env rid o g n c t where reloadError g x = diff --git a/services/proxy/src/Proxy/Options.hs b/services/proxy/src/Proxy/Options.hs index e484dccf6d3..66d8d09c36b 100644 --- a/services/proxy/src/Proxy/Options.hs +++ b/services/proxy/src/Proxy/Options.hs @@ -27,7 +27,6 @@ module Proxy.Options logLevel, logNetStrings, logFormat, - mockOpts, disabledAPIVersions, ) where @@ -36,7 +35,7 @@ import Control.Lens hiding (Level) import Data.Aeson import Data.Aeson.TH import Imports -import System.Logger.Extended (Level (Debug), LogFormat) +import System.Logger.Extended (Level, LogFormat) import Wire.API.Routes.Version data Opts = Opts @@ -64,18 +63,3 @@ data Opts = Opts makeLenses ''Opts deriveJSON defaultOptions {fieldLabelModifier = drop 1} ''Opts - --- | for testing. -mockOpts :: FilePath -> Opts -mockOpts secrets = - Opts - { _host = mempty, - _port = 0, - _secretsConfig = secrets, - _httpPoolSize = 0, - _maxConns = 0, - _logLevel = Debug, - _logNetStrings = pure $ pure $ True, - _logFormat = mempty, - _disabledAPIVersions = mempty - } diff --git a/services/proxy/src/Proxy/Proxy.hs b/services/proxy/src/Proxy/Proxy.hs index fe65dc4b920..348c96eb39b 100644 --- a/services/proxy/src/Proxy/Proxy.hs +++ b/services/proxy/src/Proxy/Proxy.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 Proxy.Proxy (Proxy, runProxy) where +module Proxy.Proxy (Proxy, appInProxy, runProxy) where import Bilge.Request (requestIdName) import Control.Lens hiding ((.=)) @@ -51,11 +51,14 @@ newtype Proxy a = Proxy instance MonadLogger Proxy where log l m = ask >>= \e -> Logger.log (e ^. applog) l (reqIdMsg (e ^. reqId) . m) -runProxy :: Env -> Request -> Proxy ResponseReceived -> IO ResponseReceived -runProxy e r m = do +appInProxy :: Env -> Request -> Proxy ResponseReceived -> IO ResponseReceived +appInProxy e r m = do rid <- lookupReqId (e ^. applog) r runReaderT (unProxy m) (reqId .~ rid $ e) +runProxy :: Env -> Proxy a -> IO a +runProxy e m = runReaderT (unProxy m) e + reqIdMsg :: RequestId -> Msg -> Msg reqIdMsg = ("request" .=) . unRequestId {-# INLINE reqIdMsg #-} diff --git a/services/proxy/src/Proxy/Run.hs b/services/proxy/src/Proxy/Run.hs index 16d43994006..706ce328bd6 100644 --- a/services/proxy/src/Proxy/Run.hs +++ b/services/proxy/src/Proxy/Run.hs @@ -20,29 +20,69 @@ module Proxy.Run ) where +import Bilge.Request (requestIdName) +import Control.Error import Control.Lens hiding ((.=)) import Control.Monad.Catch -import Data.Metrics.Middleware.Prometheus (waiPrometheusMiddleware) +import Data.Id (RequestId (RequestId), defRequestId) +import Data.Metrics.Middleware.Prometheus (waiPrometheusMiddlewarePaths) +import Data.Metrics.Servant +import Data.Metrics.Types +import Data.Metrics.WaiRoute import Imports hiding (head) +import Network.Wai (Middleware, Request, requestHeaders) import Network.Wai.Middleware.Gunzip qualified as GZip +import Network.Wai.Routing.Route import Network.Wai.Utilities.Server hiding (serverPort) -import Proxy.API (sitemap) +import Proxy.API.Internal as I +import Proxy.API.Public as P import Proxy.Env import Proxy.Options import Proxy.Proxy +import Servant qualified import Wire.API.Routes.Version import Wire.API.Routes.Version.Wai +type CombinedAPI = InternalAPI Servant.:<|> PublicAPI + +combinedSitemap :: Env -> Servant.ServerT CombinedAPI Proxy +combinedSitemap env = I.servantSitemap Servant.:<|> P.servantSitemap env + run :: Opts -> IO () run o = do e <- createEnv o s <- newSettings $ defaultServer (o ^. host) (o ^. port) (e ^. applog) - let rtree = compile (sitemap e) - let app r k = runProxy e r (route rtree r k) - let middleware = + + let metricsMW :: Middleware + metricsMW = + -- FUTUREWORK: once wai-routing has been removed from proxy: use `servantPrometheusMiddleware + -- (Servant.Proxy @CombinedAPI)` here (and probably inline the whole thing). + waiPrometheusMiddlewarePaths (pub <> int) + where + pub, int :: Paths + pub = treeToPaths $ prepare (P.waiRoutingSitemap e) + int = routesToPaths @InternalAPI + + middleware :: Middleware + middleware = versionMiddleware (foldMap expandVersionExp (o ^. disabledAPIVersions)) . requestIdMiddleware (e ^. applog) defaultRequestIdHeaderName - . waiPrometheusMiddleware (sitemap e) + . metricsMW . GZip.gunzip . catchErrors (e ^. applog) defaultRequestIdHeaderName - runSettingsWithShutdown s (middleware app) Nothing `finally` destroyEnv e + + runSettingsWithShutdown s (middleware (mkApp e)) Nothing `finally` destroyEnv e + +mkApp :: Env -> Servant.Application +mkApp env req = Servant.serve (Servant.Proxy @CombinedAPI) toServantSitemap req + where + toServantSitemap :: Servant.Server CombinedAPI + toServantSitemap = Servant.hoistServer (Servant.Proxy @CombinedAPI) toServantHandler (combinedSitemap env) + + toServantHandler :: Proxy a -> Servant.Handler a + toServantHandler p = Servant.Handler . ExceptT $ Right <$> runProxy (injectReqId req env) p + + injectReqId :: Request -> Env -> Env + injectReqId r = reqId .~ lookupReqId r + where + lookupReqId = RequestId . fromMaybe defRequestId . lookup requestIdName . requestHeaders diff --git a/services/proxy/test/scripts/proxy-test.sh b/services/proxy/test/scripts/proxy-test.sh index 3f8ee9ed3ba..ea7b89fe403 100755 --- a/services/proxy/test/scripts/proxy-test.sh +++ b/services/proxy/test/scripts/proxy-test.sh @@ -11,7 +11,8 @@ instance. this replaces more thorough integration tests, since integration tests for just proxy without the proxied services installed is hard and inadequate. -WIRE_BACKEND: $WIRE_BACKEND +WIRE_BACKEND: $WIRE_BACKEND (do not append a / to host:port!) + WIRE_ADMIN: $WIRE_ADMIN WIRE_PASSWD: " diff --git a/services/spar/default.nix b/services/spar/default.nix index fe5d88485e7..e6424e6e32b 100644 --- a/services/spar/default.nix +++ b/services/spar/default.nix @@ -20,14 +20,12 @@ , cookie , crypton , crypton-x509 -, email-validate , exceptions , extended , gitignoreSource , hscim , HsOpenSSL , hspec -, hspec-core , hspec-discover , hspec-junit-formatter , hspec-wai @@ -64,6 +62,7 @@ , tasty-hunit , text , text-latin1 +, these , time , tinylog , transformers @@ -78,6 +77,7 @@ , wai-utilities , warp , wire-api +, wire-subsystems , xml-conduit , yaml , zauth @@ -125,6 +125,7 @@ mkDerivation { servant-server text text-latin1 + these time tinylog transformers @@ -137,6 +138,7 @@ mkDerivation { wai-utilities warp wire-api + wire-subsystems yaml ]; executableHaskellDepends = [ @@ -156,13 +158,11 @@ mkDerivation { containers cookie crypton - email-validate exceptions extended hscim HsOpenSSL hspec - hspec-core hspec-junit-formatter hspec-wai http-api-data @@ -174,6 +174,7 @@ mkDerivation { lens-aeson MonadRandom mtl + network-uri optparse-applicative polysemy polysemy-plugin @@ -189,6 +190,7 @@ mkDerivation { string-conversions tasty-hunit text + these time tinylog transformers @@ -199,7 +201,6 @@ mkDerivation { vector wai-extra wai-utilities - warp wire-api xml-conduit yaml @@ -230,6 +231,7 @@ mkDerivation { servant servant-openapi3 string-conversions + these time tinylog types-common diff --git a/services/spar/migrate-data/src/Spar/DataMigration/RIO.hs b/services/spar/migrate-data/src/Spar/DataMigration/RIO.hs deleted file mode 100644 index 3db5aa9aa7c..00000000000 --- a/services/spar/migrate-data/src/Spar/DataMigration/RIO.hs +++ /dev/null @@ -1,38 +0,0 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} - --- 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 Spar.DataMigration.RIO where - -import Imports - -newtype RIO env a = RIO {unRIO :: ReaderT env IO a} - deriving newtype (Functor, Applicative, Monad, MonadIO, MonadReader env) - -runRIO :: env -> RIO env a -> IO a -runRIO e f = runReaderT (unRIO f) e - -modifyRef :: (env -> IORef a) -> (a -> a) -> RIO env () -modifyRef get_ mod' = do - ref <- asks get_ - liftIO (modifyIORef ref mod') - -readRef :: (env -> IORef b) -> RIO env b -readRef g = do - ref <- asks g - liftIO $ readIORef ref diff --git a/services/spar/migrate-data/src/Spar/DataMigration/Types.hs b/services/spar/migrate-data/src/Spar/DataMigration/Types.hs index 64d7a13c0e9..abcdf2d34df 100644 --- a/services/spar/migrate-data/src/Spar/DataMigration/Types.hs +++ b/services/spar/migrate-data/src/Spar/DataMigration/Types.hs @@ -76,8 +76,8 @@ makeLenses ''CassandraSettings toCassandraOpts :: CassandraSettings -> CassandraOpts toCassandraOpts cas = CassandraOpts - { _endpoint = Endpoint (Text.pack (cas ^. cHosts)) (cas ^. cPort), - _keyspace = C.unKeyspace (cas ^. cKeyspace), - _filterNodesByDatacentre = Nothing, - _tlsCa = cas ^. cTlsCa + { endpoint = Endpoint (Text.pack (cas ^. cHosts)) (cas ^. cPort), + keyspace = C.unKeyspace (cas ^. cKeyspace), + filterNodesByDatacentre = Nothing, + tlsCa = cas ^. cTlsCa } diff --git a/services/spar/migrate-data/src/Spar/DataMigration/V2_UserV2.hs b/services/spar/migrate-data/src/Spar/DataMigration/V2_UserV2.hs index 59c30b74b1f..ea329e08f8a 100644 --- a/services/spar/migrate-data/src/Spar/DataMigration/V2_UserV2.hs +++ b/services/spar/migrate-data/src/Spar/DataMigration/V2_UserV2.hs @@ -188,10 +188,6 @@ filterResolved resolver migMapInv = yieldOld old go --- for debugging only -resolveNothing :: CollisionResolver -resolveNothing = const (pure . Left) - combineResolver :: CollisionResolver -> CollisionResolver -> CollisionResolver combineResolver resolver1 resolver2 pair olds = resolver1 pair olds >>= \case diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index 1e161ee0560..a9b452682e4 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -40,6 +40,7 @@ library Spar.Schema.V16 Spar.Schema.V17 Spar.Schema.V18 + Spar.Schema.V19 Spar.Schema.V2 Spar.Schema.V3 Spar.Schema.V4 @@ -185,6 +186,7 @@ library , servant-server , text , text-latin1 + , these , time , tinylog , transformers @@ -197,6 +199,7 @@ library , wai-utilities , warp , wire-api + , wire-subsystems , yaml default-language: Haskell2010 @@ -264,9 +267,10 @@ executable spar executable spar-integration main-is: Main.hs - -- cabal-fmt: expand test-integration + -- we should not use cabal-fmt expand here because `Main` should not be in `other-modules`, it's wrong + -- and cabal chokes on it + -- FUTUREWORK(mangoiv): move Main to a different directory such that this one can be expanded other-modules: - Main Test.LoggingSpec Test.MetricsSpec Test.Spar.APISpec @@ -349,13 +353,11 @@ executable spar-integration , cassava , cookie , crypton - , email-validate , exceptions , extended , hscim , HsOpenSSL , hspec - , hspec-core , hspec-junit-formatter , hspec-wai , http-api-data @@ -367,6 +369,7 @@ executable spar-integration , lens-aeson , MonadRandom , mtl + , network-uri , optparse-applicative , polysemy , polysemy-plugin @@ -383,6 +386,7 @@ executable spar-integration , string-conversions , tasty-hunit , text + , these , time , tinylog , transformers @@ -392,7 +396,6 @@ executable spar-integration , vector , wai-extra , wai-utilities - , warp , wire-api , xml-conduit , yaml @@ -405,7 +408,6 @@ executable spar-migrate-data other-modules: Paths_spar Spar.DataMigration.Options - Spar.DataMigration.RIO Spar.DataMigration.Run Spar.DataMigration.Types Spar.DataMigration.V2_UserV2 @@ -632,6 +634,7 @@ test-suite spec , servant-openapi3 , spar , string-conversions + , these , time , tinylog , types-common diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index 12a53e10b96..cbf91970c4e 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -50,10 +50,10 @@ import Cassandra as Cas import Control.Lens hiding ((.=)) import qualified Data.ByteString as SBS import Data.ByteString.Builder (toLazyByteString) +import Data.HavePendingInvitations import Data.Id import Data.Proxy import Data.Range -import qualified Data.Set as Set import Data.Text.Encoding.Error import qualified Data.Text.Lazy as T import Data.Text.Lazy.Encoding @@ -73,10 +73,10 @@ import Spar.Error import qualified Spar.Intra.BrigApp as Brig import Spar.Options import Spar.Orphans () -import Spar.Scim +import Spar.Scim hiding (handle) import Spar.Sem.AReqIDStore (AReqIDStore) import Spar.Sem.AssIDStore (AssIDStore) -import Spar.Sem.BrigAccess (BrigAccess) +import Spar.Sem.BrigAccess (BrigAccess, getAccount) import qualified Spar.Sem.BrigAccess as BrigAccess import Spar.Sem.DefaultSsoCode (DefaultSsoCode) import qualified Spar.Sem.DefaultSsoCode as DefaultSsoCode @@ -103,6 +103,7 @@ import qualified Spar.Sem.VerdictFormatStore as VerdictFormatStore import System.Logger (Msg) import qualified URI.ByteString as URI import Wire.API.Routes.Internal.Spar +import Wire.API.Routes.Named import Wire.API.Routes.Public.Spar import Wire.API.Team.Member (HiddenPerm (CreateUpdateDeleteIdp, ReadIdp)) import Wire.API.User @@ -183,13 +184,13 @@ apiSSO :: Opts -> ServerT APISSO (Sem r) apiSSO opts = - SAML2.meta appName (SamlProtocolSettings.spIssuer Nothing) (SamlProtocolSettings.responseURI Nothing) - :<|> (\tid -> SAML2.meta appName (SamlProtocolSettings.spIssuer (Just tid)) (SamlProtocolSettings.responseURI (Just tid))) - :<|> authreqPrecheck - :<|> authreq (maxttlAuthreqDiffTime opts) - :<|> authresp Nothing - :<|> authresp . Just - :<|> ssoSettings + Named @"sso-metadata" (SAML2.meta appName (SamlProtocolSettings.spIssuer Nothing) (SamlProtocolSettings.responseURI Nothing)) + :<|> Named @"sso-team-metadata" (\tid -> SAML2.meta appName (SamlProtocolSettings.spIssuer (Just tid)) (SamlProtocolSettings.responseURI (Just tid))) + :<|> Named @"auth-req-precheck" authreqPrecheck + :<|> Named @"auth-req" (authreq (maxttlAuthreqDiffTime opts)) + :<|> Named @"auth-resp-legacy" (authresp Nothing) + :<|> Named @"auth-resp" (authresp . Just) + :<|> Named @"sso-settings" ssoSettings apiIDP :: ( Member Random r, @@ -204,12 +205,12 @@ apiIDP :: ) => ServerT APIIDP (Sem r) apiIDP = - idpGet -- get, json, captures idp id - :<|> idpGetRaw -- get, raw xml, capture idp id - :<|> idpGetAll -- get, json - :<|> idpCreate -- post, created - :<|> idpUpdate -- put, okay - :<|> idpDelete -- delete, no content + Named @"idp-get" idpGet -- get, json, captures idp id + :<|> Named @"idp-get-raw" idpGetRaw -- get, raw xml, capture idp id + :<|> Named @"idp-get-all" idpGetAll -- get, json + :<|> Named @"idp-create" idpCreate -- post, created + :<|> Named @"idp-update" idpUpdate -- put, okay + :<|> Named @"idp-delete" idpDelete -- delete, no content apiINTERNAL :: ( Member ScimTokenStore r, @@ -221,10 +222,10 @@ apiINTERNAL :: ) => ServerT InternalAPI (Sem r) apiINTERNAL = - internalStatus - :<|> internalDeleteTeam - :<|> internalPutSsoSettings - :<|> internalGetScimUserInfo + Named @"i_status" internalStatus + :<|> Named @"i_delete_team" internalDeleteTeam + :<|> Named @"i_put_sso_settings" internalPutSsoSettings + :<|> Named @"i_post_scim_user_info" internalGetScimUserInfo appName :: Text appName = "spar" @@ -382,7 +383,7 @@ idpGetAll :: Sem r IdPList idpGetAll zusr = withDebugLog "idpGetAll" (const Nothing) $ do teamid <- Brig.getZUsrCheckPerm zusr ReadIdp - _providers <- IdPConfigStore.getConfigsByTeam teamid + providers <- IdPConfigStore.getConfigsByTeam teamid pure IdPList {..} -- | Delete empty IdPs, or if @"purge=true"@ in the HTTP query, delete all users @@ -433,7 +434,7 @@ idpDelete mbzusr idpid (fromMaybe False -> purge) = withDebugLog "idpDelete" (co assertEmptyOrPurge teamId page = do forM_ (Cas.result page) $ \(uref, uid) -> do mAccount <- BrigAccess.getAccount NoPendingInvitations uid - let mUserTeam = userTeam . accountUser =<< mAccount + let mUserTeam = userTeam =<< mAccount when (mUserTeam == Just teamId) $ do if purge then do @@ -465,7 +466,7 @@ idpDelete mbzusr idpid (fromMaybe False -> purge) = withDebugLog "idpDelete" (co idpDoesAuthSelf :: IdP -> UserId -> Sem r Bool idpDoesAuthSelf idp uid = do let idpIssuer = idp ^. SAML.idpMetadata . SAML.edIssuer - mUserIssuer <- (>>= userIssuer) <$> Brig.getBrigUser NoPendingInvitations uid + mUserIssuer <- (>>= userIssuer) <$> getAccount NoPendingInvitations uid pure $ mUserIssuer == Just idpIssuer -- | This handler only does the json parsing, and leaves all authorization checks and @@ -790,8 +791,7 @@ internalPutSsoSettings SsoSettings {defaultSsoCode = Just code} = *> DefaultSsoCode.store code $> NoContent -internalGetScimUserInfo :: (Member ScimUserTimesStore r) => UserSet -> Sem r ScimUserInfos -internalGetScimUserInfo (UserSet uids) = do - results <- ScimUserTimesStore.readMulti (Set.toList uids) - let scimUserInfos = results <&> (\(uid, t, _) -> ScimUserInfo uid (Just t)) - pure $ ScimUserInfos scimUserInfos +internalGetScimUserInfo :: (Member ScimUserTimesStore r) => UserId -> Sem r ScimUserInfo +internalGetScimUserInfo uid = do + t <- fmap fst <$> ScimUserTimesStore.read uid + pure $ ScimUserInfo uid t diff --git a/services/spar/src/Spar/App.hs b/services/spar/src/Spar/App.hs index ee2c61ccbfe..562776433b4 100644 --- a/services/spar/src/Spar/App.hs +++ b/services/spar/src/Spar/App.hs @@ -53,6 +53,7 @@ import qualified Data.Text.Encoding as Text import Data.Text.Encoding.Error import qualified Data.Text.Lazy as LText import qualified Data.Text.Lazy.Encoding as LText +import Data.These import Imports hiding (MonadReader, asks, log) import qualified Network.HTTP.Types.Status as Http import qualified Network.Wai.Utilities.Error as Wai @@ -74,7 +75,7 @@ import qualified Spar.Intra.BrigApp as Intra import Spar.Options import Spar.Orphans () import Spar.Sem.AReqIDStore (AReqIDStore) -import Spar.Sem.BrigAccess (BrigAccess) +import Spar.Sem.BrigAccess (BrigAccess, getAccount) import qualified Spar.Sem.BrigAccess as BrigAccess import Spar.Sem.GalleyAccess (GalleyAccess) import qualified Spar.Sem.GalleyAccess as GalleyAccess @@ -94,10 +95,9 @@ import qualified System.Logger as TinyLog import URI.ByteString as URI import Web.Cookie (SetCookie, renderSetCookie) import Wire.API.Team.Role (Role, defaultRole) -import Wire.API.User hiding (validateEmail) +import Wire.API.User import Wire.API.User.IdentityProvider import Wire.API.User.Saml -import Wire.API.User.Scim (ValidExternalId (..)) import Wire.Sem.Logger (Logger) import qualified Wire.Sem.Logger as Logger import Wire.Sem.Random (Random) @@ -141,7 +141,7 @@ getUserByUrefUnsafe :: SAML.UserRef -> Sem r (Maybe User) getUserByUrefUnsafe uref = do - maybe (pure Nothing) (Intra.getBrigUser Intra.WithPendingInvitations) =<< SAMLUserStore.get uref + maybe (pure Nothing) (getAccount Intra.WithPendingInvitations) =<< SAMLUserStore.get uref -- FUTUREWORK: Remove and reinstatate getUser, in AuthID refactoring PR getUserIdByScimExternalId :: @@ -149,10 +149,10 @@ getUserIdByScimExternalId :: Member ScimExternalIdStore r ) => TeamId -> - Email -> + Text -> Sem r (Maybe UserId) -getUserIdByScimExternalId tid email = do - muid <- ScimExternalIdStore.lookup tid email +getUserIdByScimExternalId tid eid = do + muid <- ScimExternalIdStore.lookup tid eid case muid of Nothing -> pure Nothing Just uid -> do @@ -189,7 +189,7 @@ createSamlUserWithId :: createSamlUserWithId teamid buid suid role = do uname <- either (throwSparSem . SparBadUserName . LText.pack) pure $ - Intra.mkUserName Nothing (UrefOnly suid) + Intra.mkUserName Nothing (That suid) buid' <- BrigAccess.createSAML suid buid teamid uname ManagedByWire Nothing Nothing Nothing role assert (buid == buid') $ pure () SAMLUserStore.insert suid buid @@ -252,7 +252,7 @@ validateEmail :: ) => Maybe TeamId -> UserId -> - Email -> + EmailAddress -> Sem r () validateEmail mbTid uid email = do enabled <- maybe (pure False) GalleyAccess.isEmailValidationEnabledTeam mbTid @@ -390,7 +390,7 @@ moveUserToNewIssuer :: Sem r () moveUserToNewIssuer oldUserRef newUserRef uid = do SAMLUserStore.insert newUserRef uid - BrigAccess.setVeid uid (UrefOnly newUserRef) + BrigAccess.setSSOId uid (UserSSOId newUserRef) SAMLUserStore.delete uid oldUserRef verdictHandlerResultCore :: diff --git a/services/spar/src/Spar/Data.hs b/services/spar/src/Spar/Data.hs index fc79c7dfb7b..919d1ddce4b 100644 --- a/services/spar/src/Spar/Data.hs +++ b/services/spar/src/Spar/Data.hs @@ -23,7 +23,6 @@ module Spar.Data mkTTLAssertions, nominalDiffToSeconds, mkTTLAuthnRequests, - mkTTLAuthnRequestsNDT, -- * SAML Users NormalizedUNameID (..), @@ -75,9 +74,6 @@ mkEnv opts now = mkTTLAuthnRequests :: (MonadError TTLError m) => Env -> UTCTime -> m (TTL "authreq") mkTTLAuthnRequests (Env now maxttl _) = mkTTL now maxttl -mkTTLAuthnRequestsNDT :: (MonadError TTLError m) => Env -> NominalDiffTime -> m (TTL "authreq") -mkTTLAuthnRequestsNDT (Env _ maxttl _) = mkTTLNDT maxttl - mkTTLAssertions :: (MonadError TTLError m) => Env -> UTCTime -> m (TTL "authresp") mkTTLAssertions (Env now _ maxttl) = mkTTL now maxttl diff --git a/services/spar/src/Spar/Intra/Brig.hs b/services/spar/src/Spar/Intra/Brig.hs index aaac39be64b..a059b0232a6 100644 --- a/services/spar/src/Spar/Intra/Brig.hs +++ b/services/spar/src/Spar/Intra/Brig.hs @@ -28,7 +28,7 @@ module Spar.Intra.Brig setBrigUserName, setBrigUserHandle, setBrigUserManagedBy, - setBrigUserVeid, + setBrigUserSSOId, setBrigUserRichInfo, setBrigUserLocale, checkHandleAvailable, @@ -68,14 +68,10 @@ import Wire.API.User import Wire.API.User.Auth.ReAuth import Wire.API.User.Auth.Sso import Wire.API.User.RichInfo as RichInfo -import Wire.API.User.Scim (ValidExternalId (..), runValidExternalIdEither) +import Wire.UserSubsystem (HavePendingInvitations (..)) ---------------------------------------------------------------------- --- | FUTUREWORK: this is redundantly defined in "Spar.Intra.BrigApp". -veidToUserSSOId :: ValidExternalId -> UserSSOId -veidToUserSSOId = runValidExternalIdEither UserSSOId (UserScimExternalId . fromEmail) - -- | Similar to 'Network.Wire.Client.API.Auth.tokenResponse', but easier: we just need to set the -- cookie in the response, and the redirect will make the client negotiate a fresh auth token. -- (This is the easiest way, since the login-request that we are in the middle of responding to here @@ -130,7 +126,8 @@ createBrigUserSAML uref (Id buid) teamid name managedBy handle richInfo mLocale createBrigUserNoSAML :: (HasCallStack, MonadSparToBrig m) => - Email -> + Text -> + EmailAddress -> UserId -> TeamId -> -- | User name @@ -138,8 +135,8 @@ createBrigUserNoSAML :: Maybe Locale -> Role -> m UserId -createBrigUserNoSAML email uid teamid uname locale role = do - let newUser = NewUserScimInvitation teamid uid locale uname email role +createBrigUserNoSAML extId email uid teamid uname locale role = do + let newUser = NewUserScimInvitation teamid uid extId locale uname email role resp :: ResponseLBS <- call $ method POST @@ -147,10 +144,10 @@ createBrigUserNoSAML email uid teamid uname locale role = do . json newUser if statusCode resp `elem` [200, 201] - then userId . accountUser <$> parseResponse @UserAccount "brig" resp + then userId <$> parseResponse @User "brig" resp else rethrow "brig" resp -updateEmail :: (HasCallStack, MonadSparToBrig m) => UserId -> Email -> m () +updateEmail :: (HasCallStack, MonadSparToBrig m) => UserId -> EmailAddress -> m () updateEmail buid email = do resp <- call $ @@ -165,7 +162,7 @@ updateEmail buid email = do _ -> rethrow "brig" resp -- | Get a user; returns 'Nothing' if the user was not found or has been deleted. -getBrigUserAccount :: (HasCallStack, MonadSparToBrig m) => HavePendingInvitations -> UserId -> m (Maybe UserAccount) +getBrigUserAccount :: (HasCallStack, MonadSparToBrig m) => HavePendingInvitations -> UserId -> m (Maybe User) getBrigUserAccount havePending buid = do resp :: ResponseLBS <- call $ @@ -183,10 +180,10 @@ getBrigUserAccount havePending buid = do case statusCode resp of 200 -> - parseResponse @[UserAccount] "brig" resp >>= \case + parseResponse @[User] "brig" resp >>= \case [account] -> pure $ - if userDeleted $ accountUser account + if userDeleted account then Nothing else Just account _ -> pure Nothing @@ -197,7 +194,7 @@ getBrigUserAccount havePending buid = do -- -- TODO: currently this is not used, but it might be useful later when/if -- @hscim@ stops doing checks during user creation. -getBrigUserByHandle :: (HasCallStack, MonadSparToBrig m) => Handle -> m (Maybe UserAccount) +getBrigUserByHandle :: (HasCallStack, MonadSparToBrig m) => Handle -> m (Maybe User) getBrigUserByHandle handle = do resp :: ResponseLBS <- call $ @@ -206,11 +203,11 @@ getBrigUserByHandle handle = do . queryItem "handles" (toByteString' handle) . queryItem "includePendingInvitations" "true" case statusCode resp of - 200 -> listToMaybe <$> parseResponse @[UserAccount] "brig" resp + 200 -> listToMaybe <$> parseResponse @[User] "brig" resp 404 -> pure Nothing _ -> rethrow "brig" resp -getBrigUserByEmail :: (HasCallStack, MonadSparToBrig m) => Email -> m (Maybe UserAccount) +getBrigUserByEmail :: (HasCallStack, MonadSparToBrig m) => EmailAddress -> m (Maybe User) getBrigUserByEmail email = do resp :: ResponseLBS <- call $ @@ -220,8 +217,8 @@ getBrigUserByEmail email = do . queryItem "includePendingInvitations" "true" case statusCode resp of 200 -> do - macc <- listToMaybe <$> parseResponse @[UserAccount] "brig" resp - case userEmail . accountUser =<< macc of + macc <- listToMaybe <$> parseResponse @[User] "brig" resp + case userEmail =<< macc of Just email' | email' == email -> pure macc _ -> pure Nothing 404 -> pure Nothing @@ -273,13 +270,13 @@ setBrigUserManagedBy buid managedBy = do rethrow "brig" resp -- | Set user's UserSSOId. -setBrigUserVeid :: (HasCallStack, MonadSparToBrig m) => UserId -> ValidExternalId -> m () -setBrigUserVeid buid veid = do +setBrigUserSSOId :: (HasCallStack, MonadSparToBrig m) => UserId -> UserSSOId -> m () +setBrigUserSSOId buid ssoId = do resp <- call $ method PUT . paths ["i", "users", toByteString' buid, "sso-id"] - . json (veidToUserSSOId veid) + . json ssoId case statusCode resp of 200 -> pure () _ -> rethrow "brig" resp diff --git a/services/spar/src/Spar/Intra/BrigApp.hs b/services/spar/src/Spar/Intra/BrigApp.hs index acca8893826..f394ed12cbb 100644 --- a/services/spar/src/Spar/Intra/BrigApp.hs +++ b/services/spar/src/Spar/Intra/BrigApp.hs @@ -23,13 +23,10 @@ module Spar.Intra.BrigApp ( veidToUserSSOId, urefToExternalId, - urefToEmail, veidFromBrigUser, veidFromUserSSOId, mkUserName, - renderValidExternalId, HavePendingInvitations (..), - getBrigUser, getBrigUserTeam, getZUsrCheckPerm, authorizeScimTokenManagement, @@ -38,22 +35,21 @@ module Spar.Intra.BrigApp -- * re-exports, mostly for historical reasons and lazyness emailFromSAML, - emailToSAML, - emailToSAMLNameID, - emailFromSAMLNameID, ) where import Brig.Types.Intra -import Brig.Types.User import Control.Lens import Control.Monad.Except import Data.ByteString.Conversion import qualified Data.CaseInsensitive as CI import Data.Handle (Handle, parseHandle) +import Data.HavePendingInvitations import Data.Id (TeamId, UserId) import Data.Text.Encoding import Data.Text.Encoding.Error +import Data.These +import Data.These.Combinators import Imports import Polysemy import Polysemy.Error @@ -65,63 +61,71 @@ import Spar.Sem.GalleyAccess (GalleyAccess) import qualified Spar.Sem.GalleyAccess as GalleyAccess import Wire.API.Team.Member (HiddenPerm (CreateReadDeleteScimToken), IsPerm) import Wire.API.User -import Wire.API.User.Scim (ValidExternalId (..), runValidExternalIdEither) +import Wire.API.User.Scim (ValidScimId (..)) ---------------------------------------------------------------------- --- | FUTUREWORK: this is redundantly defined in "Spar.Intra.Brig" -veidToUserSSOId :: ValidExternalId -> UserSSOId -veidToUserSSOId = runValidExternalIdEither UserSSOId (UserScimExternalId . fromEmail) - -veidFromUserSSOId :: (MonadError String m) => UserSSOId -> m ValidExternalId -veidFromUserSSOId = \case - UserSSOId uref -> - case urefToEmail uref of - Nothing -> pure $ UrefOnly uref - Just email -> pure $ EmailAndUref email uref - UserScimExternalId email -> - maybe - (throwError "externalId not an email and no issuer") - (pure . EmailOnly) - (parseEmail email) - --- | If the brig user has a 'UserSSOId', transform that into a 'ValidExternalId' (this is a +veidToUserSSOId :: ValidScimId -> UserSSOId +veidToUserSSOId (ValidScimId eid authInfo) = maybe (UserScimExternalId eid) UserSSOId (justThere authInfo) + +veidFromUserSSOId :: + (MonadError String m) => + UserSSOId -> + -- | this is either the unvalidated email if exists, or otherwise the validated email. + Maybe EmailAddress -> + m ValidScimId +veidFromUserSSOId ssoId mEmail = case ssoId of + UserSSOId uref -> do + let eid = CI.original $ uref ^. SAML.uidSubject . to SAML.unsafeShowNameID + pure $ case mEmail of + Just email -> ValidScimId eid (These email uref) + Nothing -> ValidScimId eid (That uref) + UserScimExternalId veid -> do + case mEmail of + Just email -> + pure $ ValidScimId veid (This email) + Nothing -> + -- If veid can be parsed as an email, we end up in the case above with email delivered separately. + throwError "internal error: externalId is not an email and there is no SAML issuer" + +-- | If the brig user has a 'UserSSOId', transform that into a 'ValidScimId' (this is a -- total function as long as brig obeys the api). Otherwise, if the user has an email, we can --- construct a return value from that (and an optional saml issuer). If a user only has a --- phone number, or no identity at all, throw an error. +-- construct a return value from that (and an optional saml issuer). -- -- Note: the saml issuer is only needed in the case where a user has been invited via team -- settings and is now onboarded to saml/scim. If this case can safely be ruled out, it's ok -- to just set it to 'Nothing'. -veidFromBrigUser :: (MonadError String m) => User -> Maybe SAML.Issuer -> m ValidExternalId -veidFromBrigUser usr mIssuer = case (userSSOId usr, userEmail usr, mIssuer) of - (Just ssoid, _, _) -> veidFromUserSSOId ssoid - (Nothing, Just email, Just issuer) -> pure $ EmailAndUref email (SAML.UserRef issuer (emailToSAMLNameID email)) - (Nothing, Just email, Nothing) -> pure $ EmailOnly email +-- +-- `userSSOId usr` can be empty if the user has no SAML credentials and is brought under scim +-- management for the first time. In that case, the externalId is taken to +-- be the email address. +veidFromBrigUser :: (MonadError String m) => User -> Maybe SAML.Issuer -> Maybe EmailAddress -> m ValidScimId +veidFromBrigUser usr mIssuer mUnvalidatedEmail = case (userSSOId usr, userEmail usr, mIssuer) of + (Just ssoid, mValidatedEmail, _) -> do + -- `mEmail` is in synch with SCIM user schema. + let mEmail = mUnvalidatedEmail <|> mValidatedEmail + veidFromUserSSOId ssoid mEmail + (Nothing, Just email, Just issuer) -> pure $ ValidScimId (fromEmail email) (These email (SAML.UserRef issuer (fromRight' $ emailToSAMLNameID email))) + (Nothing, Just email, Nothing) -> pure $ ValidScimId (fromEmail email) (This email) (Nothing, Nothing, _) -> throwError "user has neither ssoIdentity nor userEmail" -- | Take a maybe text, construct a 'Name' from what we have in a scim user. If the text -- isn't present, use an email address or a saml subject (usually also an email address). If -- both are 'Nothing', fail. -mkUserName :: Maybe Text -> ValidExternalId -> Either String Name +mkUserName :: Maybe Text -> These EmailAddress SAML.UserRef -> Either String Name mkUserName (Just n) = const $ mkName n mkUserName Nothing = - runValidExternalIdEither - (\uref -> mkName (CI.original . SAML.unsafeShowNameID $ uref ^. SAML.uidSubject)) + these (mkName . fromEmail) - -renderValidExternalId :: ValidExternalId -> Maybe Text -renderValidExternalId = runValidExternalIdEither urefToExternalId (Just . fromEmail) + (\uref -> mkName (CI.original . SAML.unsafeShowNameID $ uref ^. SAML.uidSubject)) + (\_ uref -> mkName (CI.original . SAML.unsafeShowNameID $ uref ^. SAML.uidSubject)) ---------------------------------------------------------------------- -getBrigUser :: (HasCallStack, Member BrigAccess r) => HavePendingInvitations -> UserId -> Sem r (Maybe User) -getBrigUser ifpend = (accountUser <$$>) . BrigAccess.getAccount ifpend - -- | Check that an id maps to an user on brig that is 'Active' (or optionally -- 'PendingInvitation') and has a team id. getBrigUserTeam :: (HasCallStack, Member BrigAccess r) => HavePendingInvitations -> UserId -> Sem r (Maybe TeamId) -getBrigUserTeam ifpend = fmap (userTeam =<<) . getBrigUser ifpend +getBrigUserTeam ifpend = fmap (userTeam =<<) . BrigAccess.getAccount ifpend -- | Pull team id for z-user from brig. Check permission in galley. Return team id. Fail if -- permission check fails or the user is not in status 'Active'. diff --git a/services/spar/src/Spar/Intra/Galley.hs b/services/spar/src/Spar/Intra/Galley.hs index 03dc835df67..8e9b508c947 100644 --- a/services/spar/src/Spar/Intra/Galley.hs +++ b/services/spar/src/Spar/Intra/Galley.hs @@ -99,8 +99,8 @@ assertSSOEnabled tid = do . paths ["i", "teams", toByteString' tid, "features", "sso"] unless (statusCode resp == 200) $ rethrow "galley" resp - ws :: WithStatus SSOConfig <- parseResponse "galley" resp - unless (wsStatus ws == FeatureStatusEnabled) $ + ws :: LockableFeature SSOConfig <- parseResponse "galley" resp + unless (ws.status == FeatureStatusEnabled) $ throwSpar SparSSODisabled isEmailValidationEnabledTeam :: (HasCallStack, MonadSparToGalley m) => TeamId -> m Bool @@ -108,7 +108,7 @@ isEmailValidationEnabledTeam tid = do resp <- call $ method GET . paths ["i", "teams", toByteString' tid, "features", "validateSAMLemails"] pure ( statusCode resp == 200 - && ( (wsStatus <$> responseJsonMaybe @(WithStatus ValidateSAMLEmailsConfig) resp) + && ( ((.status) <$> responseJsonMaybe @(LockableFeature ValidateSAMLEmailsConfig) resp) == Just FeatureStatusEnabled ) ) diff --git a/services/spar/src/Spar/Run.hs b/services/spar/src/Spar/Run.hs index f07ca3ce871..170721df48b 100644 --- a/services/spar/src/Spar/Run.hs +++ b/services/spar/src/Spar/Run.hs @@ -92,14 +92,14 @@ mkApp sparCtxOpts = do sparCtxCas <- initCassandra sparCtxOpts sparCtxLogger sparCtxHttpManager <- Bilge.newManager Bilge.defaultManagerSettings let sparCtxHttpBrig = - Bilge.host (sparCtxOpts ^. to brig . host . to encodeUtf8) - . Bilge.port (sparCtxOpts ^. to brig . port) + Bilge.host (sparCtxOpts ^. to brig . to host . to encodeUtf8) + . Bilge.port (sparCtxOpts ^. to brig . to port) $ Bilge.empty let sparCtxHttpGalley = - Bilge.host (sparCtxOpts ^. to galley . host . to encodeUtf8) - . Bilge.port (sparCtxOpts ^. to galley . port) + Bilge.host (sparCtxOpts ^. to galley . to host . to encodeUtf8) + . Bilge.port (sparCtxOpts ^. to galley . to port) $ Bilge.empty - let sparCtxRequestId = RequestId "N/A" + let sparCtxRequestId = RequestId defRequestId let ctx0 = Env {..} let heavyLogOnly :: (Wai.Request, LByteString) -> Maybe (Wai.Request, LByteString) heavyLogOnly out@(req, _) = diff --git a/services/spar/src/Spar/Schema/Run.hs b/services/spar/src/Spar/Schema/Run.hs index ac273fb83c4..e3f35f9ba2e 100644 --- a/services/spar/src/Spar/Schema/Run.hs +++ b/services/spar/src/Spar/Schema/Run.hs @@ -32,6 +32,7 @@ import qualified Spar.Schema.V15 as V15 import qualified Spar.Schema.V16 as V16 import qualified Spar.Schema.V17 as V17 import qualified Spar.Schema.V18 as V18 +import qualified Spar.Schema.V19 as V19 import qualified Spar.Schema.V2 as V2 import qualified Spar.Schema.V3 as V3 import qualified Spar.Schema.V4 as V4 @@ -78,7 +79,8 @@ migrations = V15.migration, V16.migration, V17.migration, - V18.migration + V18.migration, + V19.migration -- TODO: Add a migration that removes unused fields -- (we don't want to risk running a migration which would -- effectively break the currently deployed spar service) diff --git a/libs/gundeck-types/src/Gundeck/Types.hs b/services/spar/src/Spar/Schema/V19.hs similarity index 68% rename from libs/gundeck-types/src/Gundeck/Types.hs rename to services/spar/src/Spar/Schema/V19.hs index 2658731e13e..6c55b7950c1 100644 --- a/libs/gundeck-types/src/Gundeck/Types.hs +++ b/services/spar/src/Spar/Schema/V19.hs @@ -15,11 +15,22 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Gundeck.Types - ( module G, +module Spar.Schema.V19 + ( migration, ) where -import Gundeck.Types.Event as G -import Gundeck.Types.Presence as G -import Gundeck.Types.Push as G +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = Migration 19 "Add name column to scim token info" $ do + schema' + [r| + ALTER TABLE team_provisioning_by_team ADD (name text); + |] + schema' + [r| + ALTER TABLE team_provisioning_by_token ADD (name text); + |] diff --git a/services/spar/src/Spar/Scim/Auth.hs b/services/spar/src/Spar/Scim/Auth.hs index 35e2b6a394f..5bad5826054 100644 --- a/services/spar/src/Spar/Scim/Auth.hs +++ b/services/spar/src/Spar/Scim/Auth.hs @@ -37,7 +37,7 @@ where import Control.Lens hiding (Strict, (.=)) import qualified Data.ByteString.Base64 as ES -import Data.Id (ScimTokenId, UserId) +import Data.Id import qualified Data.Text.Encoding as T import Data.Text.Encoding.Error import Imports @@ -60,6 +60,7 @@ import qualified Spar.Sem.ScimTokenStore as ScimTokenStore import qualified Web.Scim.Class.Auth as Scim.Class.Auth import qualified Web.Scim.Handler as Scim import qualified Web.Scim.Schema.Error as Scim +import Wire.API.Routes.Named import Wire.API.Routes.Public.Spar (APIScimToken) import Wire.API.User as User import Wire.API.User.Scim as Api @@ -97,9 +98,53 @@ apiScimToken :: ) => ServerT APIScimToken (Sem r) apiScimToken = - createScimToken - :<|> deleteScimToken - :<|> listScimTokens + Named @"auth-tokens-create@v6" createScimTokenV6 + :<|> Named @"auth-tokens-create" createScimToken + :<|> Named @"auth-tokens-put-name" updateScimTokenName + :<|> Named @"auth-tokens-delete" deleteScimToken + :<|> Named @"auth-tokens-list@v6" listScimTokensV6 + :<|> Named @"auth-tokens-list" listScimTokens + +updateScimTokenName :: + ( Member BrigAccess r, + Member ScimTokenStore r, + Member (Error E.SparError) r, + Member GalleyAccess r + ) => + UserId -> + ScimTokenId -> + ScimTokenName -> + Sem r () +updateScimTokenName lusr tokenId name = do + teamid <- Intra.Brig.authorizeScimTokenManagement (Just lusr) + ScimTokenStore.updateName teamid tokenId name.fromScimTokenName + +-- | > docs/reference/provisioning/scim-token.md {#RefScimTokenCreate} +-- +-- Create a token for user's team. +createScimTokenV6 :: + forall r. + ( Member Random r, + Member (Input Opts) r, + Member GalleyAccess r, + Member BrigAccess r, + Member ScimTokenStore r, + Member IdPConfigStore r, + Member Now r, + Member (Error E.SparError) r + ) => + -- | Who is trying to create a token + Maybe UserId -> + -- | Request body + CreateScimToken -> + Sem r CreateScimTokenResponseV6 +createScimTokenV6 zusr req = responseToV6 <$> createScimToken zusr req + where + responseToV6 :: CreateScimTokenResponse -> CreateScimTokenResponseV6 + responseToV6 (CreateScimTokenResponse token info) = CreateScimTokenResponseV6 token (infoToV6 info) + + infoToV6 :: ScimTokenInfo -> ScimTokenInfoV6 + infoToV6 ScimTokenInfo {..} = ScimTokenInfoV6 {..} -- | > docs/reference/provisioning/scim-token.md {#RefScimTokenCreate} -- @@ -121,9 +166,8 @@ createScimToken :: CreateScimToken -> Sem r CreateScimTokenResponse createScimToken zusr Api.CreateScimToken {..} = do - let descr = createScimTokenDescr teamid <- Intra.Brig.authorizeScimTokenManagement zusr - BrigAccess.ensureReAuthorised zusr createScimTokenPassword createScimTokenCode (Just User.CreateScimToken) + BrigAccess.ensureReAuthorised zusr password verificationCode (Just User.CreateScimToken) tokenNumber <- length <$> ScimTokenStore.lookupByTeam teamid maxTokens <- inputs maxScimTokens unless (tokenNumber < maxTokens) $ @@ -147,7 +191,8 @@ createScimToken zusr Api.CreateScimToken {..} = do stiTeam = teamid, stiCreatedAt = now, stiIdP = midpid, - stiDescr = descr + stiDescr = description, + stiName = fromMaybe (idToText tokenid) name } ScimTokenStore.insert token info pure $ CreateScimTokenResponse token info @@ -178,6 +223,23 @@ deleteScimToken zusr tokenid = do ScimTokenStore.delete teamid tokenid pure NoContent +listScimTokensV6 :: + ( Member GalleyAccess r, + Member BrigAccess r, + Member ScimTokenStore r, + Member (Error E.SparError) r + ) => + -- | Who is trying to list tokens + Maybe UserId -> + Sem r ScimTokenListV6 +listScimTokensV6 zusr = toV6 <$> listScimTokens zusr + where + toV6 :: ScimTokenList -> ScimTokenListV6 + toV6 (ScimTokenList tokens) = ScimTokenListV6 $ map infoToV6 tokens + + infoToV6 :: ScimTokenInfo -> ScimTokenInfoV6 + infoToV6 ScimTokenInfo {..} = ScimTokenInfoV6 {..} + -- | > docs/reference/provisioning/scim-token.md {#RefScimTokenList} -- -- List all tokens belonging to user's team. Tokens themselves are not available, only diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index 0ce2a38a2fd..019c20902a4 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -4,6 +4,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} @@ -32,12 +33,13 @@ -- | Doing operations with users via SCIM. -- -- Provides a 'Scim.Class.User.UserDB' instance. +-- Exported functions are used in tests. module Spar.Scim.User ( validateScimUser', synthesizeScimUser, - toScimStoredUser', - mkValidExternalId, - scimFindUserByEmail, + toScimStoredUser, + mkValidScimId, + scimFindUserByExternalId, deleteScimUser, ) where @@ -61,6 +63,8 @@ import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import Data.Text.Encoding.Error import qualified Data.Text.Lazy as LText +import Data.These +import Data.These.Combinators import qualified Data.UUID as UUID import Imports import Network.URI (URI, parseURI) @@ -76,7 +80,7 @@ import Spar.Options import Spar.Scim.Auth () import Spar.Scim.Types import qualified Spar.Scim.Types as ST -import Spar.Sem.BrigAccess (BrigAccess) +import Spar.Sem.BrigAccess (BrigAccess, getAccount) import qualified Spar.Sem.BrigAccess as BrigAccess import Spar.Sem.GalleyAccess as GalleyAccess import Spar.Sem.IdPConfigStore (IdPConfigStore) @@ -102,12 +106,13 @@ import qualified Web.Scim.Schema.Meta as Scim import qualified Web.Scim.Schema.ResourceType as Scim import qualified Web.Scim.Schema.User as Scim import qualified Web.Scim.Schema.User as Scim.User (schemas) +import qualified Web.Scim.Schema.User.Email as Scim.Email import qualified Wire.API.Team.Member as Member import Wire.API.Team.Role import Wire.API.User import Wire.API.User.IdentityProvider (IdP) import qualified Wire.API.User.RichInfo as RI -import Wire.API.User.Scim (ScimTokenInfo (..)) +import Wire.API.User.Scim (ScimTokenInfo (..), ValidScimId (..)) import qualified Wire.API.User.Scim as ST import Wire.Sem.Logger (Logger) import qualified Wire.Sem.Logger as Logger @@ -154,7 +159,7 @@ instance | Scim.isUserSchema schema -> do x <- runMaybeT $ case attrName of "username" -> scimFindUserByHandle mIdpConfig stiTeam val - "externalid" -> scimFindUserByEmail mIdpConfig stiTeam val + "externalid" -> scimFindUserByExternalId mIdpConfig stiTeam val _ -> throwError (Scim.badRequest Scim.InvalidFilter (Just "Unsupported attribute")) pure $ Scim.fromList (toList x) | otherwise -> throwError $ Scim.badRequest Scim.InvalidFilter (Just "Unsupported schema") @@ -253,10 +258,11 @@ validateHandle txt = case parseHandle txt of -- configurable on a per-team basis in the future, to accomodate different legal uses of -- @externalId@ by different teams. -- --- __Emails and phone numbers:__ we'd like to ensure that only verified emails and phone --- numbers end up in our database, and implementing verification requires design decisions +-- __Email verification:__ we'd like to ensure that only verified emails numbers end up +-- in our database, and implementing verification requires design decisions -- that we haven't made yet. We store them in our SCIM blobs, but don't syncronize them with -- Brig. See . +-- FUTUREWORK(elland): verify with fisx if this still applies. validateScimUser' :: forall r. ( Member (Error Scim.ScimError) r, @@ -273,7 +279,7 @@ validateScimUser' :: Sem r ST.ValidScimUser validateScimUser' errloc midp richInfoLimit user = do unless (isNothing $ Scim.password user) $ throw $ badRequest "Setting user passwords is not supported for security reasons." - veid <- mkValidExternalId midp (Scim.externalId user) + veid <- mkValidScimId midp (Scim.externalId user) (Scim.Email.scimEmailsToEmailAddress $ Scim.emails user) handl <- validateHandle . Text.toLower . Scim.userName $ user -- FUTUREWORK: 'Scim.userName' should be case insensitive; then the toLower here would -- be a little less brittle. @@ -286,12 +292,13 @@ validateScimUser' errloc midp richInfoLimit user = do <> " (" <> errloc <> ")" - either err pure $ Brig.mkUserName (Scim.displayName user) veid + either err pure $ Brig.mkUserName (Scim.displayName user) (ST.validScimIdAuthInfo veid) richInfo <- validateRichInfo (Scim.extra user ^. ST.sueRichInfo) let active = Scim.active user lang <- maybe (throw $ badRequest "Could not parse language. Expected format is ISO 639-1.") pure $ mapM parseLanguage $ Scim.preferredLanguage user mRole <- validateRole user - pure $ ST.ValidScimUser veid handl uname richInfo (maybe True Scim.unScimBool active) (flip Locale Nothing <$> lang) mRole + + pure $ ST.ValidScimUser veid handl uname (maybeToList (justHere veid.validScimIdAuthInfo)) richInfo (maybe True Scim.unScimBool active) (flip Locale Nothing <$> lang) mRole where validRoleNames :: Text validRoleNames = @@ -341,11 +348,11 @@ validateScimUser' errloc midp richInfoLimit user = do } pure richInfo --- | Given an 'externalId' and an 'IdP', construct a 'ST.ValidExternalId'. +-- | Given an 'externalId' and an 'IdP', construct a 'ST.ValidScimId'. -- -- This is needed primarily in 'validateScimUser', but also in 'updateValidScimUser' to -- recover the 'SAML.UserRef' of the scim user before the update from the database. -mkValidExternalId :: +mkValidScimId :: forall r. ( Member BrigAccess r, Member SAMLUserStore r, @@ -353,19 +360,22 @@ mkValidExternalId :: ) => Maybe IdP -> Maybe Text -> - Sem r ST.ValidExternalId -mkValidExternalId _ Nothing = + Maybe EmailAddress -> + Sem r ST.ValidScimId +mkValidScimId _ Nothing _ = throw $ Scim.badRequest Scim.InvalidValue (Just "externalId is required") -mkValidExternalId Nothing (Just extid) = do +mkValidScimId Nothing (Just extid) (Just email) = do + pure $ ST.ValidScimId extid (This email) +mkValidScimId Nothing (Just extid) Nothing = do let err = Scim.badRequest Scim.InvalidValue (Just "externalId must be a valid email address or (if there is a SAML IdP) a valid SAML NameID") - maybe (throw err) (pure . ST.EmailOnly) $ parseEmail extid -mkValidExternalId (Just idp) (Just extid) = do + maybe (throw err) (pure . ST.ValidScimId extid . This) $ emailAddressText extid +mkValidScimId (Just idp) (Just extid) mEmail = do let issuer = idp ^. SAML.idpMetadata . SAML.edIssuer subject <- validateSubject extid let uref = SAML.UserRef issuer subject @@ -382,9 +392,10 @@ mkValidExternalId (Just idp) (Just extid) = do -- The entry in spar.user_v2 does not exist yet during user -- creation. So we just assume that it will exist momentarily. pure uref - pure $ case parseEmail extid of - Just email -> ST.EmailAndUref email indexedUref - Nothing -> ST.UrefOnly indexedUref + pure . ST.ValidScimId extid $ case (mEmail, emailAddressText extid) of + (Just email, _) -> These email indexedUref + (Nothing, Just email) -> These email indexedUref + (Nothing, Nothing) -> That indexedUref where -- Validate a subject ID (@externalId@). validateSubject :: Text -> Sem r SAML.NameID @@ -424,13 +435,14 @@ logScim context postcontext action = Logger.info $ context . postcontext x . Log.msg @Text "call without exception" pure (Right x) -logEmail :: Email -> (Msg -> Msg) +logEmail :: EmailAddress -> (Msg -> Msg) logEmail email = Log.field "email_sha256" (sha256String . Text.pack . show $ email) logVSU :: ST.ValidScimUser -> (Msg -> Msg) -logVSU (ST.ValidScimUser veid handl _name _richInfo _active _lang _role) = - maybe id logEmail (veidEmail veid) +logVSU (ST.ValidScimUser veid handl _name _emails _richInfo _active _lang _role) = + -- FUTUREWORK(elland): Take SCIM emails field into account. + maybe id logEmail (justHere $ ST.validScimIdAuthInfo veid) . logHandle handl logTokenInfo :: ScimTokenInfo -> (Msg -> Msg) @@ -442,10 +454,8 @@ logScimUserId = logUser . Scim.id . Scim.thing logScimUserIds :: Scim.ListResponse (Scim.StoredUser ST.SparTag) -> (Msg -> Msg) logScimUserIds lresp = foldl' (.) id (logScimUserId <$> Scim.resources lresp) -veidEmail :: ST.ValidExternalId -> Maybe Email -veidEmail (ST.EmailAndUref email _) = Just email -veidEmail (ST.UrefOnly _) = Nothing -veidEmail (ST.EmailOnly email) = Just email +vsUserEmail :: ST.ValidScimUser -> Maybe EmailAddress +vsUserEmail usr = justHere $ ST.validScimIdAuthInfo usr.externalId -- in ScimTokenHash (cs @ByteString @Text (convertToBase Base64 digest)) @@ -483,7 +493,7 @@ createValidScimUser :: ScimTokenInfo -> ST.ValidScimUser -> m (Scim.StoredUser ST.SparTag) -createValidScimUser tokeninfo@ScimTokenInfo {stiTeam} vsu@(ST.ValidScimUser veid handl name richInfo _active language role) = +createValidScimUser tokeninfo@ScimTokenInfo {stiTeam} vsu@(ST.ValidScimUser {..}) = logScim ( logFunction "Spar.Scim.User.createValidScimUser" . logVSU vsu @@ -491,7 +501,7 @@ createValidScimUser tokeninfo@ScimTokenInfo {stiTeam} vsu@(ST.ValidScimUser veid ) logScimUserId $ do - lift (ScimExternalIdStore.lookupStatus stiTeam veid) >>= \case + lift (ScimExternalIdStore.lookupStatus stiTeam externalId) >>= \case Just (buid, ScimUserCreated) -> -- If the user has been created, but can't be found in brig anymore, -- the invitation has timed out and the user has been deleted on brig's side. @@ -499,10 +509,10 @@ createValidScimUser tokeninfo@ScimTokenInfo {stiTeam} vsu@(ST.ValidScimUser veid -- HALF-CREATED ACCOUNT HAS BEEN GARBAGE-COLLECTED. -- Otherwise we return a conflict error. lift (BrigAccess.getStatusMaybe buid) >>= \case - Just Active -> throwError (externalIdTakenError ("user with status Active exists: " <> Text.pack (show (veid, buid)))) - Just Suspended -> throwError (externalIdTakenError ("user with status Suspended exists" <> Text.pack (show (veid, buid)))) - Just Ephemeral -> throwError (externalIdTakenError ("user with status Ephemeral exists" <> Text.pack (show (veid, buid)))) - Just PendingInvitation -> throwError (externalIdTakenError ("user with status PendingInvitation exists" <> Text.pack (show (veid, buid)))) + Just Active -> throwError (externalIdTakenError ("user with status Active exists: " <> Text.pack (show (externalId, buid)))) + Just Suspended -> throwError (externalIdTakenError ("user with status Suspended exists" <> Text.pack (show (externalId, buid)))) + Just Ephemeral -> throwError (externalIdTakenError ("user with status Ephemeral exists" <> Text.pack (show (externalId, buid)))) + Just PendingInvitation -> throwError (externalIdTakenError ("user with status PendingInvitation exists" <> Text.pack (show (externalId, buid)))) Just Deleted -> incompleteUserCreationCleanUp buid Nothing -> incompleteUserCreationCleanUp buid Just (buid, ScimUserCreating) -> @@ -511,28 +521,25 @@ createValidScimUser tokeninfo@ScimTokenInfo {stiTeam} vsu@(ST.ValidScimUser veid -- ensure uniqueness constraints of all affected identifiers. -- {if we crash now, retry POST will just work} - assertExternalIdUnused stiTeam veid - assertHandleUnused handl + assertExternalIdUnused stiTeam externalId + assertHandleUnused handle -- {if we crash now, retry POST will just work, or user gets told the handle -- is already in use and stops POSTing} buid <- lift $ Id <$> Random.uuid - lift $ ScimExternalIdStore.insertStatus stiTeam veid buid ScimUserCreating + lift $ ScimExternalIdStore.insertStatus stiTeam externalId buid ScimUserCreating -- Generate a UserId will be used both for scim user in spar and for brig. lift $ do - ST.runValidExternalIdEither - ( \uref -> + let doUref uref = do -- FUTUREWORK: outsource this and some other fragments from -- `createValidScimUser` into a function `createValidScimUserBrig` similar -- to `createValidScimUserSpar`? - void $ BrigAccess.createSAML uref buid stiTeam name ManagedByScim (Just handl) (Just richInfo) language (fromMaybe defaultRole role) - ) - ( \email -> do - void $ BrigAccess.createNoSAML email buid stiTeam name language (fromMaybe defaultRole role) - BrigAccess.setHandle buid handl -- FUTUREWORK: possibly do the same one req as we do for saml? - ) - veid + void $ BrigAccess.createSAML uref buid stiTeam name ManagedByScim (Just handle) (Just richInfo) locale (fromMaybe defaultRole role) + doEmail email = do + void $ BrigAccess.createNoSAML externalId.validScimIdExternal email buid stiTeam name locale (fromMaybe defaultRole role) + BrigAccess.setHandle buid handle -- FUTUREWORK: possibly do the same one req as we do for saml? + these doEmail doUref (\_ uref -> doUref uref) (validScimIdAuthInfo externalId) Logger.debug ("createValidScimUser: brig says " <> show buid) BrigAccess.setRichInfo buid richInfo @@ -549,24 +556,25 @@ createValidScimUser tokeninfo@ScimTokenInfo {stiTeam} vsu@(ST.ValidScimUser veid acc <- lift (BrigAccess.getAccount Brig.WithPendingInvitations buid) >>= maybe (throwError $ Scim.serverError "Server error: user vanished") pure - synthesizeStoredUser acc veid + synthesizeStoredUser acc externalId lift $ Logger.debug ("createValidScimUser: spar says " <> show storedUser) -- {(arianvp): these two actions we probably want to make transactional.} - createValidScimUserSpar stiTeam buid storedUser veid + createValidScimUserSpar stiTeam buid storedUser externalId -- If applicable, trigger email validation procedure on brig. - lift $ Spar.App.validateEmail (Just stiTeam) buid `mapM_` veidEmail veid + -- FUTUREWORK: validate fallback emails? + lift $ Spar.App.validateEmail (Just stiTeam) buid `mapM_` vsUserEmail vsu -- TODO: suspension via scim is brittle, and may leave active users behind: if we don't -- reach the following line due to a crash, the user will be active. lift $ do old <- BrigAccess.getStatus buid - let new = ST.scimActiveFlagToAccountStatus old (Scim.unScimBool <$> active) - active = Scim.active . Scim.value . Scim.thing $ storedUser + let new = ST.scimActiveFlagToAccountStatus old (Scim.unScimBool <$> active') + active' = Scim.active . Scim.value . Scim.thing $ storedUser when (new /= old) $ BrigAccess.setStatus buid new - lift $ ScimExternalIdStore.insertStatus stiTeam veid buid ScimUserCreated + lift $ ScimExternalIdStore.insertStatus stiTeam externalId buid ScimUserCreated pure storedUser where incompleteUserCreationCleanUp :: UserId -> Scim.ScimHandler (Sem r) () @@ -594,18 +602,12 @@ createValidScimUserSpar :: TeamId -> UserId -> Scim.StoredUser ST.SparTag -> - ST.ValidExternalId -> + ST.ValidScimId -> m () createValidScimUserSpar stiTeam uid storedUser veid = lift $ do ScimUserTimesStore.write storedUser - -- This uses the "both" variant to always write all applicable index tables, even if - -- `spar.scim_external` is never consulted as long as there is an IdP. This is hoped to - -- mitigate logic errors in this code and corner cases. (eg., if the IdP is later removed?) - ST.runValidExternalIdBoth - (>>) - (`SAMLUserStore.insert` uid) - (\email -> ScimExternalIdStore.insert stiTeam email uid) - veid + ScimExternalIdStore.insert stiTeam veid.validScimIdExternal uid + for_ (justThere veid.validScimIdAuthInfo) (`SAMLUserStore.insert` uid) -- TODO(arianvp): how do we get this safe w.r.t. race conditions / crashes? updateValidScimUser :: @@ -644,12 +646,12 @@ updateValidScimUser tokinfo@ScimTokenInfo {stiTeam} uid nvsu = -- if the locale of the new valid SCIM user is not set, -- we set it to default value from brig defLocale <- lift BrigAccess.getDefaultUserLocale - let newValidScimUser = nvsu {ST._vsuLocale = ST._vsuLocale nvsu <|> Just defLocale} + let newValidScimUser = nvsu {ST.locale = ST.locale nvsu <|> Just defLocale} -- assertions about new valid scim user that cannot be checked in 'validateScimUser' because -- they differ from the ones in 'createValidScimUser'. - assertExternalIdNotUsedElsewhere stiTeam (newValidScimUser ^. ST.vsuExternalId) uid - assertHandleNotUsedElsewhere uid (newValidScimUser ^. ST.vsuHandle) + assertExternalIdNotUsedElsewhere stiTeam (newValidScimUser.externalId) uid + assertHandleNotUsedElsewhere uid (newValidScimUser.handle) if oldValidScimUser == newValidScimUser then pure oldScimStoredUser @@ -658,29 +660,29 @@ updateValidScimUser tokinfo@ScimTokenInfo {stiTeam} uid nvsu = newScimStoredUser :: Scim.StoredUser ST.SparTag <- updScimStoredUser (synthesizeScimUser newValidScimUser) oldScimStoredUser - when (oldValidScimUser ^. ST.vsuExternalId /= newValidScimUser ^. ST.vsuExternalId) $ - updateVsuUref stiTeam uid (oldValidScimUser ^. ST.vsuExternalId) (newValidScimUser ^. ST.vsuExternalId) + when (oldValidScimUser.externalId /= newValidScimUser.externalId) $ + updateVsuUref stiTeam uid (oldValidScimUser.externalId) (newValidScimUser.externalId) - when (newValidScimUser ^. ST.vsuName /= oldValidScimUser ^. ST.vsuName) $ - BrigAccess.setName uid (newValidScimUser ^. ST.vsuName) + when (newValidScimUser.name /= oldValidScimUser.name) $ + BrigAccess.setName uid (newValidScimUser.name) - when (oldValidScimUser ^. ST.vsuHandle /= newValidScimUser ^. ST.vsuHandle) $ - BrigAccess.setHandle uid (newValidScimUser ^. ST.vsuHandle) + when (oldValidScimUser.handle /= newValidScimUser.handle) $ + BrigAccess.setHandle uid (newValidScimUser.handle) - when (oldValidScimUser ^. ST.vsuRichInfo /= newValidScimUser ^. ST.vsuRichInfo) $ - BrigAccess.setRichInfo uid (newValidScimUser ^. ST.vsuRichInfo) + when (oldValidScimUser.richInfo /= newValidScimUser.richInfo) $ + BrigAccess.setRichInfo uid (newValidScimUser.richInfo) - when (oldValidScimUser ^. ST.vsuLocale /= newValidScimUser ^. ST.vsuLocale) $ do - BrigAccess.setLocale uid (newValidScimUser ^. ST.vsuLocale) + when (oldValidScimUser.locale /= newValidScimUser.locale) $ do + BrigAccess.setLocale uid (newValidScimUser.locale) - forM_ (newValidScimUser ^. ST.vsuRole) $ \newRole -> do - when (oldValidScimUser ^. ST.vsuRole /= Just newRole) $ do + forM_ (newValidScimUser.role) $ \newRole -> do + when (oldValidScimUser.role /= Just newRole) $ do GalleyAccess.updateTeamMember uid stiTeam newRole BrigAccess.getStatusMaybe uid >>= \case Nothing -> pure () Just old -> do - let new = ST.scimActiveFlagToAccountStatus old (Just $ newValidScimUser ^. ST.vsuActive) + let new = ST.scimActiveFlagToAccountStatus old (Just $ newValidScimUser.active) when (new /= old) $ BrigAccess.setStatus uid new ScimUserTimesStore.write newScimStoredUser @@ -694,20 +696,23 @@ updateVsuUref :: ) => TeamId -> UserId -> - ST.ValidExternalId -> - ST.ValidExternalId -> + ST.ValidScimId -> + ST.ValidScimId -> Sem r () updateVsuUref team uid old new = do - case (veidEmail old, veidEmail new) of + case (justHere $ ST.validScimIdAuthInfo old, justHere $ ST.validScimIdAuthInfo new) of (mo, mn@(Just email)) | mo /= mn -> Spar.App.validateEmail (Just team) uid email _ -> pure () - old & ST.runValidExternalIdBoth (>>) (SAMLUserStore.delete uid) (ScimExternalIdStore.delete team) - new & ST.runValidExternalIdBoth (>>) (`SAMLUserStore.insert` uid) (\email -> ScimExternalIdStore.insert team email uid) + ScimExternalIdStore.delete team old.validScimIdExternal + for_ (justThere old.validScimIdAuthInfo) (SAMLUserStore.delete uid) - BrigAccess.setVeid uid new + ScimExternalIdStore.insert team new.validScimIdExternal uid + for_ (justThere new.validScimIdAuthInfo) (`SAMLUserStore.insert` uid) -toScimStoredUser' :: + BrigAccess.setSSOId uid $ veidToUserSSOId new + +toScimStoredUser :: (HasCallStack) => UTCTimeMillis -> UTCTimeMillis -> @@ -715,7 +720,7 @@ toScimStoredUser' :: UserId -> Scim.User ST.SparTag -> Scim.StoredUser ST.SparTag -toScimStoredUser' createdAt lastChangedAt baseuri uid usr = +toScimStoredUser createdAt lastChangedAt baseuri uid usr = Scim.WithMeta meta $ Scim.WithId uid $ usr {Scim.User.schemas = ST.userSchemas} @@ -784,8 +789,8 @@ deleteScimUser tokeninfo@ScimTokenInfo {stiTeam, stiIdP} uid = -- ("tombstones") would not have the needed values (`userIdentity = -- Nothing`) to delete a user in spar. I.e. `SAML.UserRef` and `Email` -- cannot be figured out when a `User` has status `Deleted`. - mbBrigUser <- lift $ Brig.getBrigUser WithPendingInvitations uid - case mbBrigUser of + mbAccount <- lift $ BrigAccess.getAccount WithPendingInvitations uid + case mbAccount of Nothing -> -- Ensure there's no left-over of this user in brig. This is safe -- because the user has either been deleted (tombstone) or does not @@ -818,18 +823,14 @@ deleteScimUser tokeninfo@ScimTokenInfo {stiTeam, stiIdP} uid = ) => User -> Scim.ScimHandler (Sem r) () - deleteUserInSpar brigUser = do + deleteUserInSpar account = do mIdpConfig <- mapM (lift . IdPConfigStore.getConfig) stiIdP - case Brig.veidFromBrigUser brigUser ((^. SAML.idpMetadata . SAML.edIssuer) <$> mIdpConfig) of + case Brig.veidFromBrigUser account ((^. SAML.idpMetadata . SAML.edIssuer) <$> mIdpConfig) account.userEmailUnvalidated of Left _ -> pure () - Right veid -> - lift $ - ST.runValidExternalIdBoth - (>>) - (SAMLUserStore.delete uid) - (ScimExternalIdStore.delete stiTeam) - veid + Right veid -> lift $ do + for_ (justThere veid.validScimIdAuthInfo) (SAMLUserStore.delete uid) + ScimExternalIdStore.delete stiTeam veid.validScimIdExternal lift $ ScimUserTimesStore.delete uid ---------------------------------------------------------------------------- @@ -865,7 +866,7 @@ assertExternalIdUnused :: Member SAMLUserStore r ) => TeamId -> - ST.ValidExternalId -> + ST.ValidScimId -> Scim.ScimHandler (Sem r) () assertExternalIdUnused = assertExternalIdInAllowedValues @@ -882,7 +883,7 @@ assertExternalIdNotUsedElsewhere :: Member SAMLUserStore r ) => TeamId -> - ST.ValidExternalId -> + ST.ValidScimId -> UserId -> Scim.ScimHandler (Sem r) () assertExternalIdNotUsedElsewhere tid veid wireUserId = @@ -900,16 +901,14 @@ assertExternalIdInAllowedValues :: [Maybe UserId] -> Text -> TeamId -> - ST.ValidExternalId -> + ST.ValidScimId -> Scim.ScimHandler (Sem r) () assertExternalIdInAllowedValues allowedValues errmsg tid veid = do isGood <- - lift $ - ST.runValidExternalIdBoth - (\ma mb -> (&&) <$> ma <*> mb) - (fmap ((`elem` allowedValues) . fmap userId) . getUserByUrefUnsafe) - (fmap (`elem` allowedValues) . getUserIdByScimExternalId tid) - veid + lift $ do + mViaEid <- getUserIdByScimExternalId tid veid.validScimIdExternal + mViaUref <- join <$> (for (justThere veid.validScimIdAuthInfo) ((userId <$$>) . getUserByUrefUnsafe)) + pure $ all (`elem` allowedValues) [mViaEid, mViaUref] unless isGood $ throwError Scim.conflict {Scim.detail = Just errmsg} @@ -924,7 +923,7 @@ assertHandleUnused' msg hndl = assertHandleNotUsedElsewhere :: (Member BrigAccess r) => UserId -> Handle -> Scim.ScimHandler (Sem r) () assertHandleNotUsedElsewhere uid hndl = do - musr <- lift $ Brig.getBrigUser Brig.WithPendingInvitations uid + musr <- lift $ getAccount Brig.WithPendingInvitations uid unless ((userHandle =<< musr) == Just hndl) $ assertHandleUnused' "userName already in use by another wire user" hndl @@ -940,21 +939,21 @@ synthesizeStoredUser :: Member GalleyAccess r, Member ScimUserTimesStore r ) => - UserAccount -> - ST.ValidExternalId -> + User -> + ST.ValidScimId -> Scim.ScimHandler (Sem r) (Scim.StoredUser ST.SparTag) -synthesizeStoredUser usr veid = +synthesizeStoredUser acc veid = logScim ( logFunction "Spar.Scim.User.synthesizeStoredUser" - . logUser (userId . accountUser $ usr) - . maybe id logHandle (userHandle . accountUser $ usr) - . maybe id logTeam (userTeam . accountUser $ usr) - . maybe id logEmail (veidEmail veid) + . logUser (userId acc) + . maybe id logHandle acc.userHandle + . maybe id logTeam acc.userTeam + . maybe id logEmail (justHere $ ST.validScimIdAuthInfo veid) ) logScimUserId $ do - let uid = userId (accountUser usr) - accStatus = accountStatus usr + let uid = userId acc + accStatus = acc.userStatus let readState :: Sem r (RI.RichInfo, Maybe (UTCTimeMillis, UTCTimeMillis), URIBS.URI, Role) readState = @@ -978,33 +977,40 @@ synthesizeStoredUser usr veid = now <- toUTCTimeMillis <$> lift Now.get let (createdAt, lastUpdatedAt) = fromMaybe (now, now) accessTimes - handle <- lift $ Brig.giveDefaultHandle (accountUser usr) + handle <- lift $ Brig.giveDefaultHandle acc + + let emails = + maybeToList $ + acc.userEmailUnvalidated <|> (emailIdentity =<< userIdentity acc) <|> justHere veid.validScimIdAuthInfo storedUser <- synthesizeStoredUser' uid veid - (userDisplayName (accountUser usr)) + acc.userDisplayName + emails handle richInfo accStatus createdAt lastUpdatedAt baseuri - (userLocale (accountUser usr)) + acc.userLocale (Just role) - lift $ writeState accessTimes (userManagedBy (accountUser usr)) richInfo storedUser + lift $ writeState accessTimes acc.userManagedBy richInfo storedUser pure storedUser where getRole :: Sem r Role getRole = do let tmRoleOrDefault m = fromMaybe defaultRole $ m >>= \member -> member ^. Member.permissions . to Member.permissionsRole - maybe (pure defaultRole) (\tid -> tmRoleOrDefault <$> GalleyAccess.getTeamMember tid (userId $ accountUser usr)) (userTeam $ accountUser usr) + maybe (pure defaultRole) (\tid -> tmRoleOrDefault <$> GalleyAccess.getTeamMember tid (userId acc)) (userTeam acc) synthesizeStoredUser' :: + (MonadError Scim.ScimError m) => UserId -> - ST.ValidExternalId -> + ST.ValidScimId -> Name -> + [EmailAddress] -> Handle -> RI.RichInfo -> AccountStatus -> @@ -1013,33 +1019,33 @@ synthesizeStoredUser' :: URIBS.URI -> Locale -> Maybe Role -> - (MonadError Scim.ScimError m) => m (Scim.StoredUser ST.SparTag) -synthesizeStoredUser' uid veid dname handle richInfo accStatus createdAt lastUpdatedAt baseuri locale mbRole = do + m (Scim.StoredUser ST.SparTag) +synthesizeStoredUser' uid veid dname emails handle richInfo accStatus createdAt lastUpdatedAt baseuri locale mbRole = do let scimUser :: Scim.User ST.SparTag scimUser = synthesizeScimUser ST.ValidScimUser - { ST._vsuExternalId = veid, - ST._vsuHandle = handle {- 'Maybe' there is one in @usr@, but we want the type - checker to make sure this exists, so we add it here - redundantly, without the 'Maybe'. -}, - ST._vsuName = dname, - ST._vsuRichInfo = richInfo, - ST._vsuActive = ST.scimActiveFlagFromAccountStatus accStatus, - ST._vsuLocale = Just locale, - ST._vsuRole = mbRole + { ST.externalId = veid, + ST.handle = handle {- 'Maybe' there is one in @usr@, but we want the type + checker to make sure this exists, so we add it here + redundantly, without the 'Maybe'. -}, + ST.emails = emails, + ST.name = dname, + ST.richInfo = richInfo, + ST.active = ST.scimActiveFlagFromAccountStatus accStatus, + ST.locale = Just locale, + ST.role = mbRole } - - pure $ toScimStoredUser' createdAt lastUpdatedAt baseuri uid (normalizeLikeStored scimUser) + pure $ toScimStoredUser createdAt lastUpdatedAt baseuri uid (normalizeLikeStored scimUser) synthesizeScimUser :: ST.ValidScimUser -> Scim.User ST.SparTag synthesizeScimUser info = - let userName = info ^. ST.vsuHandle . to fromHandle - in (Scim.empty @ST.SparTag ST.userSchemas userName (ST.ScimUserExtra (info ^. ST.vsuRichInfo))) - { Scim.externalId = Brig.renderValidExternalId $ info ^. ST.vsuExternalId, - Scim.displayName = Just $ fromName (info ^. ST.vsuName), - Scim.active = Just . Scim.ScimBool $ info ^. ST.vsuActive, - Scim.preferredLanguage = lan2Text . lLanguage <$> info ^. ST.vsuLocale, + let userName = info.handle.fromHandle + in (Scim.empty @ST.SparTag ST.userSchemas userName (ST.ScimUserExtra info.richInfo)) + { Scim.externalId = Just $ validScimIdExternal info.externalId, + Scim.displayName = Just $ fromName info.name, + Scim.active = Just . Scim.ScimBool $ info.active, + Scim.preferredLanguage = lan2Text . lLanguage <$> info.locale, Scim.roles = maybe [] @@ -1048,11 +1054,11 @@ synthesizeScimUser info = . toStrict . toByteString ) - (info ^. ST.vsuRole) + (info.role), + Scim.emails = (\e -> Scim.Email.Email Nothing (Scim.Email.EmailAddress e) Nothing) <$> info.emails } -- TODO: now write a test, either in /integration or in spar, whichever is easier. (spar) - getUserById :: forall r. ( Member BrigAccess r, @@ -1072,24 +1078,25 @@ getUserById midp stiTeam uid = do brigUser <- MaybeT . lift $ BrigAccess.getAccount Brig.WithPendingInvitations uid let mbveid = Brig.veidFromBrigUser - (accountUser brigUser) + brigUser ((^. SAML.idpMetadata . SAML.edIssuer) <$> midp) + brigUser.userEmailUnvalidated case mbveid of - Right veid | userTeam (accountUser brigUser) == Just stiTeam -> lift $ do + Right veid | userTeam brigUser == Just stiTeam -> lift $ do storedUser :: Scim.StoredUser ST.SparTag <- synthesizeStoredUser brigUser veid -- if we get a user from brig that hasn't been touched by scim yet, we call this -- function to move it under scim control. assertExternalIdNotUsedElsewhere stiTeam veid uid createValidScimUserSpar stiTeam uid storedUser veid lift $ do - when (veidChanged (accountUser brigUser) veid) $ - BrigAccess.setVeid uid veid - when (managedByChanged (accountUser brigUser)) $ + when (veidChanged brigUser veid) $ + BrigAccess.setSSOId uid (veidToUserSSOId veid) + when (managedByChanged brigUser) $ BrigAccess.setManagedBy uid ManagedByScim pure storedUser _ -> Applicative.empty where - veidChanged :: User -> ST.ValidExternalId -> Bool + veidChanged :: User -> ST.ValidScimId -> Bool veidChanged usr veid = case userIdentity usr of Nothing -> True Just (EmailIdentity _) -> True @@ -1116,15 +1123,15 @@ scimFindUserByHandle :: scimFindUserByHandle mIdpConfig stiTeam hndl = do handle <- MaybeT . pure . parseHandle . Text.toLower $ hndl brigUser <- MaybeT . lift . BrigAccess.getByHandle $ handle - getUserById mIdpConfig stiTeam . userId . accountUser $ brigUser + getUserById mIdpConfig stiTeam . userId $ brigUser --- | Construct a 'ValidExternalid'. If it an 'Email', find the non-SAML SCIM user in spar; if +-- | Construct a 'ValidScimId'. If it is an 'Email', find the non-SAML SCIM user in spar; if -- that fails, find the user by email in brig. If it is a 'UserRef', find the SAML user. -- Return the result as a SCIM user. -- -- Note the user won't get an entry in `spar.user`. That will only happen on their first -- successful authentication with their SAML credentials. -scimFindUserByEmail :: +scimFindUserByExternalId :: forall r. ( Member BrigAccess r, Member GalleyAccess r, @@ -1139,34 +1146,21 @@ scimFindUserByEmail :: TeamId -> Text -> MaybeT (Scim.ScimHandler (Sem r)) (Scim.StoredUser ST.SparTag) -scimFindUserByEmail mIdpConfig stiTeam email = do - -- Azure has been observed to search for externalIds that are not emails, even if the - -- mapping is set up like it should be. This is a problem: if there is no SAML IdP, 'mkValidExternalId' - -- only supports external IDs that are emails. This is a missing feature / bug in spar tracked in - -- https://wearezeta.atlassian.net/browse/SQSERVICES-157; once it is fixed, we should go back to - -- throwing errors returned by 'mkValidExternalId' here, but *not* throw an error if the externalId is - -- a UUID, or any other text that is valid according to SCIM. - veid <- MaybeT . lift $ either (const Nothing) Just <$> runError @Scim.ScimError (mkValidExternalId mIdpConfig (pure email)) - uid <- MaybeT . lift $ ST.runValidExternalIdEither withUref withEmailOnly veid - -- since gc on `spar.users{,_v2}` is unreliable, we need to double-check with brig if the - -- user we found actually exists. - brigUser <- MaybeT . lift . BrigAccess.getAccount Brig.WithPendingInvitations $ uid - getUserById mIdpConfig stiTeam . userId . accountUser $ brigUser - where - withUref :: SAML.UserRef -> Sem r (Maybe UserId) - withUref uref = - SAMLUserStore.get uref >>= \case - Nothing -> maybe (pure Nothing) withEmailOnly $ Brig.urefToEmail uref - Just uid -> pure (Just uid) - - withEmailOnly :: Email -> Sem r (Maybe UserId) - withEmailOnly eml = maybe inbrig (pure . Just) =<< inspar - where - -- FUTUREWORK: we could also always lookup brig, that's simpler and possibly faster, - -- and it never should be visible in spar, but not in brig. - inspar, inbrig :: Sem r (Maybe UserId) - inspar = ScimExternalIdStore.lookup stiTeam eml - inbrig = userId . accountUser <$$> BrigAccess.getByEmail eml +scimFindUserByExternalId mIdpConfig stiTeam eid = do + mViaEid :: Maybe UserId <- MaybeT $ Just <$> lift (ScimExternalIdStore.lookup stiTeam eid) + uid <- case mViaEid of + Nothing -> do + veid <- MaybeT . lift $ either (const Nothing) Just <$> runError @Scim.ScimError (mkValidScimId mIdpConfig (Just eid) (emailAddressText eid)) + MaybeT . lift $ do + -- there are a few ways to find a user. this should all be redundant, especially the where + -- we lookup a user from brig by email, throw it away and only keep the uid, and then use + -- the uid to lookup the account again. but cassandra, and also reasons. + mViaEmail :: Maybe UserId <- join <$> (for (justHere veid.validScimIdAuthInfo) ((userId <$$>) . BrigAccess.getByEmail)) + mViaUref :: Maybe UserId <- join <$> (for (justThere veid.validScimIdAuthInfo) SAMLUserStore.get) + pure $ mViaEmail <|> mViaUref + Just uid -> pure uid + acc <- MaybeT . lift . BrigAccess.getAccount Brig.WithPendingInvitations $ uid + getUserById mIdpConfig stiTeam (userId acc) logFilter :: Filter -> (Msg -> Msg) logFilter (FilterAttrCompare attr op val) = @@ -1182,58 +1176,3 @@ logFilter (FilterAttrCompare attr op val) = "sha256 " <> sha256String s <> (if isJust (UUID.fromText s) then " original is a UUID" else "") - -{- TODO: might be useful later. -~~~~~~~~~~~~~~~~~~~~~~~~~ - --- | Parse a name from a user profile into an SCIM name (Okta wants given --- name and last name, so we break our names up to satisfy Okta). --- --- TODO: use the same algorithm as Wire clients use. -toScimName :: Name -> Scim.Name -toScimName (Name name) = - Scim.Name - { Scim.formatted = Just name - , Scim.givenName = Just first - , Scim.familyName = if Text.null rest then Nothing else Just rest - , Scim.middleName = Nothing - , Scim.honorificPrefix = Nothing - , Scim.honorificSuffix = Nothing - } - where - (first, Text.drop 1 -> rest) = Text.breakOn " " name - --- | Convert from the Wire phone type to the SCIM phone type. -toScimPhone :: Phone -> Scim.Phone -toScimPhone (Phone phone) = - Scim.Phone - { Scim.typ = Nothing - , Scim.value = Just phone - } - --- | Convert from the Wire email type to the SCIM email type. -toScimEmail :: Email -> Scim.Email -toScimEmail (Email eLocal eDomain) = - Scim.Email - { Scim.typ = Nothing - , Scim.value = Scim.EmailAddress2 - (unsafeEmailAddress (encodeUtf8 eLocal) (encodeUtf8 eDomain)) - , Scim.primary = Just True - } - --} - --- Note [error handling] --- ~~~~~~~~~~~~~~~~~ --- --- FUTUREWORK: There are two problems with error handling here: --- --- 1. We want all errors originating from SCIM handlers to be thrown as SCIM --- errors, not as Spar errors. Currently errors thrown from things like --- 'getTeamMembers' will look like Spar errors and won't be wrapped into --- the 'ScimError' type. This might or might not be important, depending --- on what is expected by apps that use the SCIM interface. --- --- 2. We want generic error descriptions in response bodies, while still --- logging nice error messages internally. The current messages might --- be giving too many internal details away. diff --git a/services/spar/src/Spar/Sem/BrigAccess.hs b/services/spar/src/Spar/Sem/BrigAccess.hs index 1936116030f..a9f3b69fe5a 100644 --- a/services/spar/src/Spar/Sem/BrigAccess.hs +++ b/services/spar/src/Spar/Sem/BrigAccess.hs @@ -28,7 +28,7 @@ module Spar.Sem.BrigAccess setName, setHandle, setManagedBy, - setVeid, + setSSOId, setRichInfo, setLocale, getRichInfo, @@ -44,9 +44,9 @@ module Spar.Sem.BrigAccess where import Brig.Types.Intra -import Brig.Types.User import Data.Code as Code import Data.Handle (Handle) +import Data.HavePendingInvitations import Data.Id (TeamId, UserId) import Data.Misc (PlainTextPassword6) import Imports @@ -55,23 +55,20 @@ import qualified SAML2.WebSSO as SAML import Web.Cookie import Wire.API.Locale import Wire.API.Team.Role -import Wire.API.User (AccountStatus (..), DeleteUserResult, VerificationAction) -import Wire.API.User.Identity -import Wire.API.User.Profile +import Wire.API.User import Wire.API.User.RichInfo as RichInfo -import Wire.API.User.Scim (ValidExternalId (..)) data BrigAccess m a where CreateSAML :: SAML.UserRef -> UserId -> TeamId -> Name -> ManagedBy -> Maybe Handle -> Maybe RichInfo -> Maybe Locale -> Role -> BrigAccess m UserId - CreateNoSAML :: Email -> UserId -> TeamId -> Name -> Maybe Locale -> Role -> BrigAccess m UserId - UpdateEmail :: UserId -> Email -> BrigAccess m () - GetAccount :: HavePendingInvitations -> UserId -> BrigAccess m (Maybe UserAccount) - GetByHandle :: Handle -> BrigAccess m (Maybe UserAccount) - GetByEmail :: Email -> BrigAccess m (Maybe UserAccount) + CreateNoSAML :: Text -> EmailAddress -> UserId -> TeamId -> Name -> Maybe Locale -> Role -> BrigAccess m UserId + UpdateEmail :: UserId -> EmailAddress -> BrigAccess m () + GetAccount :: HavePendingInvitations -> UserId -> BrigAccess m (Maybe User) + GetByHandle :: Handle -> BrigAccess m (Maybe User) + GetByEmail :: EmailAddress -> BrigAccess m (Maybe User) SetName :: UserId -> Name -> BrigAccess m () SetHandle :: UserId -> Handle {- not 'HandleUpdate'! -} -> BrigAccess m () SetManagedBy :: UserId -> ManagedBy -> BrigAccess m () - SetVeid :: UserId -> ValidExternalId -> BrigAccess m () + SetSSOId :: UserId -> UserSSOId -> BrigAccess m () SetRichInfo :: UserId -> RichInfo -> BrigAccess m () SetLocale :: UserId -> Maybe Locale -> BrigAccess m () GetRichInfo :: UserId -> BrigAccess m RichInfo diff --git a/services/spar/src/Spar/Sem/BrigAccess/Http.hs b/services/spar/src/Spar/Sem/BrigAccess/Http.hs index a1e5f8d04be..11ab18c6222 100644 --- a/services/spar/src/Spar/Sem/BrigAccess/Http.hs +++ b/services/spar/src/Spar/Sem/BrigAccess/Http.hs @@ -44,7 +44,7 @@ brigAccessToHttp mgr req = interpret $ viaRunHttp (RunHttpEnv mgr req) . \case CreateSAML u itlu itlt n m h ri ml r -> Intra.createBrigUserSAML u itlu itlt n m h ri ml r - CreateNoSAML e uid itlt n ml r -> Intra.createBrigUserNoSAML e uid itlt n ml r + CreateNoSAML eid e uid itlt n ml r -> Intra.createBrigUserNoSAML eid e uid itlt n ml r UpdateEmail itlu e -> Intra.updateEmail itlu e GetAccount h itlu -> Intra.getBrigUserAccount h itlu GetByHandle h -> Intra.getBrigUserByHandle h @@ -52,7 +52,7 @@ brigAccessToHttp mgr req = SetName itlu n -> Intra.setBrigUserName itlu n SetHandle itlu h -> Intra.setBrigUserHandle itlu h SetManagedBy itlu m -> Intra.setBrigUserManagedBy itlu m - SetVeid itlu v -> Intra.setBrigUserVeid itlu v + SetSSOId itlu v -> Intra.setBrigUserSSOId itlu v SetRichInfo itlu r -> Intra.setBrigUserRichInfo itlu r SetLocale itlu l -> Intra.setBrigUserLocale itlu l GetRichInfo itlu -> Intra.getBrigUserRichInfo itlu diff --git a/services/spar/src/Spar/Sem/ScimExternalIdStore.hs b/services/spar/src/Spar/Sem/ScimExternalIdStore.hs index 604c089d393..c4bb2b54ed6 100644 --- a/services/spar/src/Spar/Sem/ScimExternalIdStore.hs +++ b/services/spar/src/Spar/Sem/ScimExternalIdStore.hs @@ -28,20 +28,20 @@ module Spar.Sem.ScimExternalIdStore where import Data.Id (TeamId, UserId) +import Data.Text import Imports (Maybe, Show) import Polysemy import Polysemy.Check (deriveGenericK) import Spar.Scim.Types -import Wire.API.User.Identity (Email) import Wire.API.User.Scim data ScimExternalIdStore m a where - Insert :: TeamId -> Email -> UserId -> ScimExternalIdStore m () - Lookup :: TeamId -> Email -> ScimExternalIdStore m (Maybe UserId) - Delete :: TeamId -> Email -> ScimExternalIdStore m () - -- NB: the fact that we are using `Email` in some cases here and `ValidExternalId` in others has historical reasons (this table was only used for non-saml accounts in the past, now it is used for *all* scim-managed accounts). the interface would work equally well with just `Text` here (for unvalidated scim external id). - InsertStatus :: TeamId -> ValidExternalId -> UserId -> ScimUserCreationStatus -> ScimExternalIdStore m () - LookupStatus :: TeamId -> ValidExternalId -> ScimExternalIdStore m (Maybe (UserId, ScimUserCreationStatus)) + Insert :: TeamId -> Text -> UserId -> ScimExternalIdStore m () + Lookup :: TeamId -> Text -> ScimExternalIdStore m (Maybe UserId) + Delete :: TeamId -> Text -> ScimExternalIdStore m () + -- NB: the fact that we are using `Email` in some cases here and `ValidScimId` in others has historical reasons (this table was only used for non-saml accounts in the past, now it is used for *all* scim-managed accounts). the interface would work equally well with just `Text` here (for unvalidated scim external id). + InsertStatus :: TeamId -> ValidScimId -> UserId -> ScimUserCreationStatus -> ScimExternalIdStore m () + LookupStatus :: TeamId -> ValidScimId -> ScimExternalIdStore m (Maybe (UserId, ScimUserCreationStatus)) deriving instance Show (ScimExternalIdStore m a) diff --git a/services/spar/src/Spar/Sem/ScimExternalIdStore/Cassandra.hs b/services/spar/src/Spar/Sem/ScimExternalIdStore/Cassandra.hs index 73c192dafdf..42d098dfe33 100644 --- a/services/spar/src/Spar/Sem/ScimExternalIdStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/ScimExternalIdStore/Cassandra.hs @@ -30,8 +30,7 @@ import Polysemy import Spar.Data.Instances () import Spar.Scim.Types (ScimUserCreationStatus (ScimUserCreated)) import Spar.Sem.ScimExternalIdStore (ScimExternalIdStore (..)) -import Wire.API.User.Identity -import Wire.API.User.Scim (ValidExternalId, runValidExternalIdUnsafe) +import Wire.API.User.Scim (ValidScimId (..)) scimExternalIdStoreToCassandra :: forall m r a. @@ -41,9 +40,9 @@ scimExternalIdStoreToCassandra :: scimExternalIdStoreToCassandra = interpret $ embed @m . \case - Insert tid em uid -> insertScimExternalId tid em uid - Lookup tid em -> lookupScimExternalId tid em - Delete tid em -> deleteScimExternalId tid em + Insert tid eid uid -> insertScimExternalId tid eid uid + Lookup tid eid -> lookupScimExternalId tid eid + Delete tid eid -> deleteScimExternalId tid eid InsertStatus tid veid buid status -> insertScimExternalIdStatus tid veid buid status LookupStatus tid veid -> lookupScimExternalIdStatus tid veid @@ -52,38 +51,38 @@ scimExternalIdStoreToCassandra = -- 'UserId' here. (Note that since there is no associated IdP, the externalId is required to -- be an email address, so we enforce that in the type signature, even though we only use it -- as a 'Text'.) -insertScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> Email -> UserId -> m () -insertScimExternalId tid (fromEmail -> email) uid = - retry x5 . write insert $ params LocalQuorum (tid, email, uid) +insertScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> Text -> UserId -> m () +insertScimExternalId tid eid uid = + retry x5 . write insert $ params LocalQuorum (tid, eid, uid) where insert :: PrepQuery W (TeamId, Text, UserId) () insert = "INSERT INTO scim_external (team, external_id, user) VALUES (?, ?, ?)" -- | The inverse of 'insertScimExternalId'. -lookupScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> Email -> m (Maybe UserId) -lookupScimExternalId tid (fromEmail -> email) = runIdentity <$$> (retry x1 . query1 sel $ params LocalQuorum (tid, email)) +lookupScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> Text -> m (Maybe UserId) +lookupScimExternalId tid eid = runIdentity <$$> (retry x1 . query1 sel $ params LocalQuorum (tid, eid)) where sel :: PrepQuery R (TeamId, Text) (Identity UserId) sel = "SELECT user FROM scim_external WHERE team = ? and external_id = ?" -- | The other inverse of 'insertScimExternalId' :). -deleteScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> Email -> m () -deleteScimExternalId tid (fromEmail -> email) = - retry x5 . write delete $ params LocalQuorum (tid, email) +deleteScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> Text -> m () +deleteScimExternalId tid eid = + retry x5 . write delete $ params LocalQuorum (tid, eid) where delete :: PrepQuery W (TeamId, Text) () delete = "DELETE FROM scim_external WHERE team = ? and external_id = ?" -insertScimExternalIdStatus :: (HasCallStack, MonadClient m) => TeamId -> ValidExternalId -> UserId -> ScimUserCreationStatus -> m () +insertScimExternalIdStatus :: (HasCallStack, MonadClient m) => TeamId -> ValidScimId -> UserId -> ScimUserCreationStatus -> m () insertScimExternalIdStatus tid veid uid status = - retry x5 . write insert $ params LocalQuorum (tid, runValidExternalIdUnsafe veid, uid, status) + retry x5 . write insert $ params LocalQuorum (tid, validScimIdExternal veid, uid, status) where insert :: PrepQuery W (TeamId, Text, UserId, ScimUserCreationStatus) () insert = "INSERT INTO scim_external (team, external_id, user, creation_status) VALUES (?, ?, ?, ?)" -lookupScimExternalIdStatus :: (HasCallStack, MonadClient m) => TeamId -> ValidExternalId -> m (Maybe (UserId, ScimUserCreationStatus)) +lookupScimExternalIdStatus :: (HasCallStack, MonadClient m) => TeamId -> ValidScimId -> m (Maybe (UserId, ScimUserCreationStatus)) lookupScimExternalIdStatus tid veid = do - mResult <- retry x1 . query1 sel $ params LocalQuorum (tid, runValidExternalIdUnsafe veid) + mResult <- retry x1 . query1 sel $ params LocalQuorum (tid, validScimIdExternal veid) -- if the user exists and the status is not present, we assume the user was created successfully pure $ mResult <&> second (fromMaybe ScimUserCreated) where diff --git a/services/spar/src/Spar/Sem/ScimExternalIdStore/Mem.hs b/services/spar/src/Spar/Sem/ScimExternalIdStore/Mem.hs index 3af1a26437d..5ab14ccd4af 100644 --- a/services/spar/src/Spar/Sem/ScimExternalIdStore/Mem.hs +++ b/services/spar/src/Spar/Sem/ScimExternalIdStore/Mem.hs @@ -27,18 +27,17 @@ import qualified Data.Map as M import Imports import Polysemy import Polysemy.State -import Spar.Scim (runValidExternalIdUnsafe) import Spar.Scim.Types (ScimUserCreationStatus) import Spar.Sem.ScimExternalIdStore -import Wire.API.User (fromEmail) +import Wire.API.User.Scim (ValidScimId (..)) scimExternalIdStoreToMem :: Sem (ScimExternalIdStore ': r) a -> Sem r (Map (TeamId, Text) (UserId, Maybe ScimUserCreationStatus), a) scimExternalIdStoreToMem = (runState mempty .) $ reinterpret $ \case - Insert tid em uid -> modify $ M.insert (tid, fromEmail em) (uid, Nothing) - Lookup tid em -> fmap fst <$> gets (M.lookup (tid, fromEmail em)) - Delete tid em -> modify $ M.delete (tid, fromEmail em) - InsertStatus tid veid uid status -> modify $ M.insert (tid, runValidExternalIdUnsafe veid) (uid, Just status) - LookupStatus tid veid -> ((=<<) (\(uid, mStatus) -> (uid,) <$> mStatus)) <$> gets (M.lookup (tid, runValidExternalIdUnsafe veid)) + Insert tid eid uid -> modify $ M.insert (tid, eid) (uid, Nothing) + Lookup tid eid -> fmap fst <$> gets (M.lookup (tid, eid)) + Delete tid eid -> modify $ M.delete (tid, eid) + InsertStatus tid veid uid status -> modify $ M.insert (tid, veid.validScimIdExternal) (uid, Just status) + LookupStatus tid veid -> ((=<<) (\(uid, mStatus) -> (uid,) <$> mStatus)) <$> gets (M.lookup (tid, veid.validScimIdExternal)) diff --git a/services/spar/src/Spar/Sem/ScimTokenStore.hs b/services/spar/src/Spar/Sem/ScimTokenStore.hs index eb4ec41735d..03014de6974 100644 --- a/services/spar/src/Spar/Sem/ScimTokenStore.hs +++ b/services/spar/src/Spar/Sem/ScimTokenStore.hs @@ -22,13 +22,14 @@ module Spar.Sem.ScimTokenStore insert, lookup, lookupByTeam, + updateName, delete, deleteByTeam, ) where import Data.Id -import Imports (Maybe) +import Imports hiding (lookup) import Polysemy import Wire.API.User.Scim @@ -36,6 +37,7 @@ data ScimTokenStore m a where Insert :: ScimToken -> ScimTokenInfo -> ScimTokenStore m () Lookup :: ScimToken -> ScimTokenStore m (Maybe ScimTokenInfo) LookupByTeam :: TeamId -> ScimTokenStore m [ScimTokenInfo] + UpdateName :: TeamId -> ScimTokenId -> Text -> ScimTokenStore m () Delete :: TeamId -> ScimTokenId -> ScimTokenStore m () DeleteByTeam :: TeamId -> ScimTokenStore m () diff --git a/services/spar/src/Spar/Sem/ScimTokenStore/Cassandra.hs b/services/spar/src/Spar/Sem/ScimTokenStore/Cassandra.hs index 6f56b34e77c..70dc4e223d0 100644 --- a/services/spar/src/Spar/Sem/ScimTokenStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/ScimTokenStore/Cassandra.hs @@ -48,8 +48,9 @@ scimTokenStoreToCassandra = Insert st sti -> insertScimToken st sti Lookup st -> lookupScimToken st LookupByTeam tid -> getScimTokens tid - Delete tid ur -> deleteScimToken tid ur - DeleteByTeam tid -> deleteTeamScimTokens tid + UpdateName team token name -> updateScimTokenName team token name + Delete team token -> deleteScimToken team token + DeleteByTeam team -> deleteTeamScimTokens team ---------------------------------------------------------------------- -- SCIM auth @@ -67,25 +68,25 @@ insertScimToken token ScimTokenInfo {..} = retry x5 . batch $ do setType BatchLogged setConsistency LocalQuorum let tokenHash = hashScimToken token - addPrepQuery insByToken (ScimTokenLookupKeyHashed tokenHash, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr) - addPrepQuery insByTeam (ScimTokenLookupKeyHashed tokenHash, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr) + addPrepQuery insByToken (ScimTokenLookupKeyHashed tokenHash, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr, Just stiName) + addPrepQuery insByTeam (ScimTokenLookupKeyHashed tokenHash, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr, Just stiName) insByToken, insByTeam :: PrepQuery W ScimTokenRow () insByToken = [r| INSERT INTO team_provisioning_by_token - (token_, team, id, created_at, idp, descr) - VALUES (?, ?, ?, ?, ?, ?) + (token_, team, id, created_at, idp, descr, name) + VALUES (?, ?, ?, ?, ?, ?, ?) |] insByTeam = [r| INSERT INTO team_provisioning_by_team - (token_, team, id, created_at, idp, descr) - VALUES (?, ?, ?, ?, ?, ?) + (token_, team, id, created_at, idp, descr, name) + VALUES (?, ?, ?, ?, ?, ?, ?) |] scimTokenLookupKey :: ScimTokenRow -> ScimTokenLookupKey -scimTokenLookupKey (key, _, _, _, _, _) = key +scimTokenLookupKey (key, _, _, _, _, _, _) = key -- | Check whether a token exists and if yes, what team and IdP are -- associated with it. @@ -110,7 +111,7 @@ lookupScimToken token = do sel :: PrepQuery R (ScimTokenHash, ScimToken) ScimTokenRow sel = [r| - SELECT token_, team, id, created_at, idp, descr + SELECT token_, team, id, created_at, idp, descr, name FROM team_provisioning_by_token WHERE token_ in (?, ?) |] @@ -130,9 +131,9 @@ connvertPlaintextToken token ScimTokenInfo {..} = retry x5 . batch $ do setConsistency LocalQuorum let tokenHash = hashScimToken token -- enter by new lookup key - addPrepQuery insByToken (ScimTokenLookupKeyHashed tokenHash, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr) + addPrepQuery insByToken (ScimTokenLookupKeyHashed tokenHash, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr, Just stiName) -- update info table - addPrepQuery insByTeam (ScimTokenLookupKeyHashed tokenHash, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr) + addPrepQuery insByTeam (ScimTokenLookupKeyHashed tokenHash, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr, Just stiName) -- remove old lookup key addPrepQuery delByTokenLookup (Identity (ScimTokenLookupKeyPlaintext token)) @@ -145,12 +146,12 @@ getScimTokens team = do -- We don't need pagination here because the limit should be pretty low -- (e.g. 16). If the limit grows, we might have to introduce pagination. rows <- retry x1 . query sel $ params LocalQuorum (Identity team) - pure $ sortOn stiCreatedAt $ map fromScimTokenRow rows + pure $ sortOn (.stiCreatedAt) $ map fromScimTokenRow rows where sel :: PrepQuery R (Identity TeamId) ScimTokenRow sel = [r| - SELECT token_, team, id, created_at, idp, descr + SELECT token_, team, id, created_at, idp, descr, name FROM team_provisioning_by_team WHERE team = ? |] @@ -168,13 +169,13 @@ deleteScimToken team tokenid = do addPrepQuery delById (team, tokenid) for_ mbToken $ \(Identity key) -> addPrepQuery delByTokenLookup (Identity key) - where - selById :: PrepQuery R (TeamId, ScimTokenId) (Identity ScimTokenLookupKey) - selById = - [r| - SELECT token_ FROM team_provisioning_by_team - WHERE team = ? AND id = ? - |] + +selById :: PrepQuery R (TeamId, ScimTokenId) (Identity ScimTokenLookupKey) +selById = + [r| + SELECT token_ FROM team_provisioning_by_team + WHERE team = ? AND id = ? +|] delById :: PrepQuery W (TeamId, ScimTokenId) () delById = @@ -208,8 +209,41 @@ deleteTeamScimTokens team = do delByTeam :: PrepQuery W (Identity TeamId) () delByTeam = "DELETE FROM team_provisioning_by_team WHERE team = ?" -type ScimTokenRow = (ScimTokenLookupKey, TeamId, ScimTokenId, UTCTime, Maybe SAML.IdPId, Text) +updateScimTokenName :: (HasCallStack, MonadClient m) => TeamId -> ScimTokenId -> Text -> m () +updateScimTokenName team tokenid name = do + mbToken <- retry x1 . query1 selById $ params LocalQuorum (team, tokenid) + retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + addPrepQuery updateNameById (name, team, tokenid) + for_ mbToken $ \(Identity key) -> + addPrepQuery updateNameByTokenLookup (name, key) + where + updateNameById :: PrepQuery W (Text, TeamId, ScimTokenId) () + updateNameById = + [r| + UPDATE team_provisioning_by_team + SET name = ? + WHERE team = ? AND id = ? + |] + + updateNameByTokenLookup :: PrepQuery W (Text, ScimTokenLookupKey) () + updateNameByTokenLookup = + [r| + UPDATE team_provisioning_by_token + SET name = ? + WHERE token_ = ? + |] + +type ScimTokenRow = (ScimTokenLookupKey, TeamId, ScimTokenId, UTCTime, Maybe SAML.IdPId, Text, Maybe Text) fromScimTokenRow :: ScimTokenRow -> ScimTokenInfo -fromScimTokenRow (_, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr) = - ScimTokenInfo {..} +fromScimTokenRow (_, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr, stiName) = + ScimTokenInfo + { stiId, + stiTeam, + stiCreatedAt, + stiIdP, + stiDescr, + stiName = fromMaybe (idToText stiId) stiName + } diff --git a/services/spar/src/Spar/Sem/ScimTokenStore/Mem.hs b/services/spar/src/Spar/Sem/ScimTokenStore/Mem.hs index 255d9a8e2ad..48b869fb0f0 100644 --- a/services/spar/src/Spar/Sem/ScimTokenStore/Mem.hs +++ b/services/spar/src/Spar/Sem/ScimTokenStore/Mem.hs @@ -36,6 +36,10 @@ scimTokenStoreToMem = (runState mempty .) $ reinterpret $ \case Insert st sti -> modify $ M.insert st sti Lookup st -> gets $ M.lookup st - LookupByTeam tid -> gets $ filter ((== tid) . stiTeam) . M.elems - Delete tid stid -> modify $ M.filter $ \sti -> not $ stiTeam sti == tid && stiId sti == stid - DeleteByTeam tid -> modify $ M.filter ((/= tid) . stiTeam) + LookupByTeam tid -> gets $ filter ((== tid) . (.stiTeam)) . M.elems + UpdateName tid stid name -> modify $ M.map $ \sti -> + if (.stiTeam) sti == tid && (.stiId) sti == stid + then sti {stiName = name} + else sti + Delete tid stid -> modify $ M.filter $ \sti -> not $ (.stiTeam) sti == tid && (.stiId) sti == stid + DeleteByTeam tid -> modify $ M.filter ((/= tid) . (.stiTeam)) diff --git a/services/spar/test-integration/Main.hs b/services/spar/test-integration/Main.hs index 3eefa283be5..f1a42c99847 100644 --- a/services/spar/test-integration/Main.hs +++ b/services/spar/test-integration/Main.hs @@ -1,4 +1,5 @@ {-# LANGUAGE RecordWildCards #-} +{-# OPTIONS_GHC -Wno-deprecations #-} -- This file is part of the Wire Server implementation. -- @@ -29,7 +30,7 @@ -- the solution: https://github.com/hspec/hspec/pull/397. module Main where -import Control.Concurrent.Async +import Control.Concurrent.Async (concurrently_) import Control.Lens ((.~), (^.)) import Data.Text (pack) import Imports @@ -38,10 +39,9 @@ import Spar.Run (mkApp) import System.Environment (withArgs) import System.Random (randomRIO) import Test.Hspec -import Test.Hspec.Core.Format -import Test.Hspec.Core.Runner import Test.Hspec.JUnit import Test.Hspec.JUnit.Config.Env +import Test.Hspec.Runner (Config (configAvailableFormatters), defaultConfig, hspecWith) import qualified Test.LoggingSpec import qualified Test.MetricsSpec import qualified Test.Spar.APISpec @@ -58,8 +58,13 @@ main :: IO () main = do (wireArgs, hspecArgs) <- partitionArgs <$> getArgs let env = withArgs wireArgs mkEnvFromOptions - cfg <- hspecConfig - withArgs hspecArgs . hspecWith cfg $ do + -- FUTUREWORK(mangoiv): we should remove the deprecated module and instaed move to this config, however, this + -- needs check of whether it modifies the behaviour + -- junitConfig <- envJUnitConfig + -- withArgs hspecArgs . hspec . add junitConfig $ do + + conf <- hspecConfig + withArgs hspecArgs . hspecWith conf $ do for_ [minBound ..] $ \idpApiVersion -> do describe (show idpApiVersion) . beforeAll (env <&> teWireIdPAPIVersion .~ idpApiVersion) . afterAll destroyEnv $ do mkspecMisc @@ -77,7 +82,6 @@ hspecConfig = do : configAvailableFormatters defaultConfig } where - checksAndJUnitFormatter :: JUnitConfig -> FormatConfig -> IO Format checksAndJUnitFormatter junitConfig config = do junit <- junitFormat junitConfig config let checksFormatter = fromJust (lookup "checks" $ configAvailableFormatters defaultConfig) diff --git a/services/spar/test-integration/Test/Spar/APISpec.hs b/services/spar/test-integration/Test/Spar/APISpec.hs index 90895f2164c..9f9b0773d14 100644 --- a/services/spar/test-integration/Test/Spar/APISpec.hs +++ b/services/spar/test-integration/Test/Spar/APISpec.hs @@ -87,6 +87,7 @@ import qualified Web.Scim.Class.User as Scim import qualified Web.Scim.Schema.Common as Scim import qualified Web.Scim.Schema.Meta as Scim import qualified Web.Scim.Schema.User as Scim +import qualified Web.Scim.Schema.User.Email as Scim import Wire.API.Team.Member (newTeamMemberDeleteData, rolePermissions) import Wire.API.Team.Permission hiding (self) import Wire.API.Team.Role @@ -94,7 +95,7 @@ import Wire.API.User import Wire.API.User.Client import Wire.API.User.Client.Prekey import Wire.API.User.IdentityProvider -import Wire.API.User.Scim +import Wire.API.User.Scim hiding (handle) spec :: SpecWith TestEnv spec = do @@ -293,6 +294,7 @@ specFinalizeLogin = do authnresp <- runSimpleSP $ mkAuthnResponse privcreds idp3 spmeta authnreq True loginSuccess =<< submitAuthnResponse tid3 authnresp + -- @SF.Channel @TSFI.RESTfulAPI @S2 @S3 -- Do not authenticate if SSO IdP response is for different team context "rejectsSAMLResponseInWrongTeam" $ do it "fails" $ do @@ -319,6 +321,8 @@ specFinalizeLogin = do authnresp <- runSimpleSP $ mkAuthnResponseWithSubj subj privcreds idp2 spmeta authnreq True loginFailure =<< submitAuthnResponse tid2 authnresp + -- @END + context "user is created once, then deleted in team settings, then can login again." $ do it "responds with 'allowed'" $ do (ownerid, teamid) <- callCreateUserWithTeam @@ -516,7 +520,7 @@ specCRUDIdentityProvider = do (owner :: UserId, _teamid :: TeamId) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) callIdpGetAll (env ^. teSpar) (Just owner) - `shouldRespondWith` (null . _providers) + `shouldRespondWith` (null . providers) context "some idps are registered" $ do context "client is team owner with email" $ do it "returns a non-empty empty list" $ do @@ -525,7 +529,7 @@ specCRUDIdentityProvider = do (owner, _tid) <- callCreateUserWithTeam _ <- registerTestIdPFrom metadata (env ^. teMgr) owner (env ^. teSpar) callIdpGetAll (env ^. teSpar) (Just owner) - `shouldRespondWith` (not . null . _providers) + `shouldRespondWith` (not . null . providers) context "client is team owner without email" $ do it "returns a non-empty empty list" $ do env <- ask @@ -534,7 +538,7 @@ specCRUDIdentityProvider = do idp <- registerTestIdPFrom metadata (env ^. teMgr) firstOwner (env ^. teSpar) ssoOwner <- mkSsoOwner firstOwner tid idp privcreds callIdpGetAll (env ^. teSpar) (Just ssoOwner) - `shouldRespondWith` (not . null . _providers) + `shouldRespondWith` (not . null . providers) describe "DELETE /identity-providers/:idp" $ do testGetPutDelete (\o t i _ -> callIdpDelete' o t i) context "zuser has wrong team" $ do @@ -1053,8 +1057,19 @@ specCRUDIdentityProvider = do respId <- listUsers tok (Just (filterBy "externalId" externalId)) respHandle <- listUsers tok (Just (filterBy "userName" handle')) liftIO $ do - respId `shouldBe` [target] - respHandle `shouldBe` [target] + let patched = case target of + Scim.WithMeta _m (Scim.WithId i u) -> + let u' :: Scim.User SparTag + u' = case emailAddress (cs externalId) of + -- if the externalId is an email, and the email field was + -- empty, the scim response from spar contains the externalId + -- (parsed) in the emails field. + Just e -> u {Scim.emails = [Scim.Email Nothing (Scim.EmailAddress e) Nothing]} + Nothing -> u + in -- don't compare meta, or you need to update the ETag in version because email may have changed. + Scim.WithId i u' + (Scim.thing <$> respId) `shouldBe` [patched] + (Scim.thing <$> respHandle) `shouldBe` [patched] checkScimSearch scimStoredUser scimUser updateOrReplaceIdps (owner1, idp1, idpmeta1) @@ -1271,7 +1286,7 @@ specScimAndSAML = do userid' <- getUserIdViaRef' userref liftIO $ ('i', userid') `shouldBe` ('i', Just userid) userssoid <- getSsoidViaSelf' userid - liftIO $ ('r', preview veidUref <$$> (Intra.veidFromUserSSOId <$> userssoid)) `shouldBe` ('r', Just (Right (Just userref))) + liftIO $ ('r', veidUref <$$> ((`Intra.veidFromUserSSOId` Nothing) <$> userssoid)) `shouldBe` ('r', Just (Right (Just userref))) -- login a user for the first time with the scim-supplied credentials authnreq <- negotiateAuthnRequest idp spmeta <- getTestSPMetadata tid @@ -1513,7 +1528,7 @@ getSsoidViaAuthResp :: (HasCallStack) => SignedAuthnResponse -> TestSpar UserSSO getSsoidViaAuthResp aresp = do parsed :: AuthnResponse <- either error pure . parseFromDocument $ fromSignedAuthnResponse aresp - either error (pure . Intra.veidToUserSSOId . UrefOnly) $ getUserRef parsed + either error (pure . UserSSOId) $ getUserRef parsed specSparUserMigration :: SpecWith TestEnv specSparUserMigration = do @@ -1662,6 +1677,7 @@ specReAuthSsoUserWithPassword = ---------------------------------------------------------------------- -- tests for bsi audit +-- @SF.Channel @TSFI.RESTfulAPI @S2 @S3 testRejectsSAMLResponseSayingAccessNotGranted :: TestSpar () testRejectsSAMLResponseSayingAccessNotGranted = do (user, tid) <- callCreateUserWithTeam @@ -1683,6 +1699,10 @@ testRejectsSAMLResponseSayingAccessNotGranted = do bdy `shouldContain` "}, receiverOrigin)" hasPersistentCookieHeader sparresp `shouldBe` Left "no set-cookie header" +-- @END + +-- @SF.Channel @TSFI.RESTfulAPI @S2 @S3 +-- -- Do not authenticate if SSO IdP response is for unknown issuer testRejectsSAMLResponseFromWrongIssuer :: TestSpar () testRejectsSAMLResponseFromWrongIssuer = do @@ -1707,6 +1727,10 @@ testRejectsSAMLResponseFromWrongIssuer = do submitaresp checkresp +-- @END + +-- @SF.Channel @TSFI.RESTfulAPI @S2 @S3 +-- -- Do not authenticate if SSO IdP response is signed with wrong key testRejectsSAMLResponseSignedWithWrongKey :: TestSpar () testRejectsSAMLResponseSignedWithWrongKey = do @@ -1724,6 +1748,10 @@ testRejectsSAMLResponseSignedWithWrongKey = do checkresp sparresp = statusCode sparresp `shouldBe` 400 checkSamlFlow mkareq mkaresp submitaresp checkresp +-- @END + +-- @SF.Channel @TSFI.RESTfulAPI @S2 @S3 +-- -- Do not authenticate if SSO IdP response has no corresponding request anymore testRejectsSAMLResponseIfRequestIsStale :: TestSpar () testRejectsSAMLResponseIfRequestIsStale = do @@ -1739,6 +1767,10 @@ testRejectsSAMLResponseIfRequestIsStale = do (cs . fromJust . responseBody $ sparresp) `shouldContain` "bad InResponseTo attribute(s)" checkSamlFlow mkareq mkaresp submitaresp checkresp +-- @END + +-- @SF.Channel @TSFI.RESTfulAPI @S2 @S3 +-- -- Do not authenticate if SSO IdP response is gone missing testRejectsSAMLResponseIfResponseIsStale :: TestSpar () testRejectsSAMLResponseIfResponseIsStale = do @@ -1752,6 +1784,8 @@ testRejectsSAMLResponseIfResponseIsStale = do (cs . fromJust . responseBody $ sparresp) `shouldContain` "wire:sso:error:forbidden" checkSamlFlow mkareq mkaresp submitaresp checkresp +-- @END + ---------------------------------------------------------------------- -- Helpers diff --git a/services/spar/test-integration/Test/Spar/DataSpec.hs b/services/spar/test-integration/Test/Spar/DataSpec.hs index 25f1ee2f468..f310a265399 100644 --- a/services/spar/test-integration/Test/Spar/DataSpec.hs +++ b/services/spar/test-integration/Test/Spar/DataSpec.hs @@ -48,7 +48,6 @@ import Web.Scim.Schema.Common as Scim.Common import Web.Scim.Schema.Meta as Scim.Meta import Wire.API.User.IdentityProvider import Wire.API.User.Saml -import Wire.API.User.Scim spec :: SpecWith TestEnv spec = do @@ -248,20 +247,20 @@ testDeleteTeam = it "cleans up all the right tables after deletion" $ do liftIO $ tokens `shouldBe` [] -- The users from 'user': do - mbUser1 <- case veidFromUserSSOId ssoid1 of + mbUser1 <- case veidFromUserSSOId ssoid1 Nothing of Right veid -> runSpar $ - runValidExternalIdEither + runValidScimIdEither SAMLUserStore.get undefined -- could be @Data.lookupScimExternalId@, but we don't hit that path. veid Left _email -> undefined -- runSparCass . Data.lookupScimExternalId . fromEmail $ _email liftIO $ mbUser1 `shouldBe` Nothing do - mbUser2 <- case veidFromUserSSOId ssoid2 of + mbUser2 <- case veidFromUserSSOId ssoid2 Nothing of Right veid -> runSpar $ - runValidExternalIdEither + runValidScimIdEither SAMLUserStore.get undefined veid diff --git a/services/spar/test-integration/Test/Spar/Intra/BrigSpec.hs b/services/spar/test-integration/Test/Spar/Intra/BrigSpec.hs index 822a4ee99bf..c97ad084a90 100644 --- a/services/spar/test-integration/Test/Spar/Intra/BrigSpec.hs +++ b/services/spar/test-integration/Test/Spar/Intra/BrigSpec.hs @@ -25,6 +25,7 @@ import Data.Id (Id (Id), UserId) import qualified Data.UUID as UUID import Imports hiding (head) import qualified Spar.Intra.BrigApp as Intra +import Spar.Sem.BrigAccess (getAccount) import qualified Spar.Sem.BrigAccess as BrigAccess import Test.QuickCheck import Util @@ -45,9 +46,9 @@ spec = do r <- runSpar $ BrigAccess.deleteUser uid liftIO $ r `shouldBe` NoUser - describe "getBrigUser" $ do + describe "getAccount" $ do it "return Nothing if n/a" $ do - musr <- runSpar $ Intra.getBrigUser Intra.WithPendingInvitations (Id . fromJust $ UUID.fromText "29546d9e-ed5b-11ea-8228-c324b1ea1030") + musr <- runSpar $ getAccount Intra.WithPendingInvitations (Id . fromJust $ UUID.fromText "29546d9e-ed5b-11ea-8228-c324b1ea1030") liftIO $ musr `shouldSatisfy` isNothing it "return Just if /a" $ do @@ -60,5 +61,5 @@ spec = do scimUserId <$> createUser tok scimUser uid <- setup - musr <- runSpar $ Intra.getBrigUser Intra.WithPendingInvitations uid + musr <- runSpar $ getAccount Intra.WithPendingInvitations uid liftIO $ musr `shouldSatisfy` isJust diff --git a/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs b/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs index 9247594fe87..eb285a5e61b 100644 --- a/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs @@ -97,15 +97,18 @@ testCreateToken = do createToken owner CreateScimToken - { createScimTokenDescr = "testCreateToken", - createScimTokenPassword = Just defPassword, - createScimTokenCode = Nothing + { description = "testCreateToken", + password = Just defPassword, + verificationCode = Nothing, + name = Nothing } -- Try to do @GET /Users@ and check that it succeeds let fltr = filterBy "externalId" "67c196a0-cd0e-11ea-93c7-ef550ee48502" listUsers_ (Just token) (Just fltr) (env ^. teSpar) !!! const 200 === statusCode +-- @SF.Channel @TSFI.RESTfulAPI @S2 +-- -- Test positive case but also that a SCIM token cannot be created with wrong -- or missing second factor email verification code when this feature is enabled testCreateTokenWithVerificationCode :: TestSpar () @@ -118,17 +121,17 @@ testCreateTokenWithVerificationCode = do user <- getUserBrig owner let email = fromMaybe undefined (userEmail =<< user) - let reqMissingCode = CreateScimToken "testCreateToken" (Just defPassword) Nothing + let reqMissingCode = CreateScimToken "testCreateToken" (Just defPassword) Nothing Nothing createTokenFailsWith owner reqMissingCode 403 "code-authentication-required" void $ requestVerificationCode (env ^. teBrig) email Public.CreateScimToken let wrongCode = Code.Value $ unsafeRange (fromRight undefined (validate "123456")) - let reqWrongCode = CreateScimToken "testCreateToken" (Just defPassword) (Just wrongCode) + let reqWrongCode = CreateScimToken "testCreateToken" (Just defPassword) (Just wrongCode) Nothing createTokenFailsWith owner reqWrongCode 403 "code-authentication-failed" void $ retryNUntil 6 ((==) 200 . statusCode) $ requestVerificationCode (env ^. teBrig) email Public.CreateScimToken code <- getVerificationCode (env ^. teBrig) owner Public.CreateScimToken - let reqWithCode = CreateScimToken "testCreateToken" (Just defPassword) (Just code) + let reqWithCode = CreateScimToken "testCreateToken" (Just defPassword) (Just code) Nothing CreateScimTokenResponse token _ <- createToken owner reqWithCode -- Try to do @GET /Users@ and check that it succeeds @@ -136,18 +139,20 @@ testCreateTokenWithVerificationCode = do listUsers_ (Just token) (Just fltr) (env ^. teSpar) !!! const 200 === statusCode where - requestVerificationCode :: BrigReq -> Email -> Public.VerificationAction -> TestSpar ResponseLBS + requestVerificationCode :: BrigReq -> EmailAddress -> Public.VerificationAction -> TestSpar ResponseLBS requestVerificationCode brig email action = do call $ post (brig . paths ["verification-code", "send"] . contentJson . json (Public.SendVerificationCode action email)) +-- @END + unlockFeature :: GalleyReq -> TeamId -> TestSpar () unlockFeature galley tid = call $ put (galley . paths ["i", "teams", toByteString' tid, "features", featureNameBS @Public.SndFactorPasswordChallengeConfig, toByteString' Public.LockStatusUnlocked]) !!! const 200 === statusCode setSndFactorPasswordChallengeStatus :: GalleyReq -> TeamId -> Public.FeatureStatus -> TestSpar () setSndFactorPasswordChallengeStatus galley tid status = do - let js = RequestBodyLBS $ encode $ Public.WithStatusNoLock @Public.SndFactorPasswordChallengeConfig status Public.trivialConfig Public.FeatureTTLUnlimited + let js = RequestBodyLBS $ encode $ Public.Feature @Public.SndFactorPasswordChallengeConfig status Public.SndFactorPasswordChallengeConfig call $ put (galley . paths ["i", "teams", toByteString' tid, "features", featureNameBS @Public.SndFactorPasswordChallengeConfig] . contentJson . body js) !!! const 200 === statusCode @@ -173,25 +178,28 @@ testTokenLimit = do createToken owner CreateScimToken - { createScimTokenDescr = "testTokenLimit / #1", - createScimTokenPassword = Just defPassword, - createScimTokenCode = Nothing + { description = "testTokenLimit / #1", + password = Just defPassword, + verificationCode = Nothing, + name = Nothing } _ <- createToken owner CreateScimToken - { createScimTokenDescr = "testTokenLimit / #2", - createScimTokenPassword = Just defPassword, - createScimTokenCode = Nothing + { description = "testTokenLimit / #2", + password = Just defPassword, + verificationCode = Nothing, + name = Nothing } -- Try to create the third token and see that it fails createToken_ owner CreateScimToken - { createScimTokenDescr = "testTokenLimit / #3", - createScimTokenPassword = Just defPassword, - createScimTokenCode = Nothing + { description = "testTokenLimit / #3", + password = Just defPassword, + verificationCode = Nothing, + name = Nothing } (env ^. teSpar) !!! checkErr 403 (Just "token-limit-reached") @@ -210,15 +218,16 @@ testNumIdPs = do SAML.SampleIdP metadata _ _ _ <- SAML.makeSampleIdPMetadata void $ call $ Util.callIdpCreate apiversion spar (Just owner) metadata - createToken owner (CreateScimToken "eins" (Just defPassword) Nothing) - >>= deleteToken owner . stiId . createScimTokenResponseInfo + createToken owner (CreateScimToken "eins" (Just defPassword) Nothing Nothing) + >>= deleteToken owner . (.stiId) . (.info) addSomeIdP - createToken owner (CreateScimToken "zwei" (Just defPassword) Nothing) - >>= deleteToken owner . stiId . createScimTokenResponseInfo + createToken owner (CreateScimToken "zwei" (Just defPassword) Nothing Nothing) + >>= deleteToken owner . (.stiId) . (.info) addSomeIdP - createToken_ owner (CreateScimToken "drei" (Just defPassword) Nothing) (env ^. teSpar) + createToken_ owner (CreateScimToken "drei" (Just defPassword) Nothing Nothing) (env ^. teSpar) !!! checkErr 400 (Just "more-than-one-idp") +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- Test that a token can only be created as a team owner testCreateTokenAuthorizesOnlyAdmins :: TestSpar () testCreateTokenAuthorizesOnlyAdmins = do @@ -239,9 +248,10 @@ testCreateTokenAuthorizesOnlyAdmins = do createToken_ uid CreateScimToken - { createScimTokenDescr = "testCreateToken", - createScimTokenPassword = Just defPassword, - createScimTokenCode = Nothing + { description = "testCreateToken", + password = Just defPassword, + verificationCode = Nothing, + name = Nothing } (env ^. teSpar) @@ -251,6 +261,8 @@ testCreateTokenAuthorizesOnlyAdmins = do (mkUser RoleAdmin >>= createToken') !!! const 200 === statusCode +-- @END + -- | Test that for a user with a password, token creation requires reauthentication (i.e. the -- field @"password"@ should be provided). -- @@ -265,9 +277,10 @@ testCreateTokenRequiresPassword = do createToken_ owner CreateScimToken - { createScimTokenDescr = "testCreateTokenRequiresPassword", - createScimTokenPassword = Nothing, - createScimTokenCode = Nothing + { description = "testCreateTokenRequiresPassword", + password = Nothing, + verificationCode = Nothing, + name = Nothing } (env ^. teSpar) !!! checkErr 403 (Just "access-denied") @@ -275,9 +288,10 @@ testCreateTokenRequiresPassword = do createToken_ owner CreateScimToken - { createScimTokenDescr = "testCreateTokenRequiresPassword", - createScimTokenPassword = Just (plainTextPassword6Unsafe "wrong password"), - createScimTokenCode = Nothing + { description = "testCreateTokenRequiresPassword", + password = Just (plainTextPassword6Unsafe "wrong password"), + verificationCode = Nothing, + name = Nothing } (env ^. teSpar) !!! checkErr 403 (Just "access-denied") @@ -302,22 +316,24 @@ testListTokens = do createToken owner CreateScimToken - { createScimTokenDescr = "testListTokens / #1", - createScimTokenPassword = Just defPassword, - createScimTokenCode = Nothing + { description = "testListTokens / #1", + password = Just defPassword, + verificationCode = Nothing, + name = Nothing } _ <- createToken owner CreateScimToken - { createScimTokenDescr = "testListTokens / #2", - createScimTokenPassword = Just defPassword, - createScimTokenCode = Nothing + { description = "testListTokens / #2", + password = Just defPassword, + verificationCode = Nothing, + name = Nothing } -- Check that the token is on the list - list <- scimTokenListTokens <$> listTokens owner + list <- (.scimTokenListTokens) <$> listTokens owner liftIO $ - map stiDescr list + map (.stiDescr) list `shouldBe` ["testListTokens / #1", "testListTokens / #2"] testPlaintextTokensAreConverted :: TestSpar () @@ -411,16 +427,17 @@ testDeletedTokensAreUnusable = do createToken owner CreateScimToken - { createScimTokenDescr = "testDeletedTokensAreUnusable", - createScimTokenPassword = Just defPassword, - createScimTokenCode = Nothing + { description = "testDeletedTokensAreUnusable", + password = Just defPassword, + verificationCode = Nothing, + name = Nothing } -- An operation with the token should succeed let fltr = filterBy "externalId" "67c196a0-cd0e-11ea-93c7-ef550ee48502" listUsers_ (Just token) (Just fltr) (env ^. teSpar) !!! const 200 === statusCode -- Delete the token and now the operation should fail - deleteToken owner (stiId tokenInfo) + deleteToken owner tokenInfo.stiId listUsers_ (Just token) Nothing (env ^. teSpar) !!! checkErr 401 Nothing @@ -436,19 +453,21 @@ testDeletedTokensAreUnlistable = do createToken owner CreateScimToken - { createScimTokenDescr = "testDeletedTokensAreUnlistable", - createScimTokenPassword = Just defPassword, - createScimTokenCode = Nothing + { description = "testDeletedTokensAreUnlistable", + password = Just defPassword, + verificationCode = Nothing, + name = Nothing } -- Delete the token - deleteToken owner (stiId tokenInfo) + deleteToken owner tokenInfo.stiId -- Check that the token is not on the list - list <- scimTokenListTokens <$> listTokens owner + list <- (.scimTokenListTokens) <$> listTokens owner liftIO $ list `shouldBe` [] ---------------------------------------------------------------------------- -- Miscellaneous tests +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- This test verifies that the SCIM API responds with an authentication error -- and can't be used if it receives an invalid secret token -- or if no token is provided at all @@ -460,3 +479,5 @@ testAuthIsNeeded = do listUsers_ (Just invalidToken) Nothing (env ^. teSpar) !!! checkErr 401 Nothing -- Try to do @GET /Users@ without a token and check that it fails listUsers_ Nothing Nothing (env ^. teSpar) !!! checkErr 401 Nothing + +-- @END diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index c5d6ae7f436..71a8eaee3fe 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -30,7 +30,6 @@ where import Bilge import Bilge.Assert -import Brig.Types.User as Brig import qualified Control.Exception import Control.Lens import Control.Monad.Except (MonadError (throwError)) @@ -46,6 +45,7 @@ import Data.ByteString.Conversion import qualified Data.CaseInsensitive as CI import qualified Data.Csv as Csv import Data.Handle (Handle, fromHandle, parseHandle, parseHandleEither) +import Data.HavePendingInvitations import Data.Id (TeamId, UserId, randomId) import Data.Ix (inRange) import Data.LanguageCodes (ISO639_1 (..)) @@ -84,6 +84,7 @@ import qualified Web.Scim.Schema.Meta as Scim import Web.Scim.Schema.PatchOp (Operation) import qualified Web.Scim.Schema.PatchOp as PatchOp import qualified Web.Scim.Schema.User as Scim.User +import qualified Web.Scim.Schema.User.Email as Scim.Email import qualified Wire.API.Team.Export as CsvExport import qualified Wire.API.Team.Feature as Feature import Wire.API.Team.Invitation (Invitation (..)) @@ -131,7 +132,7 @@ specImportToScimFromSAML = setSamlEmailValidation teamid valemail -- saml-auto-provision a new user - (usr :: Scim.User.User SparTag, email :: Email) <- do + (usr :: Scim.User.User SparTag, email :: EmailAddress) <- do (usr, email) <- randomScimUserWithEmail pure ( -- when auto-provisioning via saml, user display name is set to saml name id. @@ -141,7 +142,7 @@ specImportToScimFromSAML = (uref :: SAML.UserRef, uid :: UserId) <- do let uref = SAML.UserRef tenant subj - subj = emailToSAMLNameID email + subj = fromRight' $ emailToSAMLNameID email tenant = idp ^. SAML.idpMetadata . SAML.edIssuer (Just !uid) <- createViaSaml idp privCreds uref samlUserShouldSatisfy uref isJust @@ -215,7 +216,7 @@ specImportToScimFromInvitation = env <- ask call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) - invite :: (HasCallStack) => UserId -> TeamId -> TestSpar (UserId, Email) + invite :: (HasCallStack) => UserId -> TeamId -> TestSpar (UserId, EmailAddress) invite owner teamid = do env <- ask email <- randomEmail @@ -238,7 +239,7 @@ specImportToScimFromInvitation = Maybe (SAML.IdPConfig User.WireIdP) -> TeamId -> UserId -> - Email -> + EmailAddress -> TestSpar (Scim.UserC.StoredUser SparTag) reProvisionWithScim changeHandle mbidp teamid userid email = do tok :: ScimToken <- do @@ -267,10 +268,10 @@ specImportToScimFromInvitation = (SAML.IdPConfig User.WireIdP, SAML.SignPrivCreds) -> Email -> UserId -> TestSpar () + signInWithSaml :: (HasCallStack) => (SAML.IdPConfig User.WireIdP, SAML.SignPrivCreds) -> EmailAddress -> UserId -> TestSpar () signInWithSaml (idp, privCreds) email userid = do let uref = SAML.UserRef tenant subj - subj = emailToSAMLNameID email + subj = fromRight' $ emailToSAMLNameID email tenant = idp ^. SAML.idpMetadata . SAML.edIssuer mbUid <- createViaSaml idp privCreds uref liftIO $ mbUid `shouldBe` Just userid @@ -294,7 +295,7 @@ specImportToScimFromInvitation = let scimUsr = Scim.value (Scim.thing storedUsr) uid = Scim.id (Scim.thing storedUsr) handle = fromRight undefined . parseHandleEither $ Scim.User.userName scimUsr - email = fromJust . parseEmail . fromJust . Scim.User.externalId $ scimUsr + email = fromJust . emailAddressText . fromJust . Scim.User.externalId $ scimUsr Right idpissuer = idp ^. SAML.idpMetadata . SAML.edIssuer . SAML.fromIssuer . to mkHttpsUrl Just samlNameID = Scim.User.externalId scimUsr Just scimExternalId = Scim.User.externalId scimUsr @@ -324,7 +325,7 @@ specImportToScimFromInvitation = signInWithSaml (idp, privcreds) email userid checkCsvDownload ownerid teamid idp storedusr -findUserByEmail :: ScimToken -> Email -> TestSpar (Scim.UserC.StoredUser SparTag) +findUserByEmail :: ScimToken -> EmailAddress -> TestSpar (Scim.UserC.StoredUser SparTag) findUserByEmail tok email = do let fltr = filterBy "externalid" (fromEmail email) resp <- listUsers_ (Just tok) (Just fltr) =<< view teSpar @@ -338,10 +339,10 @@ assertSparCassandraUref (uref, urefAnswer) = do liftIO . (`shouldBe` urefAnswer) =<< runSpar (SAMLUserStore.get uref) -assertSparCassandraScim :: (HasCallStack) => ((TeamId, Email), Maybe UserId) -> TestSpar () +assertSparCassandraScim :: (HasCallStack) => ((TeamId, EmailAddress), Maybe UserId) -> TestSpar () assertSparCassandraScim ((teamid, email), scimAnswer) = do liftIO . (`shouldBe` scimAnswer) - =<< runSpar (ScimExternalIdStore.lookup teamid email) + =<< runSpar (ScimExternalIdStore.lookup teamid (fromEmail email)) assertBrigCassandra :: (HasCallStack) => @@ -361,18 +362,16 @@ assertBrigCassandra uid uref usr (valemail, emailValidated) managedBy = do email = case (valemail, emailValidated) of (Feature.FeatureStatusEnabled, True) -> - Just . fromJust . parseEmail . fromJust . Scim.User.externalId $ usr + Just . fromJust . emailAddressText . fromJust . Scim.User.externalId $ usr _ -> Nothing - accountStatus acc `shouldBe` Active - userId (accountUser acc) `shouldBe` uid - userHandle (accountUser acc) `shouldBe` Just handle - userDisplayName (accountUser acc) `shouldBe` name - userManagedBy (accountUser acc) `shouldBe` managedBy - - userIdentity (accountUser acc) - `shouldBe` Just (SSOIdentity (UserSSOId uref) email) + userStatus acc `shouldBe` Active + userId acc `shouldBe` uid + userHandle acc `shouldBe` Just handle + userDisplayName acc `shouldBe` name + userManagedBy acc `shouldBe` managedBy + userIdentity acc `shouldBe` Just (SSOIdentity (UserSSOId uref) email) specSuspend :: SpecWith TestEnv specSuspend = do @@ -619,11 +618,14 @@ testCreateUserNoIdPWithRole brig tid owner tok role = do -- - if the user has a pending invitation, we have to look up the role in the invitation table -- by doing an rpc to brig liftIO $ Scim.User.roles usr `shouldBe` [cs $ toByteString defaultRole] + -- now external ID can differ from email, so emails are also returned + liftIO $ (\(Scim.Email.Email _ e _) -> Scim.Email.unEmailAddress e) <$> Scim.User.emails usr `shouldBe` [email] + liftIO $ Scim.User.externalId usr `shouldBe` (Just (fromEmail email)) -- user follows invitation flow do inv <- call $ getInvitation brig email - Just inviteeCode <- call $ getInvitationCode brig tid (inInvitation inv) + Just inviteeCode <- call $ getInvitationCode brig tid inv.invitationId registerInvitation email userName inviteeCode True -- check for correct role do @@ -647,15 +649,19 @@ testCreateUserNoIdP = do do aFewTimes (runSpar $ BrigAccess.getAccount Intra.NoPendingInvitations userid) isJust >>= maybe (pure ()) (error "pending user in brig is visible, even though it should not be") - brigUserAccount <- + brigUser <- aFewTimes (runSpar $ BrigAccess.getAccount Intra.WithPendingInvitations userid) isJust >>= maybe (error "could not find user in brig") pure - let brigUser = accountUser brigUserAccount brigUser `userShouldMatch` WrappedScimStoredUser scimStoredUser - liftIO $ accountStatus brigUserAccount `shouldBe` PendingInvitation + liftIO $ brigUser.userStatus `shouldBe` PendingInvitation liftIO $ userEmail brigUser `shouldBe` Just email liftIO $ userManagedBy brigUser `shouldBe` ManagedByScim - liftIO $ userSSOId brigUser `shouldBe` Nothing + -- Previous to the change that allowed the external ID to be different from the email, `userSSOId brigUser` was `Nothing`. + -- We now store the external id as the sso_id when the user invitation is created, whereas before + -- we stored the email address (as in EmailIdentity) and sso_id was not stored until the user registered. + -- We need to store the external id when the user is invited because in case it is different from the email, we don't have it + -- otherwise when the user registers. + liftIO $ userSSOId brigUser `shouldBe` Just (UserScimExternalId (fromEmail email)) -- searching user in brig should fail -- >>> searchUser brig owner userName False @@ -681,7 +687,7 @@ testCreateUserNoIdP = do -- user should be able to follow old team invitation flow do inv <- call $ getInvitation brig email - Just inviteeCode <- call $ getInvitationCode brig tid (inInvitation inv) + Just inviteeCode <- call $ getInvitationCode brig tid inv.invitationId registerInvitation email userName inviteeCode True call $ headInvitation404 brig email @@ -690,10 +696,10 @@ testCreateUserNoIdP = do brigUser <- aFewTimes (runSpar $ BrigAccess.getAccount Intra.NoPendingInvitations userid) isJust >>= maybe (error "could not find user in brig") pure - liftIO $ accountStatus brigUser `shouldBe` Active - liftIO $ userManagedBy (accountUser brigUser) `shouldBe` ManagedByScim - liftIO $ userHandle (accountUser brigUser) `shouldBe` Just handle - liftIO $ userSSOId (accountUser brigUser) `shouldBe` Just (UserScimExternalId (fromEmail email)) + liftIO $ brigUser.userStatus `shouldBe` Active + liftIO $ userManagedBy brigUser `shouldBe` ManagedByScim + liftIO $ userHandle brigUser `shouldBe` Just handle + liftIO $ userSSOId brigUser `shouldBe` Just (UserScimExternalId (fromEmail email)) susr <- getUser tok userid let usr = Scim.value . Scim.thing $ susr liftIO $ Scim.User.active usr `shouldNotBe` Just (Scim.ScimBool False) @@ -840,6 +846,9 @@ testExternalIdIsRequired = do createUser_ (Just tok) user' (env ^. teSpar) !!! const 400 === statusCode +-- The next line contains a mapping from this test to the following test standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 +-- -- Test that user creation fails if handle is invalid testCreateRejectsInvalidHandle :: TestSpar () testCreateRejectsInvalidHandle = do @@ -850,6 +859,8 @@ testCreateRejectsInvalidHandle = do createUser_ (Just tok) (user {Scim.User.userName = "#invalid name"}) (env ^. teSpar) !!! const 400 === statusCode +-- @END + -- | Test that user creation fails if handle is already in use (even on different team). testCreateRejectsTakenHandle :: TestSpar () testCreateRejectsTakenHandle = do @@ -1124,10 +1135,10 @@ testCreateUserTimeout = do scimStoredUser <- aFewTimesRecover (createUser tok scimUser) inv <- call $ getInvitation brig email - Just inviteeCode <- call $ getInvitationCode brig tid (inInvitation inv) + Just inviteeCode <- call $ getInvitationCode brig tid inv.invitationId pure (scimStoredUser, inv, inviteeCode) - searchUser :: (HasCallStack) => Spar.Types.ScimToken -> Scim.User.User tag -> Email -> Bool -> TestSpar () + searchUser :: (HasCallStack) => Spar.Types.ScimToken -> Scim.User.User tag -> EmailAddress -> Bool -> TestSpar () searchUser tok scimUser email shouldSucceed = do let handle = fromJust . parseHandle . Scim.User.userName $ scimUser tryquery qry = @@ -1152,11 +1163,13 @@ specListUsers = describe "GET /Users" $ do it "lists all SCIM users in a team" $ testListProvisionedUsers context "1 SAML IdP" $ do it "finds a SCIM-provisioned user by userName or externalId" $ testFindProvisionedUser + it "returns an empty list of SCIM-provisioned users if user not found (userName, externalId)" $ testDoNotFindProvisionedUser True it "finds a user autoprovisioned via saml by externalId via email" $ testFindSamlAutoProvisionedUserMigratedWithEmailInTeamWithSSO it "finds a user invited via team settings by externalId via email" $ testFindTeamSettingsInvitedUserMigratedWithEmailInTeamWithSSO it "finds a user invited via team settings by UserId" $ testFindTeamSettingsInvitedUserMigratedWithEmailInTeamWithSSOViaUserId context "0 SAML IdP" $ do it "finds a SCIM-provisioned user by userName or externalId" $ testFindProvisionedUserNoIdP + it "returns an empty list of SCIM-provisioned users if user not found (userName, externalId)" $ testDoNotFindProvisionedUser False it "finds a non-SCIM-provisioned user by userName" $ testFindNonProvisionedUserNoIdP FindByHandle it "finds a non-SCIM-provisioned user by externalId" $ testFindNonProvisionedUserNoIdP FindByExternalId it "finds a non-SCIM-provisioned user by UserId" $ testFindNonProvisionedUserNoIdP GetByUserId @@ -1183,11 +1196,27 @@ testFindProvisionedUser = do storedUser <- createUser tok user [storedUser'] <- listUsers tok (Just (filterBy "userName" (Scim.User.userName user))) liftIO $ storedUser' `shouldBe` storedUser - liftIO $ Scim.value (Scim.thing storedUser') `shouldBe` setDefaultRoleIfEmpty (normalizeLikeStored (setPreferredLanguage defLang user {Scim.User.emails = [] {- only after validation -}})) + let expected = setDefaultRoleAndEmailsIfEmpty (normalizeLikeStored (setPreferredLanguage defLang user)) + liftIO $ Scim.value (Scim.thing storedUser') `shouldBe` expected let Just externalId = Scim.User.externalId user users' <- listUsers tok (Just (filterBy "externalId" externalId)) liftIO $ users' `shouldBe` [storedUser] +testDoNotFindProvisionedUser :: Bool -> TestSpar () +testDoNotFindProvisionedUser hasSaml = do + tok <- + if hasSaml + then registerIdPAndScimToken <&> fst + else do + env <- ask + (_owner, teamid) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + registerScimToken teamid Nothing + byName <- listUsers tok (Just (filterBy "userName" "6861f068-3dc7-11ef-9bc2-73f612ae094d")) + byEmail <- listUsers tok (Just (filterBy "externalId" "6861f068-3dc7-11ef-9bc2-73f612ae094d")) + liftIO $ do + byName `shouldBe` [] + byEmail `shouldBe` [] + -- The user is migrated by using the email as the externalId testFindSamlAutoProvisionedUserMigratedWithEmailInTeamWithSSO :: TestSpar () testFindSamlAutoProvisionedUserMigratedWithEmailInTeamWithSSO = do @@ -1198,12 +1227,12 @@ testFindSamlAutoProvisionedUserMigratedWithEmailInTeamWithSSO = do -- auto-provision user via saml memberWithSSO <- do uid <- loginSsoUserFirstTime idp privCreds - Just usr <- runSpar $ Intra.getBrigUser Intra.NoPendingInvitations uid + Just usr <- runSpar $ BrigAccess.getAccount Intra.NoPendingInvitations uid handle <- nextHandle runSpar $ BrigAccess.setHandle uid handle pure usr let memberIdWithSSO = userId memberWithSSO - externalId = either error id $ veidToText =<< Intra.veidFromBrigUser memberWithSSO Nothing + externalId = either error id $ veidToText =<< Intra.veidFromBrigUser memberWithSSO Nothing Nothing -- NOTE: once SCIM is enabled, SSO auto-provisioning is disabled tok <- registerScimToken teamid (Just (idp ^. SAML.idpId)) @@ -1211,12 +1240,12 @@ testFindSamlAutoProvisionedUserMigratedWithEmailInTeamWithSSO = do liftIO $ userManagedBy memberWithSSO `shouldBe` ManagedByWire users <- listUsers tok (Just (filterBy "externalId" externalId)) liftIO $ (scimUserId <$> users) `shouldContain` [memberIdWithSSO] - Just brigUser' <- runSpar $ Intra.getBrigUser Intra.NoPendingInvitations memberIdWithSSO + Just brigUser' <- runSpar $ BrigAccess.getAccount Intra.NoPendingInvitations memberIdWithSSO liftIO $ userManagedBy brigUser' `shouldBe` ManagedByScim where - veidToText :: (MonadError String m) => ValidExternalId -> m Text + veidToText :: (MonadError String m) => ValidScimId -> m Text veidToText veid = - runValidExternalIdEither + runValidScimIdEither (\(SAML.UserRef _ subj) -> maybe (throwError "bad uref from brig") (pure . CI.original) $ SAML.shortShowNameID subj) (pure . fromEmail) veid @@ -1233,7 +1262,7 @@ testFindTeamSettingsInvitedUserMigratedWithEmailInTeamWithSSO = do users' <- listUsers tok (Just (filterBy "externalId" emailInvited)) liftIO $ (scimUserId <$> users') `shouldContain` [memberIdInvited] - Just brigUserInvited' <- runSpar $ Intra.getBrigUser Intra.NoPendingInvitations memberIdInvited + Just brigUserInvited' <- runSpar $ BrigAccess.getAccount Intra.NoPendingInvitations memberIdInvited liftIO $ userManagedBy brigUserInvited' `shouldBe` ManagedByScim testFindTeamSettingsInvitedUserMigratedWithEmailInTeamWithSSOViaUserId :: TestSpar () @@ -1246,7 +1275,7 @@ testFindTeamSettingsInvitedUserMigratedWithEmailInTeamWithSSOViaUserId = do let memberIdInvited = userId memberInvited _ <- getUser tok memberIdInvited - Just brigUserInvited' <- runSpar $ Intra.getBrigUser Intra.NoPendingInvitations memberIdInvited + Just brigUserInvited' <- runSpar $ BrigAccess.getAccount Intra.NoPendingInvitations memberIdInvited liftIO $ userManagedBy brigUserInvited' `shouldBe` ManagedByScim testFindProvisionedUserNoIdP :: TestSpar () @@ -1267,7 +1296,7 @@ testFindNonProvisionedUserNoIdP findBy = do uid <- userId <$> call (inviteAndRegisterUser (env ^. teBrig) owner teamid email) handle <- nextHandle runSpar $ BrigAccess.setHandle uid handle - Just brigUser <- runSpar $ Intra.getBrigUser Intra.NoPendingInvitations uid + Just brigUser <- runSpar $ BrigAccess.getAccount Intra.NoPendingInvitations uid do -- inspect brig user @@ -1281,7 +1310,7 @@ testFindNonProvisionedUserNoIdP findBy = do do liftIO $ users `shouldBe` [uid] - Just brigUser' <- runSpar $ Intra.getBrigUser Intra.NoPendingInvitations uid + Just brigUser' <- runSpar $ BrigAccess.getAccount Intra.NoPendingInvitations uid liftIO $ userManagedBy brigUser' `shouldBe` ManagedByScim liftIO $ brigUser' `shouldBe` scimifyBrigUserHack brigUser email @@ -1296,7 +1325,7 @@ testListNoDeletedUsers = do -- Delete the user _ <- deleteUser tok userid -- Make sure it is deleted in brig before pulling via SCIM (which would recreate it!) - Nothing <- aFewTimes (runSpar (Intra.getBrigUser Intra.WithPendingInvitations userid)) isNothing + Nothing <- aFewTimes (runSpar (BrigAccess.getAccount Intra.WithPendingInvitations userid)) isNothing -- Get all users users <- listUsers tok (Just (filterForStoredUser storedUser)) -- Check that the user is absent @@ -1368,7 +1397,7 @@ testGetUser = do shouldBeManagedBy :: (HasCallStack) => UserId -> ManagedBy -> TestSpar () shouldBeManagedBy uid flag = do - managedBy <- maybe (error "user not found") userManagedBy <$> runSpar (Intra.getBrigUser Intra.WithPendingInvitations uid) + managedBy <- maybe (error "user not found") userManagedBy <$> runSpar (BrigAccess.getAccount Intra.WithPendingInvitations uid) liftIO $ managedBy `shouldBe` flag -- | This is (roughly) the behavior on develop as well as on the branch where this test was @@ -1427,12 +1456,12 @@ testGetUserWithNoHandle = do uid <- loginSsoUserFirstTime idp privcreds tok <- registerScimToken tid (Just (idp ^. SAML.idpId)) - mhandle :: Maybe Handle <- maybe (error "user not found") userHandle <$> runSpar (Intra.getBrigUser Intra.WithPendingInvitations uid) + mhandle :: Maybe Handle <- maybe (error "user not found") userHandle <$> runSpar (BrigAccess.getAccount Intra.WithPendingInvitations uid) liftIO $ mhandle `shouldSatisfy` isNothing storedUser <- getUser tok uid liftIO $ (Scim.User.displayName . Scim.value . Scim.thing) storedUser `shouldSatisfy` isJust - mhandle' :: Maybe Handle <- aFewTimes (maybe (error "user not found") userHandle <$> runSpar (Intra.getBrigUser Intra.WithPendingInvitations uid)) isJust + mhandle' :: Maybe Handle <- aFewTimes (maybe (error "user not found") userHandle <$> runSpar (BrigAccess.getAccount Intra.WithPendingInvitations uid)) isJust liftIO $ mhandle' `shouldSatisfy` isJust liftIO $ (fromHandle <$> mhandle') `shouldBe` (Just . Scim.User.userName . Scim.value . Scim.thing $ storedUser) @@ -1583,7 +1612,10 @@ testScimSideIsUpdated = do storedUser <- createUser tok user let userid = scimUserId storedUser -- Overwrite the user with another randomly-generated user - user' <- randomScimUser + user' <- + if isJust (emailAddressText =<< user.externalId) + then fst <$> randomScimUserWithEmail + else fst <$> randomScimUserWithNick updatedUser <- updateUser tok userid user' -- Get the updated user and check that it matches the user returned by -- 'updateUser' @@ -1592,7 +1624,7 @@ testScimSideIsUpdated = do -- Check that the updated user also matches the data that we sent with -- 'updateUser' richInfoLimit <- view (teOpts . to richInfoLimit) - expectedUser <- setDefaultRoleIfEmpty <$$> whatSparReturnsFor idp richInfoLimit (setPreferredLanguage defLang user') + expectedUser <- setDefaultRoleAndEmailsIfEmpty <$$> whatSparReturnsFor idp richInfoLimit (setPreferredLanguage defLang user') liftIO $ do Right (Scim.value (Scim.thing storedUser')) `shouldBe` expectedUser Scim.id (Scim.thing storedUser') `shouldBe` Scim.id (Scim.thing storedUser) @@ -1637,9 +1669,13 @@ testUpdateSameHandle = do let userid = scimUserId storedUser -- Overwrite the user with another randomly-generated user who has the same name and -- handle - user' <- - randomScimUser <&> \u -> - u + user' <- do + rsu <- + if isJust (emailAddressText =<< user.externalId) + then fst <$> randomScimUserWithEmail + else fst <$> randomScimUserWithNick + pure + rsu { Scim.User.userName = Scim.User.userName user, Scim.User.displayName = Scim.User.displayName user } @@ -1649,7 +1685,7 @@ testUpdateSameHandle = do liftIO $ updatedUser `shouldBe` storedUser' -- Check that the updated user also matches the data that we sent with 'updateUser' richInfoLimit <- view (teOpts . to richInfoLimit) - expectedUser <- setDefaultRoleIfEmpty <$$> whatSparReturnsFor idp richInfoLimit (setPreferredLanguage defLang user') + expectedUser <- setDefaultRoleAndEmailsIfEmpty <$$> whatSparReturnsFor idp richInfoLimit (setPreferredLanguage defLang user') liftIO $ do Right (Scim.value (Scim.thing storedUser')) `shouldBe` expectedUser Scim.id (Scim.thing storedUser') `shouldBe` Scim.id (Scim.thing storedUser) @@ -1678,51 +1714,62 @@ testUpdateExternalId withidp = do (_owner, tid) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) (,Nothing,tid) <$> registerScimToken tid Nothing - let checkUpdate :: (HasCallStack) => Bool -> TestSpar () - checkUpdate hasChanged {- is externalId updated with a different value, or with itself? -} = do - -- Create a user via SCIM - email <- randomEmail - user <- randomScimUser <&> \u -> u {Scim.User.externalId = Just $ fromEmail email} - storedUser <- createUser tok user - let userid = scimUserId storedUser - if withidp - then call $ activateEmail brig email - else registerUser brig tid email - veid :: ValidExternalId <- - runSpar . runScimErrorUnsafe $ - mkValidExternalId midp (Scim.User.externalId user) - -- Overwrite the user with another randomly-generated user (only controlling externalId) - otherEmail <- randomEmail - user' <- do - let upd u = - u - { Scim.User.externalId = - if hasChanged - then Just $ fromEmail otherEmail - else Scim.User.externalId user - } - randomScimUser <&> upd - veid' <- - runSpar . runScimErrorUnsafe $ - mkValidExternalId midp (Scim.User.externalId user') - - _ <- updateUser tok userid user' - - when hasChanged (call $ activateEmail brig otherEmail) - muserid <- lookupByValidExternalId tid veid - muserid' <- lookupByValidExternalId tid veid' - liftIO $ do - if hasChanged - then do - (hasChanged, muserid) `shouldBe` (hasChanged, Nothing) - (hasChanged, muserid') `shouldBe` (hasChanged, Just userid) - else do - (hasChanged, veid') `shouldBe` (hasChanged, veid) - (hasChanged, muserid') `shouldBe` (hasChanged, Just userid) - eventually $ checkEmail userid (Just $ if hasChanged then otherEmail else email) - - checkUpdate True - checkUpdate False + email <- randomEmail + user <- randomScimUser <&> \u -> u {Scim.User.externalId = Just $ fromEmail email} + storedUser <- createUser tok user + let userid = scimUserId storedUser + if withidp + then call $ activateEmail brig email + else registerUser brig tid email + veid :: ValidScimId <- + runSpar . runScimErrorUnsafe $ + mkValidScimId midp (Scim.User.externalId user) (Just email) + + do + -- idempotency (email changes to itself) + -- Overwrite the user with another randomly-generated user (only controlling externalId) + updatedUser <- do + let upd u = u {Scim.User.externalId = Scim.User.externalId user} + randomScimUser <&> upd + veid' <- + runSpar . runScimErrorUnsafe $ + mkValidScimId midp (Scim.User.externalId updatedUser) (Just email) + _ <- updateUser tok userid updatedUser + + muserid <- lookupByValidScimId tid veid + muserid' <- lookupByValidScimId tid veid' + liftIO $ do + ('i', veid') `shouldBe` ('i', veid) + ('i', muserid) `shouldBe` ('i', Just userid) + ('i', muserid') `shouldBe` ('i', Just userid) + eventually $ checkEmail userid (Just email) + + do + -- email changes to other email + -- Overwrite the user with another randomly-generated user (only controlling externalId) + otherEmail <- randomEmail + updatedUser <- do + let upd u = u {Scim.User.externalId = Just $ fromEmail otherEmail} + randomScimUser <&> upd + veid' <- + runSpar . runScimErrorUnsafe $ + mkValidScimId midp (Scim.User.externalId updatedUser) (Just email) -- otherEmail has not been validated yet. + _ <- updateUser tok userid updatedUser + + call $ activateEmail brig otherEmail + veid'' <- + runSpar . runScimErrorUnsafe $ + mkValidScimId midp (Scim.User.externalId updatedUser) (Just otherEmail) + + muserid <- lookupByValidScimId tid veid + muserid' <- lookupByValidScimId tid veid' + muserid'' <- lookupByValidScimId tid veid'' + + liftIO $ do + ('c', muserid) `shouldBe` ('c', Nothing) + ('c', muserid') `shouldBe` ('c', if withidp then Just userid else Nothing) + ('c', muserid'') `shouldBe` ('c', Just userid) + eventually $ checkEmail userid (Just otherEmail) testUpdateExternalIdOfUnregisteredAccount :: TestSpar () testUpdateExternalIdOfUnregisteredAccount = do @@ -1735,9 +1782,9 @@ testUpdateExternalIdOfUnregisteredAccount = do user <- randomScimUser <&> \u -> u {Scim.User.externalId = Just $ fromEmail email} storedUser <- createUser tok user let userid = scimUserId storedUser - veid :: ValidExternalId <- + veid :: ValidScimId <- runSpar . runScimErrorUnsafe $ - mkValidExternalId Nothing (Scim.User.externalId user) + mkValidScimId Nothing (Scim.User.externalId user) (Just email) -- Overwrite the user with another randomly-generated user (only controlling externalId) -- And update the user before they have registered their account otherEmail <- randomEmail @@ -1746,36 +1793,41 @@ testUpdateExternalIdOfUnregisteredAccount = do randomScimUser <&> upd veid' <- runSpar . runScimErrorUnsafe $ - mkValidExternalId Nothing (Scim.User.externalId user') + mkValidScimId Nothing (Scim.User.externalId user') (Just otherEmail) _ <- updateUser tok userid user' -- Now the user registers their account (via old email) registerUser brig tid email -- Then the user activates their new email address call $ activateEmail brig otherEmail - muserid <- lookupByValidExternalId tid veid - muserid' <- lookupByValidExternalId tid veid' + muserid <- lookupByValidScimId tid veid + muserid' <- lookupByValidScimId tid veid' liftIO $ do muserid `shouldBe` Nothing muserid' `shouldBe` Just userid eventually $ checkEmail userid (Just otherEmail) -lookupByValidExternalId :: TeamId -> ValidExternalId -> TestSpar (Maybe UserId) -lookupByValidExternalId tid = - runValidExternalIdEither +lookupByValidScimId :: TeamId -> ValidScimId -> TestSpar (Maybe UserId) +lookupByValidScimId tid = + -- `SU.scimFindUserByExternalId Nothing tid vsid.validScimIdExternal` would be simpler, but + -- if you want to simplify this you'll have to fix the type errors, and this is one of the + -- abandoned test suites. + + runValidScimIdEither (runSpar . SAMLUserStore.get) ( \email -> do - let action = SU.scimFindUserByEmail Nothing tid $ fromEmail email + -- caution: now ext id and email can differ, in which case this will not work anymore + let action = SU.scimFindUserByExternalId Nothing tid $ fromEmail email result <- runSpar . runExceptT . runMaybeT $ action case result of Right muser -> pure $ Scim.id . Scim.thing <$> muser Left err -> error $ show err ) -registerUser :: BrigReq -> TeamId -> Email -> TestSpar () +registerUser :: BrigReq -> TeamId -> EmailAddress -> TestSpar () registerUser brig tid email = do let r = call $ get (brig . path "/i/teams/invitations/by-email" . queryItem "email" (toByteString' email)) - inv <- responseJsonError =<< r Just (Locale (Language EN) Nothing)} + brigUser <- maybe (error "no brig user") pure =<< runSpar (BrigAccess.getAccount Intra.WithPendingInvitations userid) + let scimUserWithDefLocale = validScimUser {Spar.Types.locale = Spar.Types.locale validScimUser <|> Just (Locale (Language EN) Nothing)} brigUser `userShouldMatch` scimUserWithDefLocale testUpdateUserRole :: TestSpar () @@ -1825,7 +1877,7 @@ testUpdateUserRole = do -- user follows invitation flow do inv <- call $ getInvitation brig email - Just inviteeCode <- call $ getInvitationCode brig tid (inInvitation inv) + Just inviteeCode <- call $ getInvitationCode brig tid inv.invitationId registerInvitation email userName inviteeCode True checkTeamMembersRole tid owner userid initialRole _ <- updateUser tok userid (scimUser {Scim.User.roles = cs . toByteString <$> maybeToList mUpdatedRole}) @@ -2060,7 +2112,7 @@ createScimUserWithRole brig tid owner tok initialRole = do -- user follows invitation flow do inv <- call $ getInvitation brig email - Just inviteeCode <- call $ getInvitationCode brig tid (inInvitation inv) + Just inviteeCode <- call $ getInvitationCode brig tid inv.invitationId registerInvitation email userName inviteeCode True checkTeamMembersRole tid owner userid initialRole pure userid @@ -2083,16 +2135,16 @@ specDeleteUser = do storedUser <- createUser tok user let uid :: UserId = scimUserId storedUser uref :: SAML.UserRef <- do - usr <- runSpar $ Intra.getBrigUser Intra.WithPendingInvitations uid + mUsr <- runSpar $ BrigAccess.getAccount Intra.WithPendingInvitations uid let err = error . ("brig user without UserRef: " <>) . show - case (`Intra.veidFromBrigUser` Nothing) <$> usr of - bad@(Just (Right veid)) -> runValidExternalIdEither pure (const $ err bad) veid + case (\usr -> Intra.veidFromBrigUser usr Nothing Nothing) <$> mUsr of + bad@(Just (Right veid)) -> runValidScimIdEither pure (const $ err bad) veid bad -> err bad spar <- view teSpar deleteUser_ (Just tok) (Just uid) spar !!! const 204 === statusCode brigUser :: Maybe User <- - aFewTimes (runSpar $ Intra.getBrigUser Intra.WithPendingInvitations uid) isNothing + aFewTimes (runSpar $ BrigAccess.getAccount Intra.WithPendingInvitations uid) isNothing samlUser :: Maybe UserId <- aFewTimes (getUserIdViaRef' uref) isNothing scimUser <- @@ -2181,7 +2233,7 @@ specDeleteUser = do do inv <- call $ getInvitation brig email - Just inviteeCode <- call $ getInvitationCode brig tid (inInvitation inv) + Just inviteeCode <- call $ getInvitationCode brig tid inv.invitationId registerInvitation email (Name "Alice") inviteeCode True call $ headInvitation404 brig email @@ -2247,7 +2299,7 @@ specAzureQuirks = do specEmailValidation :: SpecWith TestEnv specEmailValidation = do describe "email validation" $ do - let setup :: (HasCallStack) => Bool -> TestSpar (UserId, Email) + let setup :: (HasCallStack) => Bool -> TestSpar (UserId, EmailAddress) setup enabled = do (tok, (_ownerid, teamid, idp)) <- registerIdPAndScimToken if enabled @@ -2257,9 +2309,8 @@ specEmailValidation = do scimStoredUser <- createUser tok user veid <- runSpar . runScimErrorUnsafe $ - mkValidExternalId (Just idp) (Scim.User.externalId . Scim.value . Scim.thing $ scimStoredUser) - uid :: UserId <- - getUserIdViaRef (veid ^?! veidUref) + mkValidScimId (Just idp) (Scim.User.externalId . Scim.value . Scim.thing $ scimStoredUser) (Just email) + uid :: UserId <- getUserIdViaRef $ fromJust (veidUref veid) brig <- view teBrig -- we intentionally activate the email even if it's not set up to work, to make sure -- it doesn't if the feature is disabled. @@ -2294,7 +2345,7 @@ testDeletedUsersFreeExternalIdNoIdp = do -- accept invitation do inv <- call $ getInvitation brig email - Just inviteeCode <- call $ getInvitationCode brig tid (inInvitation inv) + Just inviteeCode <- call $ getInvitationCode brig tid inv.invitationId registerInvitation email userName inviteeCode True call $ headInvitation404 brig email @@ -2304,7 +2355,7 @@ testDeletedUsersFreeExternalIdNoIdp = do void $ aFewTimes - (runSpar $ ScimExternalIdStore.lookup tid email) + (runSpar $ ScimExternalIdStore.lookup tid (fromEmail email)) (== Nothing) -- | CSV download of team members is mainly tested here: 'API.Teams.testListTeamMembersCsv'. @@ -2350,7 +2401,7 @@ specSCIMManaged = do do displayName <- Name <$> randomAlphaNum - let uupd = UserUpdate (Just displayName) Nothing Nothing Nothing + let uupd = UserUpdate (Just displayName) Nothing Nothing Nothing Nothing call $ updateProfileBrig brig uid uupd !!! do (fmap Wai.label . responseJsonEither @Wai.Error) === const (Right "managed-by-scim") diff --git a/services/spar/test-integration/Util/Activation.hs b/services/spar/test-integration/Util/Activation.hs index 143e5adbdf5..58e1f05125c 100644 --- a/services/spar/test-integration/Util/Activation.hs +++ b/services/spar/test-integration/Util/Activation.hs @@ -30,7 +30,7 @@ import Wire.API.User.Identity getActivationCode :: (MonadHttp m, MonadIO m) => BrigReq -> - Email -> + EmailAddress -> m (Maybe (ActivationKey, ActivationCode)) getActivationCode brig e = do let qry = queryItem "email" . toByteString' $ e diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index a9a29c3445f..6d92d56e0df 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -38,7 +38,6 @@ module Util.Core it, pending, pendingWith, - xit, shouldRespondWith, module Test.Hspec, aFewTimes, @@ -48,8 +47,7 @@ module Util.Core -- * HTTP call, endpointToReq, - endpointToSettings, - endpointToURL, + mkVersionedRequest, -- * Other randomEmail, @@ -60,7 +58,6 @@ module Util.Core updateProfileBrig, createUserWithTeam, createUserWithTeamDisableSSO, - getSSOEnabledInternal, putSSOEnabledInternal, inviteAndRegisterUser, createTeamMember, @@ -95,7 +92,6 @@ module Util.Core loginSsoUserFirstTime, loginSsoUserFirstTime', loginCreatedSsoUser, - callAuthnReqPrecheck', callAuthnReq, callAuthnReq', callIdpGet, @@ -144,7 +140,7 @@ where import Bilge hiding (getCookie, host, port) -- we use Web.Cookie instead of the http-client type import qualified Bilge import Bilge.Assert (Assertions, (!!!), ()) @@ -186,6 +184,7 @@ import Spar.Error (SparError) import qualified Spar.Intra.BrigApp as Intra import Spar.Options import Spar.Run +import Spar.Sem.BrigAccess (getAccount) import qualified Spar.Sem.IdPConfigStore as IdPConfigStore import qualified Spar.Sem.SAMLUserStore as SAMLUserStore import qualified Spar.Sem.ScimExternalIdStore as ScimExternalIdStore @@ -201,9 +200,11 @@ import URI.ByteString as URI import Util.Options import Util.Types import qualified Web.Cookie as Web +import Web.HttpApiData +import Wire.API.Routes.Version import Wire.API.Team (Icon (..)) import qualified Wire.API.Team as Galley -import Wire.API.Team.Feature (FeatureStatus (..), FeatureTTL' (..), FeatureTrivialConfig (trivialConfig), SSOConfig, WithStatusNoLock (WithStatusNoLock)) +import Wire.API.Team.Feature import qualified Wire.API.Team.Invitation as TeamInvitation import Wire.API.Team.Member (NewTeamMember, TeamMemberList, rolePermissions) import qualified Wire.API.Team.Member as Member @@ -215,7 +216,7 @@ import Wire.API.User import qualified Wire.API.User as User import Wire.API.User.Auth hiding (Cookie) import Wire.API.User.IdentityProvider -import Wire.API.User.Scim (runValidExternalIdEither) +import Wire.API.User.Scim import Wire.Sem.Logger.TinyLog -- | Call 'mkEnv' with options from config files. @@ -264,9 +265,9 @@ mkEnv tstOpts opts = do mgr :: Manager <- newManager defaultManagerSettings sparCtxLogger <- Log.mkLogger (samlToLevel $ saml opts ^. SAML.cfgLogLevel) (logNetStrings opts) (logFormat opts) cql :: ClientState <- initCassandra opts sparCtxLogger - let brig = endpointToReq tstOpts.brig - galley = endpointToReq tstOpts.galley - spar = endpointToReq tstOpts.spar + let brig = mkVersionedRequest tstOpts.brig + galley = mkVersionedRequest tstOpts.galley + spar = mkVersionedRequest tstOpts.spar sparEnv = Spar.Env {..} wireIdPAPIVersion = WireIdPAPIV2 sparCtxOpts = opts @@ -299,15 +300,6 @@ it :: SpecWith TestEnv it msg bdy = Test.Hspec.it msg $ runReaderT bdy -xit :: - (HasCallStack) => - -- or, more generally: - -- MonadIO m, Example (TestEnv -> m ()), Arg (TestEnv -> m ()) ~ TestEnv - String -> - TestSpar () -> - SpecWith TestEnv -xit msg bdy = Test.Hspec.xit msg $ runReaderT bdy - pending :: (HasCallStack, MonadIO m) => m () pending = liftIO Test.Hspec.pending @@ -396,18 +388,12 @@ createUserWithTeamDisableSSO brg gly = do pure () pure (uid, tid) -getSSOEnabledInternal :: (HasCallStack, MonadHttp m) => GalleyReq -> TeamId -> m ResponseLBS -getSSOEnabledInternal gly tid = do - get $ - gly - . paths ["i", "teams", toByteString' tid, "features", "sso"] - putSSOEnabledInternal :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyReq -> TeamId -> FeatureStatus -> m () putSSOEnabledInternal gly tid enabled = do void . put $ gly . paths ["i", "teams", toByteString' tid, "features", "sso"] - . json (WithStatusNoLock @SSOConfig enabled trivialConfig FeatureTTLUnlimited) + . json (Feature enabled SSOConfig) . expect2xx -- | cloned from `/services/brig/test/integration/API/Team/Util.hs`. @@ -416,12 +402,12 @@ inviteAndRegisterUser :: BrigReq -> UserId -> TeamId -> - Email -> + EmailAddress -> m User inviteAndRegisterUser brig u tid inviteeEmail = do let invite = stdInvitationRequest inviteeEmail - inv <- responseJsonError =<< postInvitation tid u invite - Just inviteeCode <- getInvitationCode tid (TeamInvitation.inInvitation inv) + inv :: TeamInvitation.Invitation <- responseJsonError =<< postInvitation tid u invite + Just inviteeCode <- getInvitationCode tid inv.invitationId rspInvitee <- post ( brig @@ -437,10 +423,10 @@ inviteAndRegisterUser brig u tid inviteeEmail = do unless (selfTeam == Just tid) $ error "Team ID in self profile and team table do not match" pure invitee where - accept' :: User.Email -> User.InvitationCode -> RequestBody + accept' :: EmailAddress -> User.InvitationCode -> RequestBody accept' email code = acceptWithName (User.Name "Bob") email code -- - acceptWithName :: User.Name -> User.Email -> User.InvitationCode -> RequestBody + acceptWithName :: User.Name -> EmailAddress -> User.InvitationCode -> RequestBody acceptWithName name email code = RequestBodyLBS . Aeson.encode $ object @@ -585,17 +571,42 @@ nextUserRef = liftIO $ do (SAML.Issuer $ SAML.unsafeParseURI ("http://" <> tenant)) <$> nextSubject +-- FUTUREWORK: use an endpoint from latest API version getTeams :: (HasCallStack, MonadHttp m, MonadIO m) => UserId -> GalleyReq -> m Galley.TeamList getTeams u gly = do r <- get - ( gly + ( unversioned + . gly . paths ["teams"] . zAuthAccess u "conn" . expect2xx ) pure $ responseJsonUnsafe r +-- | Note: Apply this function last when composing (Request -> Request) functions +unversioned :: Request -> Request +unversioned r = + r + { HTTP.path = + maybe + (HTTP.path r) + (B8.pack "/" <>) + (removeVersionPrefix . removeSlash' $ HTTP.path r) + } + where + removeVersionPrefix :: ByteString -> Maybe ByteString + removeVersionPrefix bs = do + let (x, s) = B8.splitAt 1 bs + guard (x == B8.pack "v") + (_, s') <- B8.readInteger s + pure (B8.tail s') + + removeSlash' :: ByteString -> ByteString + removeSlash' s = case B8.uncons s of + Just ('/', s') -> s' + _ -> s + getTeamMemberIds :: (HasCallStack) => UserId -> TeamId -> TestSpar [UserId] getTeamMemberIds usr tid = (^. Team.userId) <$$> getTeamMembers usr tid @@ -627,13 +638,13 @@ getSelfProfile brg usr = do zAuthAccess :: UserId -> ByteString -> Request -> Request zAuthAccess u c = header "Z-Type" "access" . zUser u . zConn c -newTeam :: Galley.BindingNewTeam -newTeam = Galley.BindingNewTeam $ Galley.newNewTeam (unsafeRange "teamName") DefaultIcon +newTeam :: Galley.NewTeam +newTeam = Galley.newNewTeam (unsafeRange "teamName") DefaultIcon -randomEmail :: (MonadIO m) => m Email +randomEmail :: (MonadIO m) => m EmailAddress randomEmail = do uid <- liftIO nextRandom - pure $ Email ("success+" <> UUID.toText uid) "simulator.amazonses.com" + pure $ User.unsafeEmailAddress ("success+" <> UUID.toASCIIBytes uid) "simulator.amazonses.com" randomUser :: (HasCallStack, MonadCatch m, MonadIO m, MonadHttp m) => BrigReq -> m User randomUser brig_ = do @@ -686,22 +697,25 @@ zConn :: ByteString -> Request -> Request zConn = header "Z-Connection" endpointToReq :: Endpoint -> (Bilge.Request -> Bilge.Request) -endpointToReq ep = Bilge.host (ep ^. host . to cs) . Bilge.port (ep ^. port) +endpointToReq ep = Bilge.host (cs ep.host) . Bilge.port ep.port -endpointToSettings :: Endpoint -> Warp.Settings -endpointToSettings ep = - Warp.defaultSettings - { Warp.settingsHost = Imports.fromString . cs $ ep ^. host, - Warp.settingsPort = fromIntegral $ ep ^. port - } +mkVersionedRequest :: Endpoint -> Request -> Request +mkVersionedRequest ep = maybeAddPrefix . endpointToReq ep + +maybeAddPrefix :: Request -> Request +maybeAddPrefix r = case pathSegments $ getUri r of + ("i" : _) -> r + ("api-internal" : _) -> r + _ -> addPrefix r -endpointToURL :: (MonadIO m) => Endpoint -> Text -> m URI -endpointToURL ep urlpath = either err pure url +addPrefix :: Request -> Request +addPrefix r = r {HTTP.path = toHeader latestVersion <> "/" <> removeSlash (HTTP.path r)} where - url = parseURI' ("http://" <> urlhost <> ":" <> urlport) <&> (=/ urlpath) - urlhost = cs $ ep ^. host - urlport = cs . show $ ep ^. port - err = liftIO . throwIO . ErrorCall . show . (,(ep, url)) + removeSlash s = case B8.uncons s of + Just ('/', s') -> s' + _ -> s + latestVersion :: Version + latestVersion = maxBound -- spar specifics @@ -793,8 +807,7 @@ getCookie proxy rsp = do then Right $ SimpleSetCookie web else Left $ "bad cookie name. (found, expected) == " <> show (Web.setCookieName web, SAML.cookieName proxy) --- | In 'setResponseCookie' we set an expiration date iff cookie is persistent. So here we test for --- expiration date. Easier than parsing and inspecting the cookie value. +-- | we test for expiration date as it's asier than parsing and inspecting the cookie value. hasPersistentCookieHeader :: ResponseLBS -> Either String () hasPersistentCookieHeader rsp = do cky <- getCookie (Proxy @"zuid") rsp @@ -982,10 +995,6 @@ callAuthnReq' :: (MonadHttp m) => SparReq -> SAML.IdPId -> m ResponseLBS callAuthnReq' sparreq_ idpid = do get $ sparreq_ . path (cs $ "/sso/initiate-login/" -/ SAML.idPIdToST idpid) -callAuthnReqPrecheck' :: (MonadHttp m) => SparReq -> SAML.IdPId -> m ResponseLBS -callAuthnReqPrecheck' sparreq_ idpid = do - head $ sparreq_ . path (cs $ "/sso/initiate-login/" -/ SAML.idPIdToST idpid) - callIdpGet :: (MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> SAML.IdPId -> m IdP callIdpGet sparreq_ muid idpid = do resp <- callIdpGet' (sparreq_ . expect2xx) muid idpid @@ -1186,15 +1195,17 @@ callDeleteDefaultSsoCode sparreq_ = do -- helpers talking to spar's cassandra directly --- | Look up 'UserId' under 'UserSSOId' on spar's cassandra directly. +-- | Look up 'UserId' under 'externalId', and if no email address is given, under the saml user ref. +-- +-- This is a bit convoluted, don't try too hard to make sense of it. Better luck when +-- rewriting this in /integration! :-) ssoToUidSpar :: (HasCallStack, MonadIO m, MonadReader TestEnv m) => TeamId -> UserSSOId -> m (Maybe UserId) ssoToUidSpar tid ssoid = do - veid <- either (error . ("could not parse brig sso_id: " <>)) pure $ Intra.veidFromUserSSOId ssoid + veid <- either (error . ("could not parse brig sso_id: " <>)) pure $ Intra.veidFromUserSSOId ssoid Nothing runSpar $ - runValidExternalIdEither - SAMLUserStore.get - (ScimExternalIdStore.lookup tid) - veid + let doThat = SAMLUserStore.get + doThis _ = ScimExternalIdStore.lookup tid veid.validScimIdExternal + in these doThis doThat (const doThat) veid.validScimIdAuthInfo runSimpleSP :: (MonadReader TestEnv m, MonadIO m) => SAML.SimpleSP a -> m a runSimpleSP action = do @@ -1225,7 +1236,7 @@ getSsoidViaSelf uid = maybe (error "not found") pure =<< getSsoidViaSelf' uid getSsoidViaSelf' :: (HasCallStack) => UserId -> TestSpar (Maybe UserSSOId) getSsoidViaSelf' uid = do - musr <- aFewTimes (runSpar $ Intra.getBrigUser Intra.NoPendingInvitations uid) isJust + musr <- aFewTimes (runSpar $ getAccount Intra.NoPendingInvitations uid) isJust pure $ ssoIdentity =<< (userIdentity =<< musr) getUserIdViaRef :: (HasCallStack) => UserRef -> TestSpar UserId @@ -1246,13 +1257,13 @@ checkErrHspec :: (HasCallStack) => Int -> TestErrorLabel -> ResponseLBS -> Bool checkErrHspec status label resp = status == statusCode resp && responseJsonEither resp == Right label -- | copied from brig integration tests -stdInvitationRequest :: User.Email -> TeamInvitation.InvitationRequest +stdInvitationRequest :: EmailAddress -> TeamInvitation.InvitationRequest stdInvitationRequest = stdInvitationRequest' Nothing Nothing -- | copied from brig integration tests -stdInvitationRequest' :: Maybe User.Locale -> Maybe Role -> User.Email -> TeamInvitation.InvitationRequest +stdInvitationRequest' :: Maybe User.Locale -> Maybe Role -> EmailAddress -> TeamInvitation.InvitationRequest stdInvitationRequest' loc role email = - TeamInvitation.InvitationRequest loc role Nothing email Nothing + TeamInvitation.InvitationRequest loc role Nothing email setRandomHandleBrig :: (HasCallStack) => UserId -> TestSpar () setRandomHandleBrig uid = do diff --git a/services/spar/test-integration/Util/Email.hs b/services/spar/test-integration/Util/Email.hs index 74c564ad2bc..39bfc269152 100644 --- a/services/spar/test-integration/Util/Email.hs +++ b/services/spar/test-integration/Util/Email.hs @@ -24,13 +24,10 @@ module Util.Email where import Bilge hiding (accept, timeout) import Bilge.Assert import Brig.Types.Activation -import Control.Lens (view, (^?)) +import Control.Lens (view) import Control.Monad.Catch (MonadCatch) -import Data.Aeson.Lens import Data.ByteString.Conversion import Data.Id -import qualified Data.Misc as Misc -import Data.Text.Encoding (encodeUtf8) import qualified Data.ZAuth.Token as ZAuth import Imports import Test.Tasty.HUnit @@ -40,52 +37,13 @@ import Util.Types import qualified Wire.API.Team.Feature as Feature import Wire.API.User import Wire.API.User.Activation -import qualified Wire.API.User.Auth as Auth - -changeEmailBrig :: - (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => - BrigReq -> - User -> - Email -> - m ResponseLBS -changeEmailBrig brig usr newEmail = do - -- most of this code is stolen from brig integration tests - let oldEmail = fromJust (userEmail usr) - (cky, tok) <- do - rsp <- - login (emailLogin oldEmail defPassword Nothing) Auth.PersistentCookie - Misc.PlainTextPassword6 -> Maybe Auth.CookieLabel -> Auth.Login - emailLogin e pw cl = - Auth.PasswordLogin $ - Auth.PasswordLoginData (Auth.LoginByEmail e) pw cl Nothing - - login :: Auth.Login -> Auth.CookieType -> (MonadHttp m) => m ResponseLBS - login l t = - post $ - brig - . path "/login" - . (if t == Auth.PersistentCookie then queryItem "persist" "true" else id) - . json l - - decodeCookie :: (HasCallStack) => Response a -> Bilge.Cookie - decodeCookie = fromMaybe (error "missing zuid cookie") . Bilge.getCookie "zuid" - - decodeToken :: (HasCallStack) => Response (Maybe LByteString) -> ZAuth.Token ZAuth.Access - decodeToken r = fromMaybe (error "invalid access_token") $ do - x <- responseBody r - t <- x ^? key "access_token" . _String - fromByteString (encodeUtf8 t) changeEmailBrigCreds :: (MonadHttp m, HasCallStack) => BrigReq -> Cookie -> ZAuth.Token ZAuth.Access -> - Email -> + EmailAddress -> m ResponseLBS changeEmailBrigCreds brig cky tok newEmail = do put @@ -105,7 +63,7 @@ forceCookie cky = header "Cookie" $ cookie_name cky <> "=" <> cookie_value cky activateEmail :: (MonadCatch m, MonadIO m, HasCallStack) => BrigReq -> - Email -> + EmailAddress -> (MonadHttp m) => m () activateEmail brig email = do act <- getActivationCode brig email @@ -119,7 +77,7 @@ activateEmail brig email = do failActivatingEmail :: (MonadCatch m, MonadIO m, HasCallStack) => BrigReq -> - Email -> + EmailAddress -> (MonadHttp m) => m () failActivatingEmail brig email = do act <- getActivationCode brig email @@ -128,7 +86,7 @@ failActivatingEmail brig email = do checkEmail :: (HasCallStack) => UserId -> - Maybe Email -> + Maybe EmailAddress -> TestSpar () checkEmail uid expectedEmail = do brig <- view teBrig @@ -152,6 +110,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.WithStatusNoLock @Feature.ValidateSAMLEmailsConfig status Feature.trivialConfig Feature.FeatureTTLUnlimited) + let req = put $ galley . paths p . json (Feature.Feature @Feature.ValidateSAMLEmailsConfig status Feature.ValidateSAMLEmailsConfig) p = ["/i/teams", toByteString' tid, "features", Feature.featureNameBS @Feature.ValidateSAMLEmailsConfig] call req !!! const 200 === statusCode diff --git a/services/spar/test-integration/Util/Invitation.hs b/services/spar/test-integration/Util/Invitation.hs index 8a9d0fe6490..6779a67a44d 100644 --- a/services/spar/test-integration/Util/Invitation.hs +++ b/services/spar/test-integration/Util/Invitation.hs @@ -37,12 +37,12 @@ import Util import Wire.API.Team.Invitation (Invitation (..)) import Wire.API.User -headInvitation404 :: (HasCallStack) => BrigReq -> Email -> Http () +headInvitation404 :: (HasCallStack) => BrigReq -> EmailAddress -> Http () headInvitation404 brig email = do Bilge.head (brig . path "/teams/invitations/by-email" . contentJson . queryItem "email" (toByteString' email)) !!! const 404 === statusCode -getInvitation :: (HasCallStack) => BrigReq -> Email -> Http Invitation +getInvitation :: (HasCallStack) => BrigReq -> EmailAddress -> Http Invitation getInvitation brig email = responseJsonUnsafe <$> Bilge.get @@ -70,7 +70,7 @@ getInvitationCode brig t ref = do let lbs = fromMaybe "" $ responseBody r pure $ fromByteString (maybe (error "No code?") encodeUtf8 (lbs ^? key "code" . _String)) -registerInvitation :: (HasCallStack) => Email -> Name -> InvitationCode -> Bool -> TestSpar () +registerInvitation :: (HasCallStack) => EmailAddress -> Name -> InvitationCode -> Bool -> TestSpar () registerInvitation email name inviteeCode shouldSucceed = do env <- ask let brig = env ^. teBrig @@ -84,7 +84,7 @@ registerInvitation email name inviteeCode shouldSucceed = do ) Email -> InvitationCode -> Aeson.Value +acceptWithName :: Name -> EmailAddress -> InvitationCode -> Aeson.Value acceptWithName name email code = Aeson.object [ "name" Aeson..= fromName name, diff --git a/services/spar/test-integration/Util/Scim.hs b/services/spar/test-integration/Util/Scim.hs index 40ef9884d0a..d95bde89583 100644 --- a/services/spar/test-integration/Util/Scim.hs +++ b/services/spar/test-integration/Util/Scim.hs @@ -31,7 +31,9 @@ import Data.Handle (Handle, parseHandle) import Data.Id import Data.LanguageCodes (ISO639_1 (EN)) import Data.String.Conversions +import Data.Text.Encoding (encodeUtf8) import qualified Data.Text.Lazy as Lazy +import Data.These import Data.Time import Data.UUID as UUID import Data.UUID.V4 as UUID @@ -44,7 +46,6 @@ import qualified Spar.Intra.BrigApp as Intra import Spar.Scim.User (synthesizeScimUser, validateScimUser') import qualified Spar.Sem.ScimTokenStore as ScimTokenStore import Test.QuickCheck (arbitrary, generate) -import qualified Text.Email.Parser as Email import qualified Text.XML.DSig as SAML import Util.Core import Util.Types @@ -59,15 +60,19 @@ import qualified Web.Scim.Schema.Meta as Scim import qualified Web.Scim.Schema.PatchOp as Scim.PatchOp import qualified Web.Scim.Schema.User as Scim import qualified Web.Scim.Schema.User as Scim.User -import qualified Web.Scim.Schema.User.Email as Email +import qualified Web.Scim.Schema.User.Email as Scim.Email import qualified Web.Scim.Schema.User.Phone as Phone import qualified Wire.API.Team.Member as Member import Wire.API.Team.Role (Role, defaultRole) import Wire.API.User -import Wire.API.User.IdentityProvider hiding (team) +import Wire.API.User.IdentityProvider hiding (handle, team) import Wire.API.User.RichInfo import Wire.API.User.Scim +-- | Take apart a 'ValidScimId', using 'SAML.UserRef' if available, otherwise 'Email'. +runValidScimIdEither :: (SAML.UserRef -> a) -> (EmailAddress -> a) -> ValidScimId -> a +runValidScimIdEither doUref doEmail = these doEmail doUref (\_ uref -> doUref uref) . validScimIdAuthInfo + -- | Call 'registerTestIdP', then 'registerScimToken'. The user returned is the owner of the team; -- the IdP is registered with the team; the SCIM token can be used to manipulate the team. registerIdPAndScimToken :: (HasCallStack) => TestSpar (ScimToken, (UserId, TeamId, IdP)) @@ -109,7 +114,8 @@ registerScimToken teamid midpid = do stiId = scimTokenId, stiCreatedAt = now, stiIdP = midpid, - stiDescr = "test token" + stiDescr = "test token", + stiName = "test token" } pure tok @@ -136,7 +142,6 @@ randomScimUserWithSubjectAndRichInfo :: m (Scim.User.User SparTag, SAML.UnqualifiedNameID) randomScimUserWithSubjectAndRichInfo richInfo = do suffix <- cs <$> replicateM 20 (getRandomR ('a', 'z')) - 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) <- @@ -155,7 +160,7 @@ randomScimUserWithSubjectAndRichInfo richInfo = do ( (Scim.User.empty @SparTag userSchemas ("scimuser_" <> suffix) (ScimUserExtra richInfo)) { Scim.User.displayName = Just ("ScimUser" <> suffix), Scim.User.externalId = Just externalId, - Scim.User.emails = emails, + Scim.User.emails = [], Scim.User.phoneNumbers = phones, Scim.User.roles = ["member"] -- if we don't add this role here explicitly, some tests may show confusing failures @@ -171,10 +176,10 @@ randomScimUserWithSubjectAndRichInfo richInfo = do -- support externalIds that are not emails, and storing email addresses in `emails` in the -- scim schema. `randomScimUserWithEmail` is from a time where non-idp-authenticated users -- could only be provisioned with email as externalId. we should probably rework all that. -randomScimUserWithEmail :: (MonadRandom m) => m (Scim.User.User SparTag, Email) +randomScimUserWithEmail :: (MonadRandom m) => m (Scim.User.User SparTag, EmailAddress) randomScimUserWithEmail = do suffix <- cs <$> replicateM 7 (getRandomR ('0', '9')) - let email = Email ("email" <> suffix) "example.com" + let email = unsafeEmailAddress ("email" <> encodeUtf8 suffix) "example.com" externalId = fromEmail email pure ( (Scim.User.empty @SparTag userSchemas ("scimuser_" <> suffix) (ScimUserExtra mempty)) @@ -197,17 +202,6 @@ randomScimUserWithNick = do nick ) -randomScimEmail :: (MonadRandom m) => m Email.Email -randomScimEmail = do - let typ :: Maybe Text = Nothing - primary :: Maybe Scim.ScimBool = Nothing -- TODO: where should we catch users with more than one - -- primary email? - value :: Email.EmailAddress2 <- do - localpart <- cs <$> replicateM 15 (getRandomR ('a', 'z')) - domainpart <- (<> ".com") . cs <$> replicateM 15 (getRandomR ('a', 'z')) - pure . Email.EmailAddress2 $ Email.unsafeEmailAddress localpart domainpart - pure Email.Email {..} - randomScimPhone :: (MonadRandom m) => m Phone.Phone randomScimPhone = do let typ :: Maybe Text = Nothing @@ -603,9 +597,6 @@ acceptScim = accept "application/scim+json" scimUserId :: Scim.StoredUser SparTag -> UserId scimUserId = Scim.id . Scim.thing -scimPreferredLanguage :: Scim.StoredUser SparTag -> Maybe Text -scimPreferredLanguage = Scim.preferredLanguage . Scim.value . Scim.thing - -- | There are a number of user types that all partially map on each other. This class -- provides a uniform interface to data stored in those types. -- @@ -635,12 +626,12 @@ class IsUser u where -- is correct and don't aim to verify that name, handle, etc correspond to ones in 'vsuUser'. instance IsUser ValidScimUser where maybeUserId = Nothing - maybeHandle = Just (Just . view vsuHandle) - maybeName = Just (Just . view vsuName) - maybeTenant = Just (^? (vsuExternalId . veidUref . SAML.uidTenant)) - maybeSubject = Just (^? (vsuExternalId . veidUref . SAML.uidSubject)) - maybeScimExternalId = Just (runValidExternalIdEither Intra.urefToExternalId (Just . fromEmail) . view vsuExternalId) - maybeLocale = Just (view vsuLocale) + maybeHandle = Just (Just <$> handle) + maybeName = Just (Just <$> (.name)) + maybeTenant = Just (fmap SAML._uidTenant . veidUref . externalId) + maybeSubject = Just (fmap SAML._uidSubject . veidUref . externalId) + maybeScimExternalId = Just (runValidScimIdEither Intra.urefToExternalId (Just . fromEmail) . externalId) + maybeLocale = Just locale instance IsUser (WrappedScimStoredUser SparTag) where maybeUserId = Just $ scimUserId . fromWrappedScimStoredUser @@ -675,20 +666,20 @@ instance IsUser User where maybeHandle = Just userHandle maybeName = Just (Just . userDisplayName) maybeTenant = Just $ \usr -> - Intra.veidFromBrigUser usr Nothing + Intra.veidFromBrigUser usr Nothing Nothing & either (const Nothing) - (preview (veidUref . SAML.uidTenant)) + (fmap SAML._uidTenant . veidUref) maybeSubject = Just $ \usr -> - Intra.veidFromBrigUser usr Nothing + Intra.veidFromBrigUser usr Nothing Nothing & either (const Nothing) - (preview (veidUref . SAML.uidSubject)) + (fmap SAML._uidSubject . veidUref) maybeScimExternalId = Just $ \usr -> - Intra.veidFromBrigUser usr Nothing + Intra.veidFromBrigUser usr Nothing Nothing & either (const Nothing) - (runValidExternalIdEither Intra.urefToExternalId (Just . fromEmail)) + (runValidScimIdEither Intra.urefToExternalId (Just . fromEmail)) maybeLocale = Just $ Just . userLocale -- | For all properties that are present in both @u1@ and @u2@, check that they match. @@ -737,16 +728,20 @@ setPreferredLanguage :: Language -> Scim.User.User SparTag -> Scim.User.User Spa setPreferredLanguage lang u = u {Scim.preferredLanguage = Scim.preferredLanguage u <|> Just (lan2Text lang)} -setDefaultRoleIfEmpty :: Scim.User.User a -> Scim.User.User a -setDefaultRoleIfEmpty u = +setDefaultRoleAndEmailsIfEmpty :: Scim.User.User a -> Scim.User.User a +setDefaultRoleAndEmailsIfEmpty u = u { Scim.User.roles = case Scim.User.roles u of [] -> [cs $ toByteString' defaultRole] + xs -> xs, + -- when the emails field is empty, we try to populate it with the externalId + Scim.User.emails = case Scim.User.emails u of + [] -> maybeToList ((\e -> Scim.Email.Email Nothing (Scim.Email.EmailAddress e) Nothing) <$> (emailAddressText =<< (Scim.User.externalId u))) xs -> xs } -- this is not always correct, but hopefully for the tests that we're using it in it'll do. -scimifyBrigUserHack :: User -> Email -> User +scimifyBrigUserHack :: User -> EmailAddress -> User scimifyBrigUserHack usr email = usr { userManagedBy = ManagedByScim, diff --git a/services/spar/test-integration/Util/Types.hs b/services/spar/test-integration/Util/Types.hs index 777470f2bb2..553ed09674e 100644 --- a/services/spar/test-integration/Util/Types.hs +++ b/services/spar/test-integration/Util/Types.hs @@ -43,7 +43,6 @@ where import Bilge import Cassandra as Cas -import Control.Exception import Control.Lens (makeLenses, view) import Crypto.Random.Types (MonadRandom (..)) import Data.Aeson @@ -111,13 +110,13 @@ instance FromJSON TestErrorLabel where -- A quick unit test that serves two purposes: (1) shows that it works (and helped with debugging); -- (2) demonstrates how to use it. -_unitTestTestErrorLabel :: IO () -_unitTestTestErrorLabel = do - let val :: Either String TestErrorLabel - val = Aeson.eitherDecode "{\"code\":404,\"message\":\"Not found.\",\"label\":\"not-found\"}" - unless (val == Right "not-found") $ - throwIO . ErrorCall . show $ - val +-- _unitTestTestErrorLabel :: IO () +-- _unitTestTestErrorLabel = do +-- let val :: Either String TestErrorLabel +-- val = Aeson.eitherDecode "{\"code\":404,\"message\":\"Not found.\",\"label\":\"not-found\"}" +-- unless (val == Right "not-found") $ +-- throwIO . ErrorCall . show $ +-- val -- | FUTUREWORK(fisx): we're running all tests for all constructors of `WireIdPAPIVersion`, -- which sometimes makes little sense. 'skipIdPAPIVersions' can be used to pend individual diff --git a/services/spar/test/Arbitrary.hs b/services/spar/test/Arbitrary.hs index bc7cc42d9c2..b9d3f0de56a 100644 --- a/services/spar/test/Arbitrary.hs +++ b/services/spar/test/Arbitrary.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE TypeSynonymInstances #-} {-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -Wno-redundant-constraints #-} @@ -45,26 +44,18 @@ instance Arbitrary IdPList where instance Arbitrary WireIdP where arbitrary = WireIdP <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary -deriving instance Arbitrary ScimToken - instance Arbitrary ScimTokenHash where arbitrary = hashScimToken <$> arbitrary -instance Arbitrary ScimTokenInfo where - arbitrary = - ScimTokenInfo - <$> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - -instance Arbitrary CreateScimTokenResponse where - arbitrary = CreateScimTokenResponse <$> arbitrary <*> arbitrary - instance Arbitrary ScimTokenList where arbitrary = ScimTokenList <$> arbitrary +instance Arbitrary ScimTokenListV6 where + arbitrary = ScimTokenListV6 <$> arbitrary + +instance Arbitrary ScimTokenName where + arbitrary = ScimTokenName <$> arbitrary + instance Arbitrary NoContent where arbitrary = pure NoContent diff --git a/services/spar/test/Test/Spar/Intra/BrigSpec.hs b/services/spar/test/Test/Spar/Intra/BrigSpec.hs index 769fc29387f..5eb232d3d70 100644 --- a/services/spar/test/Test/Spar/Intra/BrigSpec.hs +++ b/services/spar/test/Test/Spar/Intra/BrigSpec.hs @@ -21,6 +21,8 @@ module Test.Spar.Intra.BrigSpec where import Arbitrary () import Data.String.Conversions +import Data.These +import Data.These.Combinators import Imports import SAML2.WebSSO as SAML import Spar.Intra.BrigApp @@ -41,32 +43,33 @@ spec = do -- remove them. it "example" $ do - let have = - UrefOnly $ + let veid = + ValidScimId "V" . That $ UserRef (Issuer $ mkuri "http://wire.com/") ( either (error . show) id $ mkNameID (mkUNameIDTransient "V") (Just "kati") (Just "rolli") (Just "jaan") ) - want = UserSSOId (SAML.UserRef iss nam) + ssoId = UserSSOId (SAML.UserRef iss nam) iss :: SAML.Issuer = fromRight undefined $ SAML.decodeElem "http://wire.com/" nam :: SAML.NameID = fromRight undefined $ SAML.decodeElem "V" - veidToUserSSOId have `shouldBe` want - veidFromUserSSOId want `shouldBe` Right have + veidToUserSSOId veid `shouldBe` ssoId + veidFromUserSSOId ssoId Nothing `shouldBe` Right veid + it "another example" $ do - let have = - UrefOnly $ + let veid = + ValidScimId "PWkS" . That $ UserRef (Issuer $ mkuri "http://wire.com/") ( either (error . show) id $ mkNameID (mkUNameIDPersistent "PWkS") (Just "hendrik") Nothing (Just "marye") ) - want = UserSSOId (SAML.UserRef iss nam) + ssoId = UserSSOId (SAML.UserRef iss nam) iss :: SAML.Issuer = fromRight undefined $ SAML.decodeElem "http://wire.com/" nam :: SAML.NameID = fromRight undefined $ SAML.decodeElem "PWkS" - - veidToUserSSOId have `shouldBe` want - veidFromUserSSOId want `shouldBe` Right have + veidToUserSSOId veid `shouldBe` ssoId + veidFromUserSSOId ssoId Nothing `shouldBe` Right veid it "roundtrips" . property $ - \(x :: ValidExternalId) -> (veidFromUserSSOId @(Either String) . veidToUserSSOId) x === Right x + \(ValidScimIdNoNameIDQualifiers x) -> + veidFromUserSSOId @(Either String) (veidToUserSSOId x) (justHere x.validScimIdAuthInfo) === Right x diff --git a/services/spar/test/Test/Spar/Scim/UserSpec.hs b/services/spar/test/Test/Spar/Scim/UserSpec.hs index 3cbd3208669..09d09eee3ad 100644 --- a/services/spar/test/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test/Test/Spar/Scim/UserSpec.hs @@ -2,9 +2,9 @@ module Test.Spar.Scim.UserSpec where import Arbitrary () import Brig.Types.Intra -import Brig.Types.User import Control.Monad.Except (runExceptT) import Data.Handle (parseHandle) +import Data.HavePendingInvitations import Data.Id import Imports import Polysemy @@ -75,16 +75,16 @@ deleteUserAndAssertDeletionInSpar :: ] r ) => - UserAccount -> + User -> ScimTokenInfo -> Sem r (Either ScimError ()) deleteUserAndAssertDeletionInSpar acc tokenInfo = do - let tid = stiTeam tokenInfo - email = (fromJust . emailIdentity . fromJust . userIdentity . accountUser) acc - uid = (userId . accountUser) acc - ScimExternalIdStore.insert tid email uid + let tid = tokenInfo.stiTeam + email = (fromJust . emailIdentity . fromJust . userIdentity) acc + uid = userId acc + ScimExternalIdStore.insert tid (fromEmail email) uid r <- runExceptT $ deleteScimUser tokenInfo uid - lr <- ScimExternalIdStore.lookup tid email + lr <- ScimExternalIdStore.lookup tid (fromEmail email) liftIO $ lr `shouldBe` Nothing pure r @@ -120,7 +120,7 @@ ignoringState f = fmap snd . f mockBrig :: forall (r :: EffectRow) a. (Member (Embed IO) r) => - (UserId -> Maybe UserAccount) -> + (UserId -> Maybe User) -> DeleteUserResult -> Sem (BrigAccess ': r) a -> Sem r a @@ -131,26 +131,24 @@ mockBrig lookup_user delete_response = interpret $ \case liftIO $ expectationFailure $ "Unexpected effect (call to brig)" error "Throw error here to avoid implementation of all cases." -withActiveUser :: UserAccount -> UserId -> Maybe UserAccount +withActiveUser :: User -> UserId -> Maybe User withActiveUser acc uid = - if uid == (userId . accountUser) acc + if uid == userId acc then Just acc else Nothing -someActiveUser :: ScimTokenInfo -> IO UserAccount +someActiveUser :: ScimTokenInfo -> IO User someActiveUser tokenInfo = do user <- generate arbitrary pure $ - UserAccount - { accountStatus = Active, - accountUser = - user - { userDisplayName = Name "Some User", - userAccentId = defaultAccentId, - userPict = noPict, - userAssets = [], - userHandle = parseHandle "some-handle", - userIdentity = (Just . EmailIdentity . fromJust . parseEmail) "someone@wire.com", - userTeam = Just $ stiTeam tokenInfo - } + user + { userDisplayName = Name "Some User", + userEmailUnvalidated = Nothing, + userAccentId = defaultAccentId, + userStatus = Active, + userPict = noPict, + userAssets = [], + userHandle = parseHandle "some-handle", + userIdentity = (Just . EmailIdentity . fromJust . emailAddressText) "someone@wire.com", + userTeam = Just $ tokenInfo.stiTeam } diff --git a/services/spar/test/Test/Spar/ScimSpec.hs b/services/spar/test/Test/Spar/ScimSpec.hs index 936e2f4f99c..212cfa7631b 100644 --- a/services/spar/test/Test/Spar/ScimSpec.hs +++ b/services/spar/test/Test/Spar/ScimSpec.hs @@ -55,7 +55,7 @@ import qualified Web.Scim.Schema.User.Name as ScimN import Wire.API.User.RichInfo spec :: Spec -spec = describe "toScimStoredUser'" $ do +spec = describe "toScimStoredUser" $ do it "works" $ do let usr :: Scim.User SparTag usr = @@ -115,7 +115,7 @@ spec = describe "toScimStoredUser'" $ do URI.ByteString.parseURI laxURIParserOptions "https://127.0.0.1/scim/v2/" uid = Id . fromJust . UUID.fromText $ "90b5ee1c-088e-11e9-9a16-73f80f483813" result :: ScimC.StoredUser SparTag - result = toScimStoredUser' now now baseuri uid usr + result = toScimStoredUser now now baseuri uid usr Scim.meta result `shouldBe` meta Scim.value (Scim.thing result) `shouldBe` usr it "roundtrips" . property $ do diff --git a/snapshots/README.md b/snapshots/README.md deleted file mode 100644 index 0a9f240cac0..00000000000 --- a/snapshots/README.md +++ /dev/null @@ -1,24 +0,0 @@ -This directory contains [custom Stack snapshots][custom] used for Wire code. - -[custom]: https://docs.haskellstack.org/en/stable/custom_snapshot/ - -Snapshot definitions should never be changed (once committed to `develop`), because in other -repositories we refer to snapshot definitions by URL. This goes for *ANY* change! What -matters is that the sha256 hash of the file remains intact! - -(Rationale: Stack only downloads snapshot definitions once, and never checks whether they have -changed. If a snapshot changes and you have a repo that depends on it, you will get -inconsistent results depending on whether you've built that repo before or not.) - -To add, modify, or remove packages, a new snapshot should be created. It can be based on the -previous snapshot version. For major changes, e.g. LTS bumps, it's better to create a snapshot -from scratch. - -Some packages in this snapshot reference tar files instead of Git repos. This is due to -several issues in Stack that make working with big Git repositories unpleasant: - - * https://github.com/commercialhaskell/stack/issues/4345 - * https://github.com/commercialhaskell/stack/issues/3551 - -Unless the fixes to those are released, it's recommended to use GitHub's tar releases for -packages with big repos. diff --git a/snapshots/wire-1.0.yaml b/snapshots/wire-1.0.yaml deleted file mode 100644 index 9902d55ac68..00000000000 --- a/snapshots/wire-1.0.yaml +++ /dev/null @@ -1,122 +0,0 @@ -# DO NOT MODIFY THIS FILE. See README.md to learn why. - -resolver: lts-12.10 -name: wire-1.0 - -packages: - -############################################################ -# Packages where we need specific lower/upper bounds -############################################################ - -- async-2.2.1 -- hinotify-0.4 -- fsnotify-0.3.0.1 -- base-prelude-1.3 -- base58-bytestring-0.1.0 -- cql-4.0.1 -- currency-codes-2.0.0.0 -- data-timeout-0.3 -- geoip2-0.3.1.0 -- mime-0.4.0.2 -- multiset-0.3.4.1 -- text-icu-translit-0.1.0.7 -- wai-middleware-gunzip-0.0.2 -- network-uri-static-0.1.1.0 # includes 'relativeReference' -- list-t-1.0.1 # v1.0.0.1 doesn't build -- unliftio-0.2.10 # for pooled concurrency utils in UnliftIO.Async -- network-2.7.0.2 # to get nicer errors when connections fail -- HaskellNet-SSL-0.3.4.1 # first version to support network-2.7 - -# DEPRECATED: hs-collectd not in use anymore, remove in next stack snapshot -- git: https://github.com/kim/hs-collectd - commit: 885da222be2375f78c7be36127620ed772b677c9 - -- git: https://github.com/kim/snappy-framing - commit: d99f702c0086729efd6848dea8a01e5266c3a61c - -- git: https://gitlab.com/twittner/wai-routing - commit: 7e996a93fec5901767f845a50316b3c18e51a61d - -# Includes the changes from -- git: https://gitlab.com/twittner/cql-io.git - commit: 8b91d053c469887a427e8c075cef43139fa189c4 - -############################################################ -# Packages that are not on Stackage -############################################################ - -- bloodhound-0.16.0.0 -- template-0.2.0.10 -- wai-route-0.4.0 -- text-format-0.3.2 -- redis-io-1.0.0 -- redis-resp-1.0.0 -- servant-multipart-0.11.2 -- wai-middleware-prometheus-1.0.0 -- prometheus-client-1.0.0 -- hedgehog-quickcheck-0.1 -- invertible-hxt-0.1 -- stomp-queue-0.3.1 -- stompl-0.5.0 - -############################################################ -# Forks -############################################################ - -# Our fork of multihash with relaxed upper bounds -- git: https://github.com/wireapp/haskell-multihash.git - commit: 300a6f46384bfca33e545c8bab52ef3717452d12 - -# Our fork of aws with minor fixes -- git: https://github.com/wireapp/aws - commit: 42695688fc20f80bf89cec845c57403954aab0a2 - -# https://github.com/hspec/hspec-wai/pull/49 -- git: https://github.com/wireapp/hspec-wai - commit: ca10d13deab929f1cc3a569abea2e7fbe35fdbe3 - -# Our fork of http-client gives us access to some guts that the upstream http-client doesn't -# expose; see -# -# The important commits for us are: -# -# * https://github.com/snoyberg/http-client/compare/master...wireapp:connection-guts -# -# The archive corresponds to commit 6a4ac55edf5e62574210c77a1468fa7accb81670. -- archive: https://github.com/wireapp/http-client/archive/wire-2019-01-25.tar.gz - subdirs: - - http-client - - http-client-openssl - - http-client-tls - - http-conduit - -# amazonka-1.6.0 is buggy: https://github.com/brendanhay/amazonka/issues/466 -# amazonka-HEAD is also buggy: https://github.com/brendanhay/amazonka/issues/490 -# -# Therefore we use our own fork of amazonka here. More precisely, we pull two libraries out of -# it: amazonka and amazonka-core. Other packages weren't changed between 1.6.0 and this -# commit, so we can use Stackage-supplied versions for them. -# -# The important commits for us are: -# -# * https://github.com/brendanhay/amazonka/commit/2688190f -# * https://github.com/brendanhay/amazonka/pull/493/files -# -# The archive corresponds to commit 52896fd46ef6812708e9e4d7456becc692698f6b. -- archive: https://github.com/wireapp/amazonka/archive/wire-2019-01-25.tar.gz - subdirs: - - amazonka - - core - -############################################################ -# Wire packages (only ones that change infrequently) -############################################################ - -- git: https://github.com/wireapp/cryptobox-haskell - commit: 7546a1a25635ef65183e3d44c1052285e8401608 # master (Jul 21, 2016) - -- git: https://github.com/wireapp/hsaml2 - commit: 000868849efd85ba82d2bf0ac5757f801d49ad5a # master (Sep 10, 2018) - -# DO NOT MODIFY THIS FILE. See README.md to learn why. diff --git a/snapshots/wire-1.1.yaml b/snapshots/wire-1.1.yaml deleted file mode 100644 index c3e61914ba6..00000000000 --- a/snapshots/wire-1.1.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# DO NOT MODIFY THIS FILE. See README.md to learn why. - -resolver: https://raw.githubusercontent.com/wireapp/wire-server/develop/snapshots/wire-1.0.yaml -name: wire-1.1 - -packages: -- git: https://github.com/wireapp/hsaml2 - commit: 678997815033584e023205fe774d16201ccf8f62 # master (Feb 13, 2019) diff --git a/snapshots/wire-1.2.yaml b/snapshots/wire-1.2.yaml deleted file mode 100644 index ce06e85e192..00000000000 --- a/snapshots/wire-1.2.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# DO NOT MODIFY THIS FILE. See README.md to learn why. - -resolver: https://raw.githubusercontent.com/wireapp/wire-server/develop/snapshots/wire-1.1.yaml -name: wire-1.2 - -packages: -- cql-io-1.1.0 # the MR in wire-1.0.yaml has been released on hackage. -- cql-io-tinylog-0.1.0 diff --git a/snapshots/wire-1.3.yaml b/snapshots/wire-1.3.yaml deleted file mode 100644 index f3774ab20a9..00000000000 --- a/snapshots/wire-1.3.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# DO NOT MODIFY THIS FILE. See README.md to learn why. - -resolver: https://raw.githubusercontent.com/wireapp/wire-server/develop/snapshots/wire-1.2.yaml -name: wire-1.3 - -packages: -- tinylog-0.15.0 diff --git a/snapshots/wire-1.4.yaml b/snapshots/wire-1.4.yaml deleted file mode 100644 index 023c078c9ba..00000000000 --- a/snapshots/wire-1.4.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# DO NOT MODIFY THIS FILE. See README.md to learn why. - -resolver: https://raw.githubusercontent.com/wireapp/wire-server/develop/snapshots/wire-1.3.yaml -name: wire-1.4 - -packages: - # http-client forked by wire, commit 032b6503ab0c47f8f85bf48e0beb1f895a95bb27 - # Contains patches on top of http-client-openssl-0.2.2.0: - # - 89136497b8e0fa0624c1451883eb011347203532 - # - 916b04313c6864e02ebed4278b43b971189c61cd - # - 64ebec4fe7b48c131b7d6f0f8d7a6c6cacae70e6 - # - 78ebeead1a2efb17c55fb72d2d0041295d1271b8 - # - 032b6503ab0c47f8f85bf48e0beb1f895a95bb27 - # These provide a hacky way to implement TLS certificate pinning. -- archive: https://github.com/wireapp/http-client/archive/wire-2019-11-04.tar.gz - subdirs: - - http-client - - http-client-openssl - - http-client-tls - - http-conduit diff --git a/snapshots/wire-2.0.yaml b/snapshots/wire-2.0.yaml deleted file mode 100644 index fffe8c8f6d0..00000000000 --- a/snapshots/wire-2.0.yaml +++ /dev/null @@ -1,101 +0,0 @@ -# DO NOT MODIFY THIS FILE. See README.md to learn why. - -resolver: lts-14.12 -name: wire-2.0 - -# compiler: ghc-8.6.5 - -packages: -- git: https://github.com/kim/hs-collectd - commit: 885da222be2375f78c7be36127620ed772b677c9 - -- git: https://github.com/kim/snappy-framing - commit: d99f702c0086729efd6848dea8a01e5266c3a61c - -- git: https://gitlab.com/twittner/wai-routing - commit: 7e996a93fec5901767f845a50316b3c18e51a61d - -# Our fork of multihash with relaxed upper bounds -- git: https://github.com/wireapp/haskell-multihash.git - commit: 300a6f46384bfca33e545c8bab52ef3717452d12 - -# Our fork of aws with minor fixes -- git: https://github.com/wireapp/aws - commit: 42695688fc20f80bf89cec845c57403954aab0a2 - -# https://github.com/hspec/hspec-wai/pull/49 -- git: https://github.com/wireapp/hspec-wai - commit: 0a5142cd3ba48116ff059c041348b817fb7bdb25 - -# amazonka-1.6.0 is buggy: https://github.com/brendanhay/amazonka/issues/466 -# amazonka-HEAD is also buggy: https://github.com/brendanhay/amazonka/issues/490 -# -# Therefore we use our own fork of amazonka here. More precisely, we pull two libraries out of -# it: amazonka and amazonka-core. Other packages weren't changed between 1.6.0 and this -# commit, so we can use Stackage-supplied versions for them. -# -# The important commits for us are: -# -# * https://github.com/brendanhay/amazonka/commit/2688190f -# * https://github.com/brendanhay/amazonka/pull/493/files -# -# The archive corresponds to commit 52896fd46ef6812708e9e4d7456becc692698f6b. -- archive: https://github.com/wireapp/amazonka/archive/wire-2019-01-25.tar.gz - sha256: b1cecd0e5e17cd41395ec56e4f6926e0c8bbeef493ff3a575bf7561b72db0525 - size: 11128501 - subdirs: - - amazonka - - core - -############################################################ -# Wire packages (only ones that change infrequently) -############################################################ - -- git: https://github.com/wireapp/cryptobox-haskell - commit: 7546a1a25635ef65183e3d44c1052285e8401608 # master (Jul 21, 2016) - -- git: https://github.com/wireapp/hsaml2 - commit: 2d56f432464e9bf6be8ee214d7f5bb28639457ac # master (Feb 4, 2020) - -- git: https://github.com/wireapp/http-client - commit: a160cef95d9daaff7d9cfe616d95754c2f8202bf # master (Feb 4, 2020) - subdirs: - - http-client - - http-client-openssl - - http-client-tls - - http-conduit - -# Dropped from upstream snapshot -- bloodhound-0.16.0.0 -- template-0.2.0.10 -- HaskellNet-0.5.1 -- HaskellNet-SSL-0.3.4.1 -- snappy-0.2.0.2 -- smtp-mail-0.2.0.0 -- stm-containers-1.1.0.4 -- redis-io-1.0.0 -- redis-resp-1.0.0 -- hedgehog-quickcheck-0.1.1 - -# Only in nightly -- stm-hamt-1.2.0.4 -- optics-th-0.2 -- primitive-unlifted-0.1.2.0 - -# Not on stackage -- currency-codes-3.0.0.1 -- mime-0.4.0.2 -- data-timeout-0.3.1 -- geoip2-0.4.0.1 -- stomp-queue-0.3.1 -- text-icu-translit-0.1.0.7 -- wai-middleware-gunzip-0.0.2 -- cql-io-tinylog-0.1.0 -- invertible-hxt-0.1 -- network-uri-static-0.1.2.1 -- base58-bytestring-0.1.0 -- stompl-0.5.0 -- pattern-trie-0.1.0 - -# Not latest as latst one breaks wai-routing -- wai-route-0.4.0 diff --git a/snapshots/wire-2.1.yaml b/snapshots/wire-2.1.yaml deleted file mode 100644 index 057d5182856..00000000000 --- a/snapshots/wire-2.1.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# DO NOT MODIFY THIS FILE. See README.md to learn why. - -resolver: https://raw.githubusercontent.com/wireapp/wire-server/develop/snapshots/wire-2.0.yaml -name: wire-2.1 - -packages: -# amazonka-1.6.1 is buggy: https://github.com/brendanhay/amazonka/issues/466 -# Therefore we pin an unreleased commit directly. -# -# More precisely, we pull just some libraries out of it, -# the other packages weren't changed between 1.6.1 and this commit, -# so we can use Stackage-supplied versions for them. -# See https://github.com/brendanhay/amazonka/compare/1.6.1...9cf5b5777b69ac494d23d43a692294882927df34 -# -# Once there has been made a new hackage release, we can use that instead. -- archive: https://github.com/brendanhay/amazonka/archive/9cf5b5777b69ac494d23d43a692294882927df34.tar.gz - sha256: c3044f803a7652aee88fe600a97321175cdc1443d671246ba7ff78e14bf5b49f - size: 11137527 - subdirs: - - amazonka - - amazonka-elb - - amazonka-redshift - - amazonka-route53 - - core diff --git a/snapshots/wire-2.2.yaml b/snapshots/wire-2.2.yaml deleted file mode 100644 index 26b49c246a1..00000000000 --- a/snapshots/wire-2.2.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# DO NOT MODIFY THIS FILE. See README.md to learn why. - -resolver: https://raw.githubusercontent.com/wireapp/wire-server/develop/snapshots/wire-2.1.yaml -name: wire-2.2 - -packages: -- git: https://github.com/wireapp/hsaml2 - commit: cc47da1d097b0b26595b8889e40c33c6c0c1c551 # master (Feb 27, 2020) diff --git a/snapshots/wire-3.0.yaml b/snapshots/wire-3.0.yaml deleted file mode 100644 index 655d191c63e..00000000000 --- a/snapshots/wire-3.0.yaml +++ /dev/null @@ -1,104 +0,0 @@ -# DO NOT MODIFY THIS FILE. See README.md to learn why. - -resolver: lts-14.12 -name: wire-3.0 - -# compiler: ghc-8.6.5 - -packages: -- git: https://github.com/kim/hs-collectd - commit: 885da222be2375f78c7be36127620ed772b677c9 - -- git: https://github.com/kim/snappy-framing - commit: d99f702c0086729efd6848dea8a01e5266c3a61c - -- git: https://gitlab.com/twittner/wai-routing - commit: 7e996a93fec5901767f845a50316b3c18e51a61d - -# Includes the changes from -# - git: https://gitlab.com/twittner/cql-io.git -# commit: 8b91d053c469887a427e8c075cef43139fa189c4 - -# Our fork of multihash with relaxed upper bounds -- git: https://github.com/wireapp/haskell-multihash.git - commit: 300a6f46384bfca33e545c8bab52ef3717452d12 - -# Our fork of aws with minor fixes -- git: https://github.com/wireapp/aws - commit: 42695688fc20f80bf89cec845c57403954aab0a2 - -# https://github.com/hspec/hspec-wai/pull/49 -- git: https://github.com/wireapp/hspec-wai - commit: 0a5142cd3ba48116ff059c041348b817fb7bdb25 - -# amazonka-1.6.1 is buggy: https://github.com/brendanhay/amazonka/issues/466 -# Therefore we pin an unreleased commit directly. -# -# More precisely, we pull just some libraries out of it, -# the other packages weren't changed between 1.6.1 and this commit, -# so we can use Stackage-supplied versions for them. -# See https://github.com/brendanhay/amazonka/compare/1.6.1...9cf5b5777b69ac494d23d43a692294882927df34 -# -# Once there has been made a new hackage release, we can use that instead. -- archive: https://github.com/brendanhay/amazonka/archive/9cf5b5777b69ac494d23d43a692294882927df34.tar.gz - sha256: c3044f803a7652aee88fe600a97321175cdc1443d671246ba7ff78e14bf5b49f - size: 11137527 - subdirs: - - amazonka - - amazonka-elb - - amazonka-redshift - - amazonka-route53 - - core - -############################################################ -# Wire packages (only ones that change infrequently) -############################################################ - -- git: https://github.com/wireapp/cryptobox-haskell - commit: 7546a1a25635ef65183e3d44c1052285e8401608 # master (Jul 21, 2016) - -- git: https://github.com/wireapp/hsaml2 - commit: cc47da1d097b0b26595b8889e40c33c6c0c1c551 # master (Feb 27, 2020) - -- git: https://github.com/wireapp/http-client - commit: a160cef95d9daaff7d9cfe616d95754c2f8202bf # master (Feb 4, 2020) - subdirs: - - http-client - - http-client-openssl - - http-client-tls - - http-conduit - -# Dropped from upstream snapshot -- bloodhound-0.16.0.0 -- template-0.2.0.10 -- HaskellNet-0.5.1 -- HaskellNet-SSL-0.3.4.1 -- snappy-0.2.0.2 -- smtp-mail-0.2.0.0 -- stm-containers-1.1.0.4 -- redis-io-1.0.0 -- redis-resp-1.0.0 -- hedgehog-quickcheck-0.1.1 - -# Only in nightly -- stm-hamt-1.2.0.4 -- optics-th-0.2 -- primitive-unlifted-0.1.2.0 - -# Not on stackage -- currency-codes-3.0.0.1 -- mime-0.4.0.2 -- data-timeout-0.3.1 -- geoip2-0.4.0.1 -- stomp-queue-0.3.1 -- text-icu-translit-0.1.0.7 -- wai-middleware-gunzip-0.0.2 -- cql-io-tinylog-0.1.0 -- invertible-hxt-0.1 -- network-uri-static-0.1.2.1 -- base58-bytestring-0.1.0 -- stompl-0.5.0 -- pattern-trie-0.1.0 - -# Not latest as latst one breaks wai-routing -- wai-route-0.4.0 diff --git a/tools/db/inconsistencies/src/DanglingUserKeys.hs b/tools/db/inconsistencies/src/DanglingUserKeys.hs index 3d27d4c208c..de0bc70a7a9 100644 --- a/tools/db/inconsistencies/src/DanglingUserKeys.hs +++ b/tools/db/inconsistencies/src/DanglingUserKeys.hs @@ -81,8 +81,7 @@ data Inconsistency = Inconsistency userId :: UserId, time :: Writetime UserId, status :: Maybe (WithWritetime AccountStatus), - userEmail :: Maybe (WithWritetime Email), - userPhone :: Maybe (WithWritetime Phone), + userEmail :: Maybe (WithWritetime EmailAddress), inconsistencyCase :: Text } deriving (Generic) @@ -113,7 +112,7 @@ getKeys = paginateC cql (paramsP LocalQuorum () pageSize) x5 cql = "SELECT key, user, writetime(user) from user_keys" parseKey :: Text -> Maybe EmailKey -parseKey t = mkEmailKey <$> parseEmail t +parseKey t = mkEmailKey <$> emailAddressText t instance Cql EmailKey where ctype = Tagged TextColumn @@ -130,13 +129,13 @@ instance Cql EmailKey where instance Aeson.ToJSON EmailKey where toJSON = Aeson.toJSON . emailKeyUniq -type UserDetailsRow = (Maybe AccountStatus, Maybe (Writetime AccountStatus), Maybe Email, Maybe (Writetime Email), Maybe Phone, Maybe (Writetime Phone)) +type UserDetailsRow = (Maybe AccountStatus, Maybe (Writetime AccountStatus), Maybe EmailAddress, Maybe (Writetime EmailAddress)) getUserDetails :: UserId -> Client (Maybe UserDetailsRow) getUserDetails uid = retry x5 $ query1 cql (params LocalQuorum (Identity uid)) where cql :: PrepQuery R (Identity UserId) UserDetailsRow - cql = "SELECT status, writetime(status), email, writetime(email), phone, writetime(phone) from user where id = ?" + cql = "SELECT status, writetime(status), email, writetime(email) from user where id = ?" checkKey :: Logger -> ClientState -> EmailKey -> Bool -> IO (Maybe Inconsistency) checkKey l brig key repairData = do @@ -179,16 +178,14 @@ checkUser l brig key uid time repairData = do Nothing -> do let status = Nothing userEmail = Nothing - userPhone = Nothing inconsistencyCase = "2." when repairData $ -- case 2. runClient brig $ freeEmailKey l key pure . Just $ Inconsistency {userId = uid, ..} - Just (mStatus, mStatusWriteTime, mEmail, mEmailWriteTime, mPhone, mPhoneWriteTime) -> do + Just (mStatus, mStatusWriteTime, mEmail, mEmailWriteTime) -> do let status = WithWritetime <$> mStatus <*> mStatusWriteTime userEmail = WithWritetime <$> mEmail <*> mEmailWriteTime - userPhone = WithWritetime <$> mPhone <*> mPhoneWriteTime statusError = case mStatus of Nothing -> True Just Deleted -> True diff --git a/tools/db/inconsistencies/src/EmailLessUsers.hs b/tools/db/inconsistencies/src/EmailLessUsers.hs index 021a5064ae3..5d93ad9c5b4 100644 --- a/tools/db/inconsistencies/src/EmailLessUsers.hs +++ b/tools/db/inconsistencies/src/EmailLessUsers.hs @@ -80,7 +80,7 @@ data EmailInfo = EmailInfo { userId :: UserId, status :: WithWritetime AccountStatus, -- | Email in the user table - userEmail :: WithWritetime Email, + userEmail :: WithWritetime EmailAddress, -- | Email in the user_keys table emailKey :: Maybe (WithWritetime UserId), inconsistencyCase :: Text @@ -112,13 +112,13 @@ getUsers = paginateC cql (paramsP LocalQuorum () pageSize) x5 cql :: PrepQuery R () UserDetailsRow cql = "SELECT id, status, writetime(status), email, writetime(email), activated from user" -type UserDetailsRow = (UserId, Maybe AccountStatus, Maybe (Writetime AccountStatus), Maybe Email, Maybe (Writetime Email), Bool) +type UserDetailsRow = (UserId, Maybe AccountStatus, Maybe (Writetime AccountStatus), Maybe EmailAddress, Maybe (Writetime EmailAddress), Bool) -insertMissingEmail :: Logger -> ClientState -> Email -> UserId -> IO () +insertMissingEmail :: Logger -> ClientState -> EmailAddress -> UserId -> IO () insertMissingEmail l brig email uid = do runClient brig $ K.insertKey l uid (mkEmailKey email) -userWithEmailAndStatus :: UserDetailsRow -> Maybe (UserId, AccountStatus, Writetime AccountStatus, Email, Writetime Email) +userWithEmailAndStatus :: UserDetailsRow -> Maybe (UserId, AccountStatus, Writetime AccountStatus, EmailAddress, Writetime EmailAddress) userWithEmailAndStatus (uid, mStatus, mStatusWritetime, mEmail, mEmailWritetime, activated) = do let act = if activated then Just True else Nothing case (,,,,) <$> mStatus <*> mStatusWritetime <*> mEmail <*> mEmailWritetime <*> act of @@ -133,7 +133,7 @@ repairUser l brig repairData uid = do Nothing -> pure Nothing Just x -> checkUser l brig repairData x -checkUser :: Logger -> ClientState -> Bool -> (UserId, AccountStatus, Writetime AccountStatus, Email, Writetime Email) -> IO (Maybe EmailInfo) +checkUser :: Logger -> ClientState -> Bool -> (UserId, AccountStatus, Writetime AccountStatus, EmailAddress, Writetime EmailAddress) -> IO (Maybe EmailInfo) checkUser l brig repairData (uid, statusValue, statusWritetime, userEmailValue, userEmailWriteTime) = do let status = WithWritetime statusValue statusWritetime userEmail = WithWritetime userEmailValue userEmailWriteTime diff --git a/tools/db/inconsistencies/src/Options.hs b/tools/db/inconsistencies/src/Options.hs index 95f7e27e95f..ad81539b070 100644 --- a/tools/db/inconsistencies/src/Options.hs +++ b/tools/db/inconsistencies/src/Options.hs @@ -20,7 +20,6 @@ module Options where import Cassandra qualified as C -import Data.Id import Data.Text qualified as Text import Imports import Options.Applicative @@ -97,15 +96,6 @@ inconsistenciesFileParser = <> metavar "FILEPATH" ) -teamIdParser :: Parser TeamId -teamIdParser = - option - (eitherReader (parseIdFromText . Text.pack)) - ( long "team-id" - <> help "Team id to search into" - <> metavar "TEAM_ID" - ) - cassandraSettingsParser :: String -> Parser CassandraSettings cassandraSettingsParser ks = CassandraSettings diff --git a/tools/db/move-team/src/Schema.hs b/tools/db/move-team/src/Schema.hs index fdf17e81cae..00c08360bf6 100644 --- a/tools/db/move-team/src/Schema.hs +++ b/tools/db/move-team/src/Schema.hs @@ -22,7 +22,6 @@ module Schema where import Cassandra import Common import Data.Conduit -import Data.Handle (Handle) import Data.IP (IP) import Data.Id import Data.Time @@ -32,7 +31,6 @@ import Imports import System.FilePath.Posix (()) import Types import Wire.API.Team.Permission -import Wire.API.User.Password (PasswordResetKey) -- This file was autogenerated by move-team-generate @@ -178,14 +176,6 @@ importBrigLoginCodes Env {..} path = do type RowBrigPasswordReset = (Maybe Ascii, Maybe Ascii, Maybe Int32, Maybe UTCTime, Maybe UUID) -selectBrigPasswordReset :: PrepQuery R (Identity [PasswordResetKey]) RowBrigPasswordReset -selectBrigPasswordReset = "SELECT key, code, retries, timeout, user FROM password_reset WHERE key in ?" - -readBrigPasswordReset :: Env -> [PasswordResetKey] -> ConduitM () [RowBrigPasswordReset] IO () -readBrigPasswordReset Env {..} reset_keys = - transPipe (runClient envBrig) $ - paginateC selectBrigPasswordReset (paramsP LocalQuorum (pure reset_keys) envPageSize) x5 - selectBrigPasswordResetAll :: PrepQuery R () RowBrigPasswordReset selectBrigPasswordResetAll = "SELECT key, code, retries, timeout, user FROM password_reset" @@ -360,10 +350,10 @@ importBrigRichInfo Env {..} path = do -- brig.user -type RowBrigUser = (Maybe UUID, Maybe [Float], Maybe Int32, Maybe Bool, Maybe [AssetIgnoreData], Maybe Ascii, Maybe Text, Maybe UTCTime, Maybe Text, Maybe Ascii, Maybe Int32, Maybe Text, Maybe Blob, Maybe Text, Maybe [Blob], Maybe UUID, Maybe Bool, Maybe UUID, Maybe Text, Maybe Int32, Maybe UUID) +type RowBrigUser = (Maybe UUID, Maybe [Float], Maybe Int32, Maybe Bool, Maybe [AssetIgnoreData], Maybe Ascii, Maybe Text, Maybe UTCTime, Maybe Text, Maybe Ascii, Maybe Int32, Maybe Text, Maybe Blob, Maybe [Blob], Maybe UUID, Maybe Bool, Maybe UUID, Maybe Text, Maybe Int32, Maybe UUID) selectBrigUser :: PrepQuery R (Identity [UserId]) RowBrigUser -selectBrigUser = "SELECT id, accent, accent_id, activated, assets, country, email, expires, handle, language, managed_by, name, password, phone, picture, provider, searchable, service, sso_id, status, team FROM user WHERE id in ?" +selectBrigUser = "SELECT id, accent, accent_id, activated, assets, country, email, expires, handle, language, managed_by, name, password, picture, provider, searchable, service, sso_id, status, team FROM user WHERE id in ?" readBrigUser :: Env -> [UserId] -> ConduitM () [RowBrigUser] IO () readBrigUser Env {..} uids = @@ -371,7 +361,7 @@ readBrigUser Env {..} uids = paginateC selectBrigUser (paramsP LocalQuorum (pure uids) envPageSize) x5 selectBrigUserAll :: PrepQuery R () RowBrigUser -selectBrigUserAll = "SELECT id, accent, accent_id, activated, assets, country, email, expires, handle, language, managed_by, name, password, phone, picture, provider, searchable, service, sso_id, status, team FROM user" +selectBrigUserAll = "SELECT id, accent, accent_id, activated, assets, country, email, expires, handle, language, managed_by, name, password, picture, provider, searchable, service, sso_id, status, team FROM user" readBrigUserAll :: Env -> ConduitM () [RowBrigUser] IO () readBrigUserAll Env {..} = @@ -388,7 +378,7 @@ exportBrigUserFull env path = do insertBrigUser :: PrepQuery W RowBrigUser () insertBrigUser = - "INSERT INTO user (id, accent, accent_id, activated, assets, country, email, expires, handle, language, managed_by, name, password, phone, picture, provider, searchable, service, sso_id, status, team) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + "INSERT INTO user (id, accent, accent_id, activated, assets, country, email, expires, handle, language, managed_by, name, password, picture, provider, searchable, service, sso_id, status, team) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" importBrigUser :: Env -> FilePath -> IO () importBrigUser Env {..} path = do @@ -408,14 +398,6 @@ importBrigUser Env {..} path = do type RowBrigUserHandle = (Maybe Text, Maybe UUID) -selectBrigUserHandle :: PrepQuery R (Identity [Handle]) RowBrigUserHandle -selectBrigUserHandle = "SELECT handle, user FROM user_handle WHERE handle in ?" - -readBrigUserHandle :: Env -> [Handle] -> ConduitM () [RowBrigUserHandle] IO () -readBrigUserHandle Env {..} handles = - transPipe (runClient envBrig) $ - paginateC selectBrigUserHandle (paramsP LocalQuorum (pure handles) envPageSize) x5 - selectBrigUserHandleAll :: PrepQuery R () RowBrigUserHandle selectBrigUserHandleAll = "SELECT handle, user FROM user_handle" @@ -454,14 +436,6 @@ importBrigUserHandle Env {..} path = do type RowBrigUserKeys = (Maybe Text, Maybe UUID) -selectBrigUserKeys :: PrepQuery R (Identity [Int32]) RowBrigUserKeys -selectBrigUserKeys = "SELECT key, user FROM user_keys WHERE key in ?" - -readBrigUserKeys :: Env -> [Int32] -> ConduitM () [RowBrigUserKeys] IO () -readBrigUserKeys Env {..} keys = - transPipe (runClient envBrig) $ - paginateC selectBrigUserKeys (paramsP LocalQuorum (pure keys) envPageSize) x5 - selectBrigUserKeysAll :: PrepQuery R () RowBrigUserKeys selectBrigUserKeysAll = "SELECT key, user FROM user_keys" @@ -1144,14 +1118,6 @@ importSparScimUserTimes Env {..} path = do type RowSparUser = (Maybe Text, Maybe Text, Maybe UUID) -selectSparUser :: PrepQuery R (Identity [Text]) RowSparUser -selectSparUser = "SELECT issuer, sso_id, uid FROM user WHERE issuer in ?" - -readSparUser :: Env -> [Text] -> ConduitM () [RowSparUser] IO () -readSparUser Env {..} issuer = - transPipe (runClient envSpar) $ - paginateC selectSparUser (paramsP LocalQuorum (pure issuer) envPageSize) x5 - selectSparUserAll :: PrepQuery R () RowSparUser selectSparUserAll = "SELECT issuer, sso_id, uid FROM user" diff --git a/tools/db/move-team/src/Types.hs b/tools/db/move-team/src/Types.hs index 3f7ef7ebe36..2839331ccb1 100644 --- a/tools/db/move-team/src/Types.hs +++ b/tools/db/move-team/src/Types.hs @@ -104,8 +104,8 @@ deriving instance ToJSON TimeUuid deriving instance FromJSON TimeUuid -instance (ToJSON a, ToJSON b, ToJSON c, ToJSON d, ToJSON e, ToJSON f, ToJSON g, ToJSON h, ToJSON i, ToJSON j, ToJSON k, ToJSON l, ToJSON m, ToJSON n, ToJSON o, ToJSON p, ToJSON q, ToJSON r, ToJSON s, ToJSON t, ToJSON u) => ToJSON ((,,,,,,,,,,,,,,,,,,,,) a b c d e f g h i j k l m n o p q r s t u) where - toJSON (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u) = +instance (ToJSON a, ToJSON b, ToJSON c, ToJSON d, ToJSON e, ToJSON f, ToJSON g, ToJSON h, ToJSON i, ToJSON j, ToJSON k, ToJSON l, ToJSON m, ToJSON n, ToJSON o, ToJSON p, ToJSON q, ToJSON r, ToJSON s, ToJSON t) => ToJSON ((,,,,,,,,,,,,,,,,,,,) a b c d e f g h i j k l m n o p q r s t) where + toJSON (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) = Array $ V.fromList [ toJSON a, @@ -127,14 +127,13 @@ instance (ToJSON a, ToJSON b, ToJSON c, ToJSON d, ToJSON e, ToJSON f, ToJSON g, toJSON q, toJSON r, toJSON s, - toJSON t, - toJSON u + toJSON t ] -instance (FromJSON a, FromJSON b, FromJSON c, FromJSON d, FromJSON e, FromJSON f, FromJSON g, FromJSON h, FromJSON i, FromJSON j, FromJSON k, FromJSON l, FromJSON m, FromJSON n, FromJSON o, FromJSON p, FromJSON q, FromJSON r, FromJSON s, FromJSON t, FromJSON u) => FromJSON ((,,,,,,,,,,,,,,,,,,,,) a b c d e f g h i j k l m n o p q r s t u) where +instance (FromJSON a, FromJSON b, FromJSON c, FromJSON d, FromJSON e, FromJSON f, FromJSON g, FromJSON h, FromJSON i, FromJSON j, FromJSON k, FromJSON l, FromJSON m, FromJSON n, FromJSON o, FromJSON p, FromJSON q, FromJSON r, FromJSON s, FromJSON t) => FromJSON ((,,,,,,,,,,,,,,,,,,,) a b c d e f g h i j k l m n o p q r s t) where parseJSON = withArray "Tuple" $ \case - (toList -> [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u]) -> - (,,,,,,,,,,,,,,,,,,,,) + (toList -> [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t]) -> + (,,,,,,,,,,,,,,,,,,,) <$> parseJSON a <*> parseJSON b <*> parseJSON c @@ -155,5 +154,4 @@ instance (FromJSON a, FromJSON b, FromJSON c, FromJSON d, FromJSON e, FromJSON f <*> parseJSON r <*> parseJSON s <*> parseJSON t - <*> parseJSON u _ -> fail "Expected array of length 21" diff --git a/tools/db/phone-users/src/PhoneUsers/Types.hs b/tools/db/phone-users/src/PhoneUsers/Types.hs index 9a19a26f001..087dafd2b70 100644 --- a/tools/db/phone-users/src/PhoneUsers/Types.hs +++ b/tools/db/phone-users/src/PhoneUsers/Types.hs @@ -158,7 +158,7 @@ type Activated = Bool data UserRow = UserRow { id :: UserId, - email :: Maybe Email, + email :: Maybe EmailAddress, phone :: Maybe Phone, activated :: Activated, status :: Maybe AccountStatus, diff --git a/tools/db/team-info/.ormolu b/tools/db/team-info/.ormolu new file mode 120000 index 00000000000..ffc2ca9745e --- /dev/null +++ b/tools/db/team-info/.ormolu @@ -0,0 +1 @@ +../../../.ormolu \ No newline at end of file diff --git a/tools/db/team-info/README.md b/tools/db/team-info/README.md new file mode 100644 index 00000000000..81961bae4de --- /dev/null +++ b/tools/db/team-info/README.md @@ -0,0 +1,48 @@ +# Team info + +This program scans brig's and galley's cassandra for members of a team, their clients, and those clients' last access times. + +Useful for finding out which accounts you don't want to pay license fees any more. + +Example usage: + +```shell +team-info \ + --brig-cassandra-port 9048 --brig-cassandra-keyspace brig \ + --galley-cassandra-port 9049 --galley-cassandra-keyspace galley \ + --team-id=904912aa-7c10-11ef-9c85-8bfd758593f6 +``` + +Display usage: + +```shell +team-info -h +``` + +```text +team-info + +Usage: team-info [--brig-cassandra-host HOST] [--brig-cassandra-port PORT] + [--brig-cassandra-keyspace STRING] + [--galley-cassandra-host HOST] [--galley-cassandra-port PORT] + [--galley-cassandra-keyspace STRING] (-t|--team-id ID) + + get team info + +Available options: + -h,--help Show this help text + --brig-cassandra-host HOST + Cassandra Host for brig (default: "localhost") + --brig-cassandra-port PORT + Cassandra Port for brig (default: 9042) + --brig-cassandra-keyspace STRING + Cassandra Keyspace for brig (default: "brig_test") + --galley-cassandra-host HOST + Cassandra Host for galley (default: "localhost") + --galley-cassandra-port PORT + Cassandra Port for galley (default: 9043) + --galley-cassandra-keyspace STRING + Cassandra Keyspace for galley + (default: "galley_test") + -t,--team-id ID Team ID +``` diff --git a/libs/gundeck-types/src/Gundeck/Types/Push.hs b/tools/db/team-info/app/Main.hs similarity index 83% rename from libs/gundeck-types/src/Gundeck/Types/Push.hs rename to tools/db/team-info/app/Main.hs index fcd4d6af18c..46b640ea4b6 100644 --- a/libs/gundeck-types/src/Gundeck/Types/Push.hs +++ b/tools/db/team-info/app/Main.hs @@ -1,6 +1,6 @@ -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2022 Wire Swiss GmbH +-- Copyright (C) 2024 Wire Swiss GmbH -- -- This program is free software: you can redistribute it and/or modify it under -- the terms of the GNU Affero General Public License as published by the Free @@ -15,9 +15,9 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Gundeck.Types.Push - ( module V2, - ) -where +module Main where -import Gundeck.Types.Push.V2 as V2 +import qualified TeamInfo.Lib as Lib + +main :: IO () +main = Lib.main diff --git a/libs/gundeck-types/default.nix b/tools/db/team-info/default.nix similarity index 56% rename from libs/gundeck-types/default.nix rename to tools/db/team-info/default.nix index 522b4e84b10..d939d1c1fe4 100644 --- a/libs/gundeck-types/default.nix +++ b/tools/db/team-info/default.nix @@ -3,38 +3,38 @@ # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. { mkDerivation -, aeson -, attoparsec , base -, bytestring -, bytestring-conversion -, containers +, cassandra-util +, conduit +, cql , gitignoreSource , imports , lens , lib -, network-uri -, text +, optparse-applicative +, time +, tinylog , types-common -, wire-api }: mkDerivation { - pname = "gundeck-types"; - version = "1.45.0"; + pname = "team-info"; + version = "1.0.0"; src = gitignoreSource ./.; + isLibrary = true; + isExecutable = true; libraryHaskellDepends = [ - aeson - attoparsec - base - bytestring - bytestring-conversion - containers + cassandra-util + conduit + cql imports lens - network-uri - text + optparse-applicative + time + tinylog types-common - wire-api ]; + executableHaskellDepends = [ base ]; + description = "get team info from cassandra"; license = lib.licenses.agpl3Only; + mainProgram = "team-info"; } diff --git a/tools/db/team-info/src/TeamInfo/Lib.hs b/tools/db/team-info/src/TeamInfo/Lib.hs new file mode 100644 index 00000000000..f68fa9fa153 --- /dev/null +++ b/tools/db/team-info/src/TeamInfo/Lib.hs @@ -0,0 +1,93 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2024 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module TeamInfo.Lib where + +import Cassandra as C +import Cassandra.Settings as C +import Data.Conduit +import qualified Data.Conduit.Combinators as Conduit +import qualified Data.Conduit.List as CL +import Data.Id (TeamId, UserId) +import Data.Time +import qualified Database.CQL.Protocol as CQL +import Imports +import Options.Applicative +import qualified System.Logger as Log +import TeamInfo.Types + +lookupClientsLastActiveTimestamps :: ClientState -> UserId -> IO [Maybe UTCTime] +lookupClientsLastActiveTimestamps client u = do + runClient client $ runIdentity <$$> retry x1 (query selectClients (params One (Identity u))) + where + selectClients :: PrepQuery R (Identity UserId) (Identity (Maybe UTCTime)) + selectClients = "SELECT last_active from clients where user = ?" + +selectTeamMembers :: ClientState -> TeamId -> ConduitM () [TeamMemberRow] IO () +selectTeamMembers client teamId = + transPipe (runClient client) (paginateC cql (paramsP One (Identity teamId) 1000) x5) + .| Conduit.map (fmap CQL.asRecord) + where + cql :: C.PrepQuery C.R (Identity TeamId) (CQL.TupleType TeamMemberRow) + cql = + "SELECT user, legalhold_status FROM team_member WHERE team = ?" + +lookUpActivity :: ClientState -> TeamMemberRow -> IO TeamMember +lookUpActivity brigClient tmr = do + lastActiveTimestamps <- catMaybes <$> lookupClientsLastActiveTimestamps brigClient tmr.id + if null lastActiveTimestamps + then do + pure $ TeamMember tmr.id tmr.legalhold Nothing + else do + let lastActive = maximum lastActiveTimestamps + pure $ TeamMember tmr.id tmr.legalhold (Just lastActive) + +process :: TeamId -> ClientState -> ClientState -> IO [TeamMember] +process teamId brigClient galleyClient = + runConduit + $ selectTeamMembers galleyClient teamId + .| Conduit.concat + .| Conduit.mapM (lookUpActivity brigClient) + .| CL.consume + +main :: IO () +main = do + opts <- execParser (info (helper <*> optsParser) desc) + logger <- initLogger + brigClient <- initCas opts.brigDb logger + galleyClient <- initCas opts.galleyDb logger + teamMembers <- process opts.teamId brigClient galleyClient + for_ teamMembers $ \tm -> Log.info logger $ Log.msg (show tm) + where + initLogger = + Log.new + . Log.setLogLevel Log.Info + . Log.setOutput Log.StdOut + . Log.setFormat Nothing + . Log.setBufSize 0 + $ Log.defSettings + initCas settings l = + C.init + . C.setLogger (C.mkLogger l) + . C.setContacts settings.host [] + . C.setPortNumber (fromIntegral settings.port) + . C.setKeyspace settings.keyspace + . C.setProtocolVersion C.V4 + $ C.defSettings + desc = header "team-info" <> progDesc "get team info" <> fullDesc diff --git a/tools/db/team-info/src/TeamInfo/Types.hs b/tools/db/team-info/src/TeamInfo/Types.hs new file mode 100644 index 00000000000..e9112a7b1bc --- /dev/null +++ b/tools/db/team-info/src/TeamInfo/Types.hs @@ -0,0 +1,134 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2024 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module TeamInfo.Types where + +import Cassandra as C +import Control.Lens +import Data.Id +import Data.LegalHold (UserLegalHoldStatus) +import Data.Text.Strict.Lens +import Data.Time +import Database.CQL.Protocol hiding (Result) +import Imports +import Options.Applicative + +data CassandraSettings = CassandraSettings + { host :: String, + port :: Int, + keyspace :: C.Keyspace + } + +data Opts = Opts + { brigDb :: CassandraSettings, + galleyDb :: CassandraSettings, + teamId :: TeamId + } + +optsParser :: Parser Opts +optsParser = + Opts + <$> brigCassandraParser + <*> galleyCassandraParser + <*> ( option + auto + ( long "team-id" + <> short 't' + <> metavar "ID" + <> help "Team ID" + ) + ) + +galleyCassandraParser :: Parser CassandraSettings +galleyCassandraParser = + CassandraSettings + <$> strOption + ( long "galley-cassandra-host" + <> metavar "HOST" + <> help "Cassandra Host for galley" + <> value "localhost" + <> showDefault + ) + <*> option + auto + ( long "galley-cassandra-port" + <> metavar "PORT" + <> help "Cassandra Port for galley" + <> value 9043 + <> showDefault + ) + <*> ( C.Keyspace + . view packed + <$> strOption + ( long "galley-cassandra-keyspace" + <> metavar "STRING" + <> help "Cassandra Keyspace for galley" + <> value "galley_test" + <> showDefault + ) + ) + +brigCassandraParser :: Parser CassandraSettings +brigCassandraParser = + CassandraSettings + <$> strOption + ( long "brig-cassandra-host" + <> metavar "HOST" + <> help "Cassandra Host for brig" + <> value "localhost" + <> showDefault + ) + <*> option + auto + ( long "brig-cassandra-port" + <> metavar "PORT" + <> help "Cassandra Port for brig" + <> value 9042 + <> showDefault + ) + <*> ( C.Keyspace + . view packed + <$> strOption + ( long "brig-cassandra-keyspace" + <> metavar "STRING" + <> help "Cassandra Keyspace for brig" + <> value "brig_test" + <> showDefault + ) + ) + +data TeamMemberRow = TeamMemberRow + { id :: UserId, + legalhold :: Maybe UserLegalHoldStatus + } + deriving (Show, Generic) + +recordInstance ''TeamMemberRow + +data TeamMember = TeamMember + { id :: UserId, + legalhold :: Maybe UserLegalHoldStatus, + lastActive :: Maybe UTCTime + } + deriving (Generic) + +-- output as csv +instance Show TeamMember where + show tm = show tm.id <> "," <> maybe " " show tm.legalhold <> "," <> maybe " " show tm.lastActive diff --git a/tools/fedcalls/fedcalls.cabal b/tools/db/team-info/team-info.cabal similarity index 58% rename from tools/fedcalls/fedcalls.cabal rename to tools/db/team-info/team-info.cabal index aa2f03e2d4e..c96cb3485c1 100644 --- a/tools/fedcalls/fedcalls.cabal +++ b/tools/db/team-info/team-info.cabal @@ -1,19 +1,37 @@ -cabal-version: 1.12 -name: fedcalls +cabal-version: 3.0 +name: team-info version: 1.0.0 -synopsis: - Generate a dot file from swagger docs representing calls to federated instances. - +synopsis: get team info from cassandra category: Network author: Wire Swiss GmbH maintainer: Wire Swiss GmbH -copyright: (c) 2020 Wire Swiss GmbH -license: AGPL-3 +copyright: (c) 2024 Wire Swiss GmbH +license: AGPL-3.0-only build-type: Simple -executable fedcalls - main-is: Main.hs +library hs-source-dirs: src + exposed-modules: + TeamInfo.Lib + TeamInfo.Types + + ghc-options: + -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates + -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -funbox-strict-fields -threaded -with-rtsopts=-N + -Wredundant-constraints -Wunused-packages + + build-depends: + , cassandra-util + , conduit + , cql + , imports + , lens + , optparse-applicative + , time + , tinylog + , types-common + default-extensions: AllowAmbiguousTypes BangPatterns @@ -32,6 +50,7 @@ executable fedcalls FlexibleInstances FunctionalDependencies GADTs + GeneralizedNewtypeDeriving InstanceSigs KindSignatures LambdaCase @@ -39,6 +58,7 @@ executable fedcalls MultiWayIf NamedFieldPuns NoImplicitPrelude + OverloadedLabels OverloadedRecordDot OverloadedStrings PackageImports @@ -46,6 +66,7 @@ executable fedcalls PolyKinds QuasiQuotes RankNTypes + RecordWildCards ScopedTypeVariables StandaloneDeriving TupleSections @@ -56,20 +77,16 @@ executable fedcalls UndecidableInstances ViewPatterns +executable team-info + main-is: Main.hs + build-depends: + , base + , team-info + + hs-source-dirs: app + default-language: Haskell2010 ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -funbox-strict-fields -threaded "-with-rtsopts=-N -T" -rtsopts + -funbox-strict-fields -threaded -with-rtsopts=-N -Wredundant-constraints -Wunused-packages - - build-depends: - base - , containers - , imports - , language-dot - , lens - , mtl - , servant - , wire-api - - default-language: GHC2021 diff --git a/tools/fedcalls/.ormolu b/tools/fedcalls/.ormolu deleted file mode 120000 index 157b212d7cd..00000000000 --- a/tools/fedcalls/.ormolu +++ /dev/null @@ -1 +0,0 @@ -../../.ormolu \ No newline at end of file diff --git a/tools/fedcalls/README.md b/tools/fedcalls/README.md deleted file mode 100644 index bb62be4be4c..00000000000 --- a/tools/fedcalls/README.md +++ /dev/null @@ -1,38 +0,0 @@ -our swaggger docs contain information about which end-points call -which federation end-points internally. this command line tool -extracts that information from the swagger json and converts it into -two files: dot (for feeding into graphviz), and csv. - -### try it out - -``` -cabal run fedcalls -ls wire-fedcalls.* # these names are hard-coded (sorry!) -dot -Tpng wire-fedcalls.dot > wire-fedcalls.png -``` - -`dot` layouts only work for small data sets (at least without tweaking). for a better one paste into [sketchvis](https://sketchviz.com/new). - -### links - -for users: - -- blog post explaining the technology: https://reasonablypolymorphic.com/blog/abusing-constraints/index.html -- https://sketchviz.com/new -- https://graphviz.org/doc/info/lang.html - -for developers: - -- `./example.png` -- [MakesFederatedCall.hs (as of 2023-01-16)](https://github.com/wireapp/wire-server/blob/8760b4978ccb039b229d458b7a08136a05e12ff9/libs/wire-api/src/Wire/API/MakesFederatedCall.hs) -- PRs: https://github.com/wireapp/wire-server/pull/2973, https://github.com/wireapp/wire-server/pull/2940, https://github.com/wireapp/wire-server/pull/2950, https://github.com/wireapp/wire-server/pull/2957 - -### swagger-ui - -you can get the same data for the public API in the swagger-ui output. just load the page, open your javascript console, and type: - -``` -window.ui.getConfigs().showExtensions = true -``` - -then drop down on things like normal, and you'll see federated calls. diff --git a/tools/fedcalls/default.nix b/tools/fedcalls/default.nix deleted file mode 100644 index f1738ca4dfe..00000000000 --- a/tools/fedcalls/default.nix +++ /dev/null @@ -1,36 +0,0 @@ -# WARNING: GENERATED FILE, DO NOT EDIT. -# This file is generated by running hack/bin/generate-local-nix-packages.sh and -# must be regenerated whenever local packages are added or removed, or -# dependencies are added or removed. -{ mkDerivation -, base -, containers -, gitignoreSource -, imports -, language-dot -, lens -, lib -, mtl -, servant -, wire-api -}: -mkDerivation { - pname = "fedcalls"; - version = "1.0.0"; - src = gitignoreSource ./.; - isLibrary = false; - isExecutable = true; - executableHaskellDepends = [ - base - containers - imports - language-dot - lens - mtl - servant - wire-api - ]; - description = "Generate a dot file from swagger docs representing calls to federated instances"; - license = lib.licenses.agpl3Only; - mainProgram = "fedcalls"; -} diff --git a/tools/fedcalls/example.png b/tools/fedcalls/example.png deleted file mode 100644 index 26bc63134fc..00000000000 Binary files a/tools/fedcalls/example.png and /dev/null differ diff --git a/tools/fedcalls/src/Main.hs b/tools/fedcalls/src/Main.hs deleted file mode 100644 index ad14e495706..00000000000 --- a/tools/fedcalls/src/Main.hs +++ /dev/null @@ -1,195 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} - --- 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 Main - ( main, - ) -where - -import Control.Exception (assert) -import Control.Lens -import Control.Monad.State (evalState) -import Data.Data (Proxy (Proxy)) -import Data.Map qualified as M -import Imports -import Language.Dot as D -import Servant.API -import Wire.API.MakesFederatedCall (Calls (..), FedCallFrom' (..), HasFeds (..)) -import Wire.API.Routes.API -import Wire.API.Routes.Internal.Brig qualified as BrigIRoutes -import Wire.API.Routes.Public.Brig -import Wire.API.Routes.Public.Cannon -import Wire.API.Routes.Public.Cargohold -import Wire.API.Routes.Public.Galley -import Wire.API.Routes.Public.Gundeck -import Wire.API.Routes.Public.Proxy -import Wire.API.Routes.Public.Spar -import Wire.API.Routes.Version - ------------------------------- - -main :: IO () -main = do - writeFile "wire-fedcalls.dot" . D.renderDot . mkDotGraph $ calls - writeFile "wire-fedcalls.csv" . toCsv $ calls - -calls :: [MakesCallTo] -calls = assert (calls' == nub calls') calls' - where - calls' = parse $ Proxy @Swaggers - -type Swaggers = - -- TODO: introduce allSwaggerApis in wire-api that collects these for all - -- services, use that in /services/brig/src/Brig/API/Public.hs instead of - -- doing it by hand. - SpecialisedAPIRoutes 'V5 BrigAPITag - :<|> SpecialisedAPIRoutes 'V5 CannonAPITag - :<|> SpecialisedAPIRoutes 'V5 CargoholdAPITag - :<|> SpecialisedAPIRoutes 'V5 GalleyAPITag - :<|> SpecialisedAPIRoutes 'V5 GundeckAPITag - :<|> SpecialisedAPIRoutes 'V5 ProxyAPITag - :<|> SpecialisedAPIRoutes 'V5 SparAPITag - -- TODO: collect all internal apis somewhere else (brig?) - :<|> BrigIRoutes.API - --- :<|> CannonIRoutes.API --- :<|> CargoholdIRoutes.API --- :<|> LegalHoldIRoutes.API - ------------------------------- - -data MakesCallTo = MakesCallTo - { -- who is calling? - sourcePath :: String, - sourceMethod :: String, - -- where does the call go? - targetComp :: String, - targetName :: String - } - deriving (Eq, Show) - ------------------------------- - -fromFedCall :: FedCallFrom' Identity -> [MakesCallTo] -fromFedCall FedCallFrom {..} = do - (comp, names) <- M.assocs $ unCalls fedCalls - MakesCallTo - (runIdentity name) - (runIdentity method) - comp - <$> names - -filterCalls :: FedCallFrom' Maybe -> Maybe (FedCallFrom' Identity) -filterCalls fedCall = - FedCallFrom - <$> fmap pure (name fedCall) - <*> fmap pure (method fedCall) - <*> pure (fedCalls fedCall) - -parse :: (HasFeds api) => Proxy api -> [MakesCallTo] -parse p = do - fedCallM <- evalState (getFedCalls p) mempty - fedCallI <- maybeToList $ filterCalls fedCallM - fromFedCall fedCallI - ------------------------------- - --- | (this function can be simplified by tossing the serial numbers for nodes, but they might --- be useful for fine-tuning the output or rendering later.) --- --- the layout isn't very useful on realistic data sets. maybe we can tweak it with --- [layers](https://www.graphviz.org/docs/attr-types/layerRange/)? -mkDotGraph :: [MakesCallTo] -> D.Graph -mkDotGraph inbound = Graph StrictGraph DirectedGraph Nothing (mods <> nodes <> edges) - where - mods = - [ AttributeStatement GraphAttributeStatement [AttributeSetValue (NameId "rankdir") (NameId "LR")], - AttributeStatement NodeAttributeStatement [AttributeSetValue (NameId "shape") (NameId "rectangle")], - AttributeStatement EdgeAttributeStatement [AttributeSetValue (NameId "style") (NameId "dashed")] - ] - nodes = - [ SubgraphStatement (NewSubgraph Nothing (mkCallingNode <$> M.toList callingNodes)), - SubgraphStatement (NewSubgraph Nothing (mkCalledNode <$> M.toList calledNodes)) - ] - edges = mkEdge <$> inbound - - itemSourceNode :: MakesCallTo -> String - itemSourceNode (MakesCallTo path method _ _) = method <> " " <> path - - itemTargetNode :: MakesCallTo -> String - itemTargetNode (MakesCallTo _ _ comp rpcName) = "[" <> comp <> "]:" <> rpcName - - callingNodes :: Map String Integer - callingNodes = - foldl - (\mp (i, caller) -> M.insert caller i mp) - mempty - ((zip [0 ..] . nub $ itemSourceNode <$> inbound) :: [(Integer, String)]) - - calledNodes :: Map String Integer - calledNodes = - foldl - (\mp (i, called) -> M.insert called i mp) - mempty - ((zip [(fromIntegral $ M.size callingNodes) ..] . nub $ itemTargetNode <$> inbound) :: [(Integer, String)]) - - mkCallingNode :: (String, Integer) -> Statement - mkCallingNode n = - NodeStatement (mkCallingNodeId n) [] - - mkCallingNodeId :: (String, Integer) -> NodeId - mkCallingNodeId (caller, i) = - NodeId (NameId . show $ show i <> ": " <> caller) (Just (PortC CompassW)) - - mkCalledNode :: (String, Integer) -> Statement - mkCalledNode n = - NodeStatement (mkCalledNodeId n) [] - - mkCalledNodeId :: (String, Integer) -> NodeId - mkCalledNodeId (callee, i) = - NodeId (NameId . show $ show i <> ": " <> callee) (Just (PortC CompassE)) - - mkEdge :: MakesCallTo -> Statement - mkEdge item = - EdgeStatement - [ ENodeId NoEdge (mkCallingNodeId (caller, callerId)), - ENodeId DirectedEdge (mkCalledNodeId (callee, calleeId)) - ] - [] - where - caller = itemSourceNode item - callee = itemTargetNode item - callerId = fromMaybe (error "impossible") $ M.lookup caller callingNodes - calleeId = fromMaybe (error "impossible") $ M.lookup callee calledNodes - ------------------------------- - -toCsv :: [MakesCallTo] -> String -toCsv = - intercalate "\n" - . fmap (intercalate ",") - . addhdr - . fmap dolines - where - addhdr :: [[String]] -> [[String]] - addhdr = (["source method", "source path", "target component", "target name"] :) - - dolines :: MakesCallTo -> [String] - dolines (MakesCallTo spath smeth tcomp tname) = [smeth, spath, tcomp, tname] diff --git a/tools/hlint.sh b/tools/hlint.sh index e03ecb7161a..067a75e01ce 100755 --- a/tools/hlint.sh +++ b/tools/hlint.sh @@ -5,6 +5,8 @@ usage() { echo "Usage: $0 -f [all, changeset, pr] -m [check, inplace]" 1>&2; exi files='' +PR_BASE=${PR_BASE:-"origin/develop"} + while getopts ':f:m:k' opt do case $opt in @@ -12,7 +14,7 @@ while getopts ':f:m:k' opt if [ "$f" = "all" ]; then files=$(git ls-files | grep \.hs\$) elif [ "$f" = "pr" ]; then - files=$(git diff --name-only origin/develop... | grep \.hs\$) + files=$(git diff --name-only "$PR_BASE"... | grep \.hs\$) elif [ "$f" = "changeset" ]; then files=$(git diff --name-only HEAD | grep \.hs\$) else diff --git a/tools/ormolu.sh b/tools/ormolu.sh index 901baa11e0d..de3e4fc73b6 100755 --- a/tools/ormolu.sh +++ b/tools/ormolu.sh @@ -6,6 +6,7 @@ cd "$( dirname "${BASH_SOURCE[0]}" )/.." ALLOW_DIRTY_WC="0" ARG_ORMOLU_MODE="inplace" +PR_BASE=${PR_BASE:-"origin/develop"} USAGE=" This bash script can either @@ -73,7 +74,7 @@ fi if [ "$f" = "all" ] || [ "$f" = "" ]; then files=$(git ls-files | grep '\.hsc\?$') elif [ "$f" = "pr" ]; then - files=$(git diff --diff-filter=ACMR --name-only origin/develop... | { grep '\.hsc\?$' || true; }; git diff --diff-filter=ACMR --name-only HEAD | { grep \.hs\$ || true ; }) + files=$(git diff --diff-filter=ACMR --name-only "$PR_BASE"... | { grep '\.hsc\?$' || true; }; git diff --diff-filter=ACMR --name-only HEAD | { grep \.hs\$ || true ; }) fi count=$( echo "$files" | sed '/^\s*$/d' | wc -l ) diff --git a/tools/rabbitmq-consumer/src/RabbitMQConsumer/Lib.hs b/tools/rabbitmq-consumer/src/RabbitMQConsumer/Lib.hs index c5fb64196c8..e779f01a8e8 100644 --- a/tools/rabbitmq-consumer/src/RabbitMQConsumer/Lib.hs +++ b/tools/rabbitmq-consumer/src/RabbitMQConsumer/Lib.hs @@ -29,8 +29,8 @@ import Imports import Network.AMQP import Network.Socket import Options.Applicative +import Wire.API.Component (Component) import Wire.API.Federation.BackendNotifications (BackendNotification (..)) -import Wire.API.MakesFederatedCall (Component) main :: IO () main = do diff --git a/tools/stern/default.nix b/tools/stern/default.nix index 8ccf0f63f20..18246b4fc52 100644 --- a/tools/stern/default.nix +++ b/tools/stern/default.nix @@ -11,6 +11,7 @@ , bytestring-conversion , containers , cookie +, data-default , errors , exceptions , extended @@ -87,7 +88,6 @@ mkDerivation { types-common unliftio utf8-string - uuid wai wai-utilities wire-api @@ -101,6 +101,7 @@ mkDerivation { bytestring-conversion containers cookie + data-default exceptions extra HsOpenSSL diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index 8b071b59b45..53616908c64 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -29,7 +29,7 @@ where import Brig.Types.Intra import Control.Error -import Control.Lens ((.~), (^.)) +import Control.Lens ((.~)) import Control.Monad.Except import Data.Aeson hiding (Error, json) import Data.Aeson.KeyMap qualified as KeyMap @@ -62,7 +62,7 @@ import Stern.App import Stern.Intra qualified as Intra import Stern.Options import Stern.Types -import System.Logger.Class hiding (Error, name, trace, (.=)) +import System.Logger.Class hiding (Error, flush, name, trace, (.=)) import Util.Options import Wire.API.Connection import Wire.API.Internal.Notification (QueuedNotification) @@ -70,7 +70,7 @@ import Wire.API.Routes.Internal.Brig.Connection (ConnectionStatus) import Wire.API.Routes.Internal.Brig.EJPD qualified as EJPD import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Routes.Named (Named (Named)) -import Wire.API.Team.Feature hiding (setStatus) +import Wire.API.Team.Feature import Wire.API.Team.SearchVisibility import Wire.API.User import Wire.API.User.Search @@ -81,15 +81,15 @@ start :: Opts -> IO () start o = do e <- newEnv o s <- Server.newSettings (server e) - Server.runSettingsWithShutdown s (requestIdMiddleware (e ^. applog) defaultRequestIdHeaderName $ servantApp e) Nothing + Server.runSettingsWithShutdown s (requestIdMiddleware e.appLogger defaultRequestIdHeaderName $ servantApp e) Nothing where server :: Env -> Server.Server - server e = Server.defaultServer (unpack $ stern o ^. host) (stern o ^. port) (e ^. applog) + server e = Server.defaultServer (unpack o.stern.host) o.stern.port e.appLogger servantApp :: Env -> Application servantApp e0 req cont = do let rid = getRequestId defaultRequestIdHeaderName req - let e = requestId .~ rid $ e0 + let e = requestIdLens .~ rid $ e0 Servant.serve ( Proxy @( SwaggerDocsAPI @@ -184,7 +184,7 @@ sitemap' = :<|> Named @"put-sso-domain-redirect" Intra.putSsoDomainRedirect :<|> Named @"delete-sso-domain-redirect" Intra.deleteSsoDomainRedirect :<|> Named @"register-oauth-client" Intra.registerOAuthClient - :<|> Named @"get-oauth-client" Intra.getOAuthClient + :<|> Named @"stern-get-oauth-client" Intra.getOAuthClient :<|> Named @"update-oauth-client" Intra.updateOAuthClient :<|> Named @"delete-oauth-client" Intra.deleteOAuthClient @@ -207,13 +207,13 @@ suspendUser uid = NoContent <$ Intra.putUserStatus Suspended uid unsuspendUser :: UserId -> Handler NoContent unsuspendUser uid = NoContent <$ Intra.putUserStatus Active uid -usersByEmail :: Email -> Handler [UserAccount] +usersByEmail :: EmailAddress -> Handler [User] usersByEmail = Intra.getUserProfilesByIdentity -usersByIds :: [UserId] -> Handler [UserAccount] +usersByIds :: [UserId] -> Handler [User] usersByIds = Intra.getUserProfiles . Left -usersByHandles :: [Handle] -> Handler [UserAccount] +usersByHandles :: [Handle] -> Handler [User] usersByHandles = Intra.getUserProfiles . Right ejpdInfoByHandles :: Maybe Bool -> [Handle] -> Handler EJPD.EJPDResponseBody @@ -232,17 +232,17 @@ searchOnBehalf (fromMaybe (unsafeRange 10) . checked @1 @100 @Int32 . fromMaybe 10 -> s) = Intra.getContacts uid q (fromRange s) -revokeIdentity :: Email -> Handler NoContent +revokeIdentity :: EmailAddress -> Handler NoContent revokeIdentity e = NoContent <$ Intra.revokeIdentity e changeEmail :: UserId -> EmailUpdate -> Handler NoContent changeEmail uid upd = NoContent <$ Intra.changeEmail uid upd -deleteUser :: UserId -> Email -> Handler NoContent +deleteUser :: UserId -> EmailAddress -> Handler NoContent deleteUser uid email = do usrs <- Intra.getUserProfilesByIdentity email case usrs of - [accountUser -> u] -> + [u] -> if userId u == uid then do info $ userMsg uid . msg (val "Deleting account") @@ -255,10 +255,10 @@ deleteUser uid email = do setTeamStatusH :: Team.TeamStatus -> TeamId -> Handler NoContent setTeamStatusH status tid = NoContent <$ Intra.setStatusBindingTeam tid status -deleteTeam :: TeamId -> Maybe Bool -> Maybe Email -> Handler NoContent +deleteTeam :: TeamId -> Maybe Bool -> Maybe EmailAddress -> Handler NoContent deleteTeam givenTid (fromMaybe False -> False) (Just email) = do acc <- Intra.getUserProfilesByIdentity email >>= handleNoUser . listToMaybe - userTid <- (Intra.getUserBindingTeam . userId . accountUser $ acc) >>= handleNoTeam + userTid <- (Intra.getUserBindingTeam . userId $ acc) >>= handleNoTeam when (givenTid /= userTid) $ throwE bindingTeamMismatch tInfo <- Intra.getTeamInfo givenTid @@ -276,25 +276,25 @@ deleteTeam tid (fromMaybe False -> True) _ = do deleteTeam _ _ _ = throwE $ mkError status400 "Bad Request" "either email or 'force=true' parameter is required" -isUserKeyBlacklisted :: Email -> Handler NoContent +isUserKeyBlacklisted :: EmailAddress -> Handler NoContent isUserKeyBlacklisted email = do bl <- Intra.isBlacklisted email if bl then throwE $ mkError status200 "blacklisted" "The given user key IS blacklisted" else throwE $ mkError status404 "not-blacklisted" "The given user key is NOT blacklisted" -addBlacklist :: Email -> Handler NoContent +addBlacklist :: EmailAddress -> Handler NoContent addBlacklist email = do NoContent <$ Intra.setBlacklistStatus True email -deleteFromBlacklist :: Email -> Handler NoContent +deleteFromBlacklist :: EmailAddress -> Handler NoContent deleteFromBlacklist email = do NoContent <$ Intra.setBlacklistStatus False email -getTeamInfoByMemberEmail :: Email -> Handler TeamInfo +getTeamInfoByMemberEmail :: EmailAddress -> Handler TeamInfo getTeamInfoByMemberEmail e = do acc <- Intra.getUserProfilesByIdentity e >>= handleUser . listToMaybe - tid <- (Intra.getUserBindingTeam . userId . accountUser $ acc) >>= handleTeam + tid <- (Intra.getUserBindingTeam . userId $ acc) >>= handleTeam Intra.getTeamInfo tid where handleUser = ifNothing (mkError status404 "no-user" "No such user with that email") @@ -308,36 +308,25 @@ getTeamAdminInfo = fmap toAdminInfo . Intra.getTeamInfo mkFeatureGetRoute :: forall cfg. - ( IsFeatureConfig cfg, - ToSchema cfg, - KnownSymbol (FeatureSymbol cfg), - Typeable cfg - ) => + (IsFeatureConfig cfg, Typeable cfg) => TeamId -> - Handler (WithStatus cfg) + Handler (LockableFeature cfg) mkFeatureGetRoute = Intra.getTeamFeatureFlag @cfg mkFeaturePutRoute :: forall cfg. - ( KnownSymbol (FeatureSymbol cfg), - ToJSON (WithStatusNoLock cfg) - ) => + (IsFeatureConfig cfg) => TeamId -> - WithStatusNoLock cfg -> + Feature cfg -> Handler NoContent mkFeaturePutRoute tid payload = NoContent <$ Intra.setTeamFeatureFlag @cfg tid payload type MkFeaturePutConstraints cfg = - ( MkFeaturePutLockConstraints cfg, - FeatureTrivialConfig cfg - ) - -type MkFeaturePutLockConstraints cfg = ( IsFeatureConfig cfg, KnownSymbol (FeatureSymbol cfg), ToSchema cfg, - FromJSON (WithStatusNoLock cfg), - ToJSON (WithStatusNoLock cfg), + FromJSON (Feature cfg), + ToJSON (Feature cfg), Typeable cfg ) @@ -346,7 +335,7 @@ mkFeaturePutRouteTrivialConfigNoTTL :: mkFeaturePutRouteTrivialConfigNoTTL tid status = mkFeaturePutRouteTrivialConfig @cfg tid status Nothing mkFeatureLockUnlockRouteTrivialConfigNoTTL :: - forall cfg. (MkFeaturePutLockConstraints cfg) => TeamId -> LockStatus -> Handler NoContent + forall cfg. (MkFeaturePutConstraints cfg) => TeamId -> LockStatus -> Handler NoContent mkFeatureLockUnlockRouteTrivialConfigNoTTL tid lstat = NoContent <$ Intra.setTeamFeatureLockStatus @cfg tid lstat mkFeaturePutRouteTrivialConfigWithTTL :: @@ -355,9 +344,9 @@ mkFeaturePutRouteTrivialConfigWithTTL tid status = mkFeaturePutRouteTrivialConfi mkFeaturePutRouteTrivialConfig :: forall cfg. (MkFeaturePutConstraints cfg) => TeamId -> FeatureStatus -> Maybe FeatureTTLDays -> Handler NoContent -mkFeaturePutRouteTrivialConfig tid status (maybe FeatureTTLUnlimited convertFeatureTTLDaysToSeconds -> ttl) = do - let fullStatus = WithStatusNoLock status trivialConfig ttl - NoContent <$ Intra.setTeamFeatureFlag @cfg tid fullStatus +mkFeaturePutRouteTrivialConfig tid status _ = do + let patch = LockableFeaturePatch (Just status) Nothing Nothing + NoContent <$ Intra.patchTeamFeatureFlag @cfg tid patch getSearchVisibility :: TeamId -> Handler TeamSearchVisibilityView getSearchVisibility = Intra.getSearchVisibility @@ -402,7 +391,7 @@ setTeamBillingInfo tid billingInfo = do Intra.setTeamBillingInfo tid billingInfo getTeamBillingInfo tid -getConsentLog :: Email -> Handler ConsentLogAndMarketo +getConsentLog :: EmailAddress -> Handler ConsentLogAndMarketo getConsentLog e = do acc <- listToMaybe <$> Intra.getUserProfilesByIdentity e when (isJust acc) $ @@ -438,7 +427,7 @@ getUserData uid mMaxConvs mMaxNotifs = do consentLog <- (Intra.getUserConsentLog uid <&> toJSON @ConsentLog) `catchE` (pure . String . T.pack . show) - let em = userEmail $ accountUser account + let em = userEmail account marketo <- do let noEmail = MarketoResult $ KeyMap.singleton "results" emptyArray maybe diff --git a/tools/stern/src/Stern/API/Routes.hs b/tools/stern/src/Stern/API/Routes.hs index b52d262f142..777bd118c5d 100644 --- a/tools/stern/src/Stern/API/Routes.hs +++ b/tools/stern/src/Stern/API/Routes.hs @@ -21,13 +21,11 @@ module Stern.API.Routes SwaggerDocsAPI, swaggerDocs, UserConnectionGroups (..), - doubleMaybeToEither, RedirectToSwaggerDocsAPI, ) where import Control.Lens -import Control.Monad.Trans.Except import Data.Aeson qualified as A import Data.Handle import Data.Id @@ -35,8 +33,6 @@ import Data.Kind import Data.OpenApi qualified as S import Data.Schema qualified as Schema import Imports hiding (head) -import Network.HTTP.Types.Status -import Network.Wai.Utilities import Servant hiding (Handler, WithStatus (..), addHeader, respond) import Servant.OpenApi (HasOpenApi (toOpenApi)) import Servant.OpenApi.Internal.Orphans () @@ -88,8 +84,8 @@ type SternAPI = ( Summary "Displays user's info given an email address" :> "users" :> "by-email" - :> QueryParam' [Required, Strict, Description "Email address"] "email" Email - :> Get '[JSON] [UserAccount] + :> QueryParam' [Required, Strict, Description "Email address"] "email" EmailAddress + :> Get '[JSON] [User] ) :<|> Named "get-users-by-ids" @@ -97,7 +93,7 @@ type SternAPI = :> "users" :> "by-ids" :> QueryParam' [Required, Strict, Description "List of IDs of the users, separated by comma"] "ids" [UserId] - :> Get '[JSON] [UserAccount] + :> Get '[JSON] [User] ) :<|> Named "get-users-by-handles" @@ -105,7 +101,7 @@ type SternAPI = :> "users" :> "by-handles" :> QueryParam' [Required, Strict, Description "List of Handles of the users, without '@', separated by comma"] "handles" [Handle] - :> Get '[JSON] [UserAccount] + :> Get '[JSON] [User] ) :<|> Named "get-user-connections" @@ -145,7 +141,7 @@ type SternAPI = \If the given identity is not taken / verified, this is a no-op." :> "users" :> "revoke-identity" - :> QueryParam' [Required, Strict, Description "A verified email address"] "email" Email + :> QueryParam' [Required, Strict, Description "A verified email address"] "email" EmailAddress :> Post '[JSON] NoContent ) :<|> Named @@ -165,7 +161,7 @@ type SternAPI = "Email must match UserId's (to prevent copy/paste mistakes)." :> "users" :> Capture "uid" UserId - :> QueryParam' [Required, Strict, Description "A verified email address"] "email" Email + :> QueryParam' [Required, Strict, Description "A verified email address"] "email" EmailAddress :> Delete '[JSON] NoContent ) :<|> Named @@ -195,7 +191,7 @@ type SternAPI = :> "teams" :> Capture "tid" TeamId :> QueryParam' [Optional, Strict, Description "THIS WILL PERMANENTLY DELETE ALL TEAM MEMBERS! CHECK TEAM MEMBER LIST (SEE ABOVE OR BELOW) IF YOU ARE UNCERTAIN THAT'S WHAT YOU WANT."] "force" Bool - :> QueryParam' [Optional, Strict, Description "Matching verified remaining user address"] "email" Email + :> QueryParam' [Optional, Strict, Description "Matching verified remaining user address"] "email" EmailAddress :> Delete '[JSON] NoContent ) :<|> Named @@ -211,7 +207,7 @@ type SternAPI = ( Summary "Fetch blacklist information on a email (200: blacklisted; 404: not blacklisted)" :> "users" :> "blacklist" - :> QueryParam' [Required, Strict, Description "A verified email address"] "email" Email + :> QueryParam' [Required, Strict, Description "A verified email address"] "email" EmailAddress :> Verb 'GET 200 '[JSON] NoContent ) :<|> Named @@ -219,7 +215,7 @@ type SternAPI = ( Summary "Add the email to our blacklist" :> "users" :> "blacklist" - :> QueryParam' [Required, Strict, Description "A verified email address"] "email" Email + :> QueryParam' [Required, Strict, Description "A verified email address"] "email" EmailAddress :> Post '[JSON] NoContent ) :<|> Named @@ -227,14 +223,14 @@ type SternAPI = ( Summary "Remove the email from our blacklist" :> "users" :> "blacklist" - :> QueryParam' [Required, Strict, Description "A verified email address"] "email" Email + :> QueryParam' [Required, Strict, Description "A verified email address"] "email" EmailAddress :> Delete '[JSON] NoContent ) :<|> Named "get-team-info-by-member-email" ( Summary "Fetch a team information given a member's email" :> "teams" - :> QueryParam' [Required, Strict, Description "A verified email address"] "email" Email + :> QueryParam' [Required, Strict, Description "A verified email address"] "email" EmailAddress :> Get '[JSON] TeamInfo ) :<|> Named @@ -253,20 +249,20 @@ type SternAPI = :> Get '[JSON] TeamAdminInfo ) :<|> Named "get-route-legalhold-config" (MkFeatureGetRoute LegalholdConfig) - :<|> Named "put-route-legalhold-config" (MkFeaturePutRouteTrivialConfigNoTTL LegalholdConfig) + :<|> Named "put-route-legalhold-config" (MkFeaturePutRouteNoTTL LegalholdConfig) :<|> Named "get-route-sso-config" (MkFeatureGetRoute SSOConfig) - :<|> Named "put-route-sso-config" (MkFeaturePutRouteTrivialConfigNoTTL SSOConfig) + :<|> Named "put-route-sso-config" (MkFeaturePutRouteNoTTL SSOConfig) :<|> Named "get-route-search-visibility-available-config" (MkFeatureGetRoute SearchVisibilityAvailableConfig) - :<|> Named "put-route-search-visibility-available-config" (MkFeaturePutRouteTrivialConfigNoTTL SearchVisibilityAvailableConfig) + :<|> Named "put-route-search-visibility-available-config" (MkFeaturePutRouteNoTTL SearchVisibilityAvailableConfig) :<|> Named "get-route-validate-saml-emails-config" (MkFeatureGetRoute ValidateSAMLEmailsConfig) - :<|> Named "put-route-validate-saml-emails-config" (MkFeaturePutRouteTrivialConfigNoTTL ValidateSAMLEmailsConfig) + :<|> Named "put-route-validate-saml-emails-config" (MkFeaturePutRouteNoTTL ValidateSAMLEmailsConfig) :<|> Named "get-route-digital-signatures-config" (MkFeatureGetRoute DigitalSignaturesConfig) - :<|> Named "put-route-digital-signatures-config" (MkFeaturePutRouteTrivialConfigNoTTL DigitalSignaturesConfig) + :<|> Named "put-route-digital-signatures-config" (MkFeaturePutRouteNoTTL DigitalSignaturesConfig) :<|> Named "get-route-file-sharing-config" (MkFeatureGetRoute FileSharingConfig) - :<|> Named "put-route-file-sharing-config" (MkFeaturePutRouteTrivialConfigNoTTL FileSharingConfig) + :<|> Named "put-route-file-sharing-config" (MkFeaturePutRouteNoTTL FileSharingConfig) :<|> Named "get-route-classified-domains-config" (MkFeatureGetRoute ClassifiedDomainsConfig) :<|> Named "get-route-conference-calling-config" (MkFeatureGetRoute ConferenceCallingConfig) - :<|> Named "put-route-conference-calling-config" (MkFeaturePutRouteTrivialConfigWithTTL ConferenceCallingConfig) + :<|> Named "put-route-conference-calling-config" (MkFeaturePutRouteWithTTL ConferenceCallingConfig) :<|> Named "get-route-applock-config" (MkFeatureGetRoute AppLockConfig) :<|> Named "put-route-applock-config" (MkFeaturePutRoute AppLockConfig) :<|> Named "get-route-mls-config" (MkFeatureGetRoute MLSConfig) @@ -297,8 +293,8 @@ type SternAPI = :> Put '[JSON] NoContent ) :<|> Named "get-route-outlook-cal-config" (MkFeatureGetRoute OutlookCalIntegrationConfig) - :<|> Named "lock-unlock-route-outlook-cal-config" (MkFeatureLockUnlockRouteTrivialConfigNoTTL OutlookCalIntegrationConfig) - :<|> Named "put-route-outlook-cal-config" (MkFeaturePutRouteTrivialConfigNoTTL OutlookCalIntegrationConfig) + :<|> Named "lock-unlock-route-outlook-cal-config" (MkFeatureLockUnlockRouteNoTTL OutlookCalIntegrationConfig) + :<|> Named "put-route-outlook-cal-config" (MkFeaturePutRouteNoTTL OutlookCalIntegrationConfig) :<|> Named "get-route-enforce-file-download-location" ( Description @@ -309,7 +305,7 @@ type SternAPI = "lock-unlock-route-enforce-file-download-location" ( Description "

Custom feature: only supported for some decidated on-prem systems.

" - :> MkFeatureLockUnlockRouteTrivialConfigNoTTL EnforceFileDownloadLocationConfig + :> MkFeatureLockUnlockRouteNoTTL EnforceFileDownloadLocationConfig ) :<|> Named "put-route-enforce-file-download-location" @@ -364,7 +360,7 @@ type SternAPI = :> Description "Relevant only internally at Wire" :> "i" :> "consent" - :> QueryParam' [Required, Strict, Description "A verified email address"] "email" Email + :> QueryParam' [Required, Strict, Description "A verified email address"] "email" EmailAddress :> Get '[JSON] ConsentLogAndMarketo ) :<|> Named @@ -415,7 +411,7 @@ type SternAPI = :> Post '[JSON] OAuthClientCredentials ) :<|> Named - "get-oauth-client" + "stern-get-oauth-client" ( Summary "Get OAuth client by id" :> "i" :> "oauth" @@ -482,20 +478,15 @@ instance Schema.ToSchema UserConnectionGroups where <*> ucgMissingLegalholdConsent Schema..= Schema.field "ucgMissingLegalholdConsent" Schema.schema <*> ucgTotal Schema..= Schema.field "ucgTotal" Schema.schema -doubleMaybeToEither :: (Monad m) => LText -> Maybe a -> Maybe b -> ExceptT Error m (Either a b) -doubleMaybeToEither _ (Just a) Nothing = pure $ Left a -doubleMaybeToEither _ Nothing (Just b) = pure $ Right b -doubleMaybeToEither msg _ _ = throwE $ mkError status400 "either-params" ("Must use exactly one of two query params: " <> msg) - type MkFeatureGetRoute (feature :: Type) = Summary "Shows whether a feature flag is enabled or not for a given team." :> "teams" :> Capture "tid" TeamId :> "features" :> FeatureSymbol feature - :> Get '[JSON] (WithStatus feature) + :> Get '[JSON] (LockableFeature feature) -type MkFeaturePutRouteTrivialConfigNoTTL (feature :: Type) = +type MkFeaturePutRouteNoTTL (feature :: Type) = Summary "Disable / enable status for a given feature / team" :> "teams" :> Capture "tid" TeamId @@ -504,7 +495,7 @@ type MkFeaturePutRouteTrivialConfigNoTTL (feature :: Type) = :> QueryParam' [Required, Strict] "status" FeatureStatus :> Put '[JSON] NoContent -type MkFeaturePutRouteTrivialConfigWithTTL (feature :: Type) = +type MkFeaturePutRouteWithTTL (feature :: Type) = Summary "Disable / enable status for a given feature / team" :> Description "team feature time to live, given in days, or 'unlimited' (default). only available on *some* features!" :> "teams" @@ -515,7 +506,7 @@ type MkFeaturePutRouteTrivialConfigWithTTL (feature :: Type) = :> QueryParam' [Required, Strict, Description "team feature time to live, given in days, or 'unlimited' (default)."] "ttl" FeatureTTLDays :> Put '[JSON] NoContent -type MkFeatureLockUnlockRouteTrivialConfigNoTTL (feature :: Type) = +type MkFeatureLockUnlockRouteNoTTL (feature :: Type) = Summary "Lock / unlock status for a given feature / team (en-/disable should happen in team settings)" :> "teams" :> Capture "tid" TeamId @@ -531,5 +522,5 @@ type MkFeaturePutRoute (feature :: Type) = :> Capture "tid" TeamId :> "features" :> FeatureSymbol feature - :> ReqBody '[JSON] (WithStatusNoLock feature) + :> ReqBody '[JSON] (Feature feature) :> Put '[JSON] NoContent diff --git a/tools/stern/src/Stern/App.hs b/tools/stern/src/Stern/App.hs index 3a75f308748..7f9ba34850b 100644 --- a/tools/stern/src/Stern/App.hs +++ b/tools/stern/src/Stern/App.hs @@ -26,7 +26,7 @@ module Stern.App where import Bilge qualified import Bilge.RPC (HasRequestId (..)) import Control.Error -import Control.Lens (makeLenses, set, view, (^.)) +import Control.Lens (lensField, lensRules, makeLensesWith, (.~)) import Control.Monad.Catch (MonadCatch, MonadThrow) import Control.Monad.IO.Class import Control.Monad.Reader.Class @@ -34,43 +34,45 @@ import Control.Monad.Trans.Class import Data.ByteString.Conversion (toByteString') import Data.Id import Data.Text.Encoding (encodeUtf8) -import Data.UUID (toString) -import Data.UUID.V4 qualified as UUID import Imports import Network.HTTP.Client (responseTimeoutMicro) -import Network.Wai (Request, Response, ResponseReceived) -import Network.Wai.Utilities (Error (..), lookupRequestId) -import Network.Wai.Utilities.Error qualified as WaiError -import Network.Wai.Utilities.Response (json, setStatus) -import Network.Wai.Utilities.Server (defaultRequestIdHeaderName) -import Network.Wai.Utilities.Server qualified as Server -import Stern.Options as O +import Network.Wai (Response, ResponseReceived) +import Network.Wai.Utilities (Error (..)) +import Stern.Options as Opts import System.Logger qualified as Log import System.Logger.Class hiding (Error, info) import System.Logger.Class qualified as LC import System.Logger.Extended qualified as Log import Util.Options +import Util.SuffixNamer data Env = Env - { _brig :: !Bilge.Request, - _galley :: !Bilge.Request, - _gundeck :: !Bilge.Request, - _ibis :: !Bilge.Request, - _galeb :: !Bilge.Request, - _applog :: !Logger, - _requestId :: !Bilge.RequestId, - _httpManager :: !Bilge.Manager + { brig :: !Bilge.Request, + galley :: !Bilge.Request, + gundeck :: !Bilge.Request, + ibis :: !Bilge.Request, + galeb :: !Bilge.Request, + appLogger :: !Logger, + requestId :: !Bilge.RequestId, + httpManager :: !Bilge.Manager } -makeLenses ''Env +makeLensesWith (lensRules & lensField .~ suffixNamer) ''Env newEnv :: Opts -> IO Env -newEnv o = do - l <- Log.mkLogger (O.logLevel o) (O.logNetStrings o) (O.logFormat o) - Env (mkRequest $ O.brig o) (mkRequest $ O.galley o) (mkRequest $ O.gundeck o) (mkRequest $ O.ibis o) (mkRequest $ O.galeb o) l (RequestId "N/A") +newEnv opts = do + l <- Log.mkLogger opts.logLevel opts.logNetStrings opts.logFormat + Env + (mkRequest opts.brig) + (mkRequest opts.galley) + (mkRequest opts.gundeck) + (mkRequest opts.ibis) + (mkRequest opts.galeb) + l + (RequestId defRequestId) <$> newManager where - mkRequest s = Bilge.host (encodeUtf8 (s ^. host)) . Bilge.port (s ^. port) $ Bilge.empty + mkRequest s = Bilge.host (encodeUtf8 s.host) . Bilge.port s.port $ Bilge.empty newManager = Bilge.newManager (Bilge.defaultManagerSettings {Bilge.managerResponseTimeout = responseTimeoutMicro 10000000}) -- Monads @@ -91,8 +93,8 @@ type App = AppT IO instance (MonadIO m) => MonadLogger (AppT m) where log l m = do - g <- view applog - r <- view requestId + g <- asks (.appLogger) + r <- asks (.requestId) Log.log g l $ "request" .= Bilge.unRequestId r ~~ m instance MonadLogger (ExceptT e App) where @@ -100,18 +102,18 @@ instance MonadLogger (ExceptT e App) where instance (MonadIO m) => Bilge.MonadHttp (AppT m) where handleRequestWithCont req h = do - m <- view httpManager + m <- asks (.httpManager) liftIO $ Bilge.withResponse req m h instance (Monad m) => HasRequestId (AppT m) where - getRequestId = view requestId + getRequestId = asks (.requestId) instance HasRequestId (ExceptT e App) where - getRequestId = view requestId + getRequestId = asks (.requestId) instance Bilge.MonadHttp (ExceptT e App) where handleRequestWithCont req h = do - m <- view httpManager + m <- asks (.httpManager) liftIO $ Bilge.withResponse req m h runAppT :: Env -> AppT m a -> m a @@ -122,25 +124,10 @@ runAppT e (AppT ma) = runReaderT ma e type Handler = ExceptT Error App -type Continue m = Response -> m ResponseReceived +runHandler :: Env -> Handler a -> IO (Either Error a) +runHandler env = runAppT env . runExceptT -runHandler :: Env -> Request -> Handler ResponseReceived -> Continue IO -> IO ResponseReceived -runHandler e r h k = do - i <- reqId (lookupRequestId defaultRequestIdHeaderName r) - let e' = set requestId (Bilge.RequestId i) e - a <- runAppT e' (runExceptT h) - either (onError (view applog e) r k) pure a - where - reqId (Just i) = pure i - reqId Nothing = do - uuid <- UUID.nextRandom - pure $ toByteString' $ "stern-" ++ toString uuid - -onError :: Logger -> Request -> Continue IO -> Error -> IO ResponseReceived -onError g r k e = do - Server.logError g (Just r) e - Server.flushRequestBody r - k (setStatus (WaiError.code e) (json e)) +type Continue m = Response -> m ResponseReceived userMsg :: UserId -> Msg -> Msg userMsg = field "user" . toByteString' diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index 59636d7e5ba..a5e17507ff2 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -23,7 +23,6 @@ module Stern.Intra ( backendApiVersion, - putUser, putUserStatus, getContacts, getUserConnections, @@ -35,17 +34,18 @@ module Stern.Intra getInvoiceUrl, revokeIdentity, changeEmail, - changePhone, deleteAccount, setStatusBindingTeam, deleteBindingTeam, deleteBindingTeamForce, + getTeamMembers, getTeamInfo, getUserBindingTeam, isBlacklisted, setBlacklistStatus, getTeamFeatureFlag, setTeamFeatureFlag, + patchTeamFeatureFlag, setTeamFeatureLockStatus, getTeamData, getSearchVisibility, @@ -70,7 +70,7 @@ module Stern.Intra ) where -import Bilge hiding (head, options, path, paths, requestId) +import Bilge hiding (head, options, patch, path, paths, requestId) import Bilge qualified import Bilge.RPC import Brig.Types.Intra @@ -87,14 +87,12 @@ import Data.Id import Data.Int import Data.List.Split (chunksOf) import Data.Map qualified as Map -import Data.Proxy (Proxy (Proxy)) import Data.Qualified (qUnqualified) import Data.Text (strip) import Data.Text.Encoding import Data.Text.Encoding.Error import Data.Text.Lazy as LT (pack) import Data.Text.Lazy.Encoding qualified as TL -import GHC.TypeLits (KnownSymbol, symbolVal) import Imports import Network.HTTP.Types (urlEncode) import Network.HTTP.Types.Method @@ -141,28 +139,10 @@ versionedPaths = Bilge.paths . (encodeUtf8 (toUrlPiece backendApiVersion) :) ------------------------------------------------------------------------------- -putUser :: UserId -> UserUpdate -> Handler () -putUser uid upd = do - info $ userMsg uid . msg "Changing user state" - b <- view brig - void $ - catchRpcErrors $ - rpc' - "brig" - b - ( method PUT - . versionedPath "self" - . header "Z-User" (toByteString' uid) - . header "Z-Connection" (toByteString' "") - . lbytes (encode upd) - . contentJson - . expect2xx - ) - putUserStatus :: AccountStatus -> UserId -> Handler () putUserStatus status uid = do info $ userMsg uid . msg "Changing user status" - b <- view brig + b <- asks (.brig) void $ catchRpcErrors $ rpc' @@ -192,7 +172,7 @@ getUserConnections uid = do else pure (batch ++ xs) fetchBatch :: Maybe UserId -> Handler UserConnectionList fetchBatch start = do - b <- view brig + b <- asks (.brig) r <- catchRpcErrors $ rpc' @@ -211,7 +191,7 @@ getUserConnections uid = do getUsersConnections :: List UserId -> Handler [ConnectionStatus] getUsersConnections uids = do info $ msg "Getting user connections" - b <- view brig + b <- asks (.brig) let reqBody = ConnectionsStatusRequest (fromList uids) Nothing r <- catchRpcErrors $ @@ -226,13 +206,13 @@ getUsersConnections uids = do info $ msg ("Response" ++ show r) parseResponse (mkError status502 "bad-upstream") r -getUserProfiles :: Either [UserId] [Handle] -> Handler [UserAccount] +getUserProfiles :: Either [UserId] [Handle] -> Handler [User] getUserProfiles uidsOrHandles = do info $ msg "Getting user accounts" - b <- view brig + b <- asks (.brig) concat <$> mapM (doRequest b) (prepareQS uidsOrHandles) where - doRequest :: Request -> (Request -> Request) -> Handler [UserAccount] + doRequest :: Request -> (Request -> Request) -> Handler [User] doRequest b qry = do r <- catchRpcErrors $ @@ -253,10 +233,10 @@ getUserProfiles uidsOrHandles = do fmap (BS.intercalate "," . map toByteString') . chunksOf 50 -getUserProfilesByIdentity :: Email -> Handler [UserAccount] +getUserProfilesByIdentity :: EmailAddress -> Handler [User] getUserProfilesByIdentity email = do info $ msg "Getting user accounts by identity" - b <- view brig + b <- asks (.brig) r <- catchRpcErrors $ rpc' @@ -272,7 +252,7 @@ getUserProfilesByIdentity email = do getEjpdInfo :: [Handle] -> Bool -> Handler EJPD.EJPDResponseBody getEjpdInfo handles includeContacts = do info $ msg "Getting ejpd info on users by handle" - b <- view brig + b <- asks (.brig) let bdy :: Value bdy = object @@ -295,7 +275,7 @@ getEjpdInfo handles includeContacts = do getContacts :: UserId -> Text -> Int32 -> Handler (SearchResult Contact) getContacts u q s = do info $ msg "Getting user contacts" - b <- view brig + b <- asks (.brig) r <- catchRpcErrors $ rpc' @@ -310,10 +290,10 @@ getContacts u q s = do ) parseResponse (mkError status502 "bad-upstream") r -revokeIdentity :: Email -> Handler () +revokeIdentity :: EmailAddress -> Handler () revokeIdentity email = do info $ msg "Revoking user identity" - b <- view brig + b <- asks (.brig) void . catchRpcErrors $ rpc' @@ -328,7 +308,7 @@ revokeIdentity email = do deleteAccount :: UserId -> Handler () deleteAccount uid = do info $ msg "Deleting account" - b <- view brig + b <- asks (.brig) void . catchRpcErrors $ rpc' @@ -346,7 +326,7 @@ setStatusBindingTeam tid status = do ( "Setting team status to " <> UTF8.toString (BS.toStrict . encode $ status) ) - g <- view galley + g <- asks (.galley) void . catchRpcErrors $ rpc' @@ -361,7 +341,7 @@ setStatusBindingTeam tid status = do deleteBindingTeam :: TeamId -> Handler () deleteBindingTeam tid = do info $ msg "Deleting team" - g <- view galley + g <- asks (.galley) void . catchRpcErrors $ rpc' @@ -376,7 +356,7 @@ deleteBindingTeam tid = do deleteBindingTeamForce :: TeamId -> Handler () deleteBindingTeamForce tid = do info $ msg "Deleting team with force flag" - g <- view galley + g <- asks (.galley) void . catchRpcErrors $ rpc' @@ -391,7 +371,7 @@ deleteBindingTeamForce tid = do changeEmail :: UserId -> EmailUpdate -> Handler () changeEmail u upd = do info $ msg "Updating email address" - b <- view brig + b <- asks (.brig) void . catchRpcErrors $ rpc' @@ -407,24 +387,6 @@ changeEmail u upd = do . expect2xx ) -changePhone :: UserId -> PhoneUpdate -> Handler () -changePhone u upd = do - info $ msg "Updating phone number" - b <- view brig - void - . catchRpcErrors - $ rpc' - "brig" - b - ( method PUT - . versionedPath "self/phone" - . header "Z-User" (toByteString' u) - . header "Z-Connection" (toByteString' "") - . lbytes (encode upd) - . contentJson - . expect2xx - ) - getTeamInfo :: TeamId -> Handler TeamInfo getTeamInfo tid = do d <- getTeamData tid @@ -434,7 +396,7 @@ getTeamInfo tid = do getUserBindingTeam :: UserId -> Handler (Maybe TeamId) getUserBindingTeam u = do info $ msg "Getting user binding team" - g <- view galley + g <- asks (.galley) r <- catchRpcErrors $ rpc' @@ -457,7 +419,7 @@ getUserBindingTeam u = do getInvoiceUrl :: TeamId -> InvoiceId -> Handler ByteString getInvoiceUrl tid iid = do info $ msg "Getting invoice" - i <- view ibis + i <- asks (.ibis) r <- catchRpcErrors $ rpc' @@ -473,7 +435,7 @@ getInvoiceUrl tid iid = do getTeamBillingInfo :: TeamId -> Handler (Maybe TeamBillingInfo) getTeamBillingInfo tid = do info $ msg "Getting team billing info" - i <- view ibis + i <- asks (.ibis) resp <- catchRpcErrors $ rpc' @@ -490,7 +452,7 @@ getTeamBillingInfo tid = do setTeamBillingInfo :: TeamId -> TeamBillingInfo -> Handler () setTeamBillingInfo tid tbu = do info $ msg "Setting team billing info" - i <- view ibis + i <- asks (.ibis) void . catchRpcErrors $ rpc' @@ -503,10 +465,10 @@ setTeamBillingInfo tid tbu = do . expect2xx ) -isBlacklisted :: Email -> Handler Bool +isBlacklisted :: EmailAddress -> Handler Bool isBlacklisted email = do info $ msg "Checking blacklist" - b <- view brig + b <- asks (.brig) resp <- catchRpcErrors $ rpc' @@ -521,10 +483,10 @@ isBlacklisted email = do 404 -> pure False _ -> throwE (mkError status502 "bad-upstream" (errorMessage resp)) -setBlacklistStatus :: Bool -> Email -> Handler () +setBlacklistStatus :: Bool -> EmailAddress -> Handler () setBlacklistStatus status email = do info $ msg "Changing blacklist status" - b <- view brig + b <- asks (.brig) void . catchRpcErrors $ rpc' @@ -541,78 +503,68 @@ setBlacklistStatus status email = do getTeamFeatureFlag :: forall cfg. - ( Typeable (Public.WithStatus cfg), - FromJSON (Public.WithStatus cfg), - KnownSymbol (Public.FeatureSymbol cfg) - ) => + (IsFeatureConfig cfg, Typeable cfg) => TeamId -> - Handler (Public.WithStatus cfg) + Handler (Public.LockableFeature cfg) getTeamFeatureFlag tid = do info $ msg "Getting team feature status" - gly <- view galley + gly <- asks (.galley) let req = method GET . Bilge.paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] resp <- catchRpcErrors $ rpc' "galley" gly req case Bilge.statusCode resp of - 200 -> pure $ responseJsonUnsafe @(Public.WithStatus cfg) resp + 200 -> pure $ responseJsonUnsafe @(Public.LockableFeature cfg) resp 404 -> throwE (mkError status404 "bad-upstream" "team doesnt exist") _ -> throwE (mkError status502 "bad-upstream" (errorMessage resp)) setTeamFeatureFlag :: forall cfg. - ( ToJSON (Public.WithStatusNoLock cfg), - KnownSymbol (Public.FeatureSymbol cfg) - ) => + (IsFeatureConfig cfg) => TeamId -> - Public.WithStatusNoLock cfg -> + Public.Feature cfg -> Handler () setTeamFeatureFlag tid status = do info $ msg "Setting team feature status" - checkDaysLimit (wssTTL status) - gly <- view galley - let req = - method PUT - . Bilge.paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] - . Bilge.json status - . contentJson + galleyRpc $ + method PUT + . Bilge.paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] + . Bilge.json status + . contentJson + +patchTeamFeatureFlag :: + forall cfg. + (IsFeatureConfig cfg) => + TeamId -> + Public.LockableFeaturePatch cfg -> + Handler () +patchTeamFeatureFlag tid patch = do + info $ msg "Patching team feature status" + galleyRpc $ + method PATCH + . Bilge.paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] + . Bilge.json patch + . contentJson + +galleyRpc :: (Bilge.Request -> Bilge.Request) -> Handler () +galleyRpc req = do + gly <- asks (.galley) resp <- catchRpcErrors $ rpc' "galley" gly req case statusCode resp of 200 -> pure () 404 -> throwE (mkError status404 "bad-upstream" "team does not exist") - 403 -> throwE (mkError status403 "bad-upstream" "legal hold config cannot be changed") + 403 -> throwE (mkError status403 "bad-upstream" "config cannot be changed") _ -> throwE (mkError status502 "bad-upstream" (errorMessage resp)) - where - checkDaysLimit :: FeatureTTL -> Handler () - checkDaysLimit = \case - FeatureTTLUnlimited -> pure () - FeatureTTLSeconds ((`div` (60 * 60 * 24)) -> days) -> do - unless (days <= daysLimit) $ do - throwE - ( mkError - status400 - "bad-data" - ( LT.pack $ - "ttl limit is " - <> show daysLimit - <> " days; I got " - <> show days - <> "." - ) - ) - where - daysLimit = 2000 setTeamFeatureLockStatus :: forall cfg. - ( KnownSymbol (Public.FeatureSymbol cfg) - ) => + (IsFeatureConfig cfg) => TeamId -> LockStatus -> Handler () setTeamFeatureLockStatus tid lstat = do - info $ msg ("Setting lock status: " <> show (symbolVal (Proxy @(Public.FeatureSymbol cfg)), lstat)) - gly <- view galley + info $ msg ("Setting lock status: " <> featureName @cfg) + gly <- asks (.galley) fromResponseBody <=< catchRpcErrors $ rpc' @@ -635,7 +587,7 @@ setTeamFeatureLockStatus tid lstat = do getSearchVisibility :: TeamId -> Handler TeamSearchVisibilityView getSearchVisibility tid = do info $ msg "Getting TeamSearchVisibilityView value" - gly <- view galley + gly <- asks (.galley) fromResponseBody <=< catchRpcErrors $ rpc' @@ -652,7 +604,7 @@ getSearchVisibility tid = do setSearchVisibility :: TeamId -> TeamSearchVisibility -> Handler () setSearchVisibility tid typ = do info $ msg "Setting TeamSearchVisibility value" - gly <- view galley + gly <- asks (.galley) resp <- catchRpcErrors $ rpc' @@ -679,7 +631,7 @@ setSearchVisibility tid typ = do stripBS :: ByteString -> ByteString stripBS = encodeUtf8 . strip . decodeUtf8 -userKeyToParam :: Email -> Request -> Request +userKeyToParam :: EmailAddress -> Request -> Request userKeyToParam e = queryItem "email" (stripBS $ toByteString' e) errorMessage :: Response (Maybe LByteString) -> LText @@ -699,7 +651,7 @@ catchRpcErrors action = ExceptT $ catch (Right <$> action) catchRPCException getTeamData :: TeamId -> Handler TeamData getTeamData tid = do info $ msg "Getting team information" - g <- view galley + g <- asks (.galley) r <- catchRpcErrors $ rpc' @@ -716,7 +668,7 @@ getTeamData tid = do getTeamMembers :: TeamId -> Handler TeamMemberList getTeamMembers tid = do info $ msg "Getting team members" - g <- view galley + g <- asks (.galley) r <- catchRpcErrors $ rpc' @@ -728,10 +680,10 @@ getTeamMembers tid = do ) parseResponse (mkError status502 "bad-upstream") r -getEmailConsentLog :: Email -> Handler ConsentLog +getEmailConsentLog :: EmailAddress -> Handler ConsentLog getEmailConsentLog email = do info $ msg "Getting email consent log" - g <- view galeb + g <- asks (.galeb) r <- catchRpcErrors $ rpc' @@ -749,7 +701,7 @@ getEmailConsentLog email = do getUserConsentValue :: UserId -> Handler ConsentValue getUserConsentValue uid = do info $ msg "Getting user consent value" - g <- view galeb + g <- asks (.galeb) r <- catchRpcErrors $ rpc' @@ -762,10 +714,10 @@ getUserConsentValue uid = do ) parseResponse (mkError status502 "bad-upstream") r -getMarketoResult :: Email -> Handler MarketoResult +getMarketoResult :: EmailAddress -> Handler MarketoResult getMarketoResult email = do info $ msg "Getting marketo results" - g <- view galeb + g <- asks (.galeb) r <- catchRpcErrors $ rpc' @@ -794,7 +746,7 @@ getMarketoResult email = do getUserConsentLog :: UserId -> Handler ConsentLog getUserConsentLog uid = do info $ msg "Getting user consent log" - g <- view galeb + g <- asks (.galeb) r <- catchRpcErrors $ rpc' @@ -809,7 +761,7 @@ getUserConsentLog uid = do getUserCookies :: UserId -> Handler CookieList getUserCookies uid = do info $ msg "Getting user cookies" - g <- view brig + g <- asks (.brig) r <- catchRpcErrors $ rpc' @@ -837,7 +789,7 @@ getUserConversations uid maxConvs = do else pure (batch ++ xs) fetchBatch :: Maybe ConvId -> Int -> Handler (ConversationList Conversation) fetchBatch start batchSize = do - baseReq <- view galley + baseReq <- asks (.galley) r <- catchRpcErrors $ rpc' @@ -855,7 +807,7 @@ getUserConversations uid maxConvs = do getUserClients :: UserId -> Handler [Client] getUserClients uid = do info $ msg "Getting user clients" - b <- view brig + b <- asks (.brig) r <- catchRpcErrors $ rpc' @@ -867,7 +819,7 @@ getUserClients uid = do . expect2xx ) info $ msg ("Response" ++ show r) - let resultOrError :: Either String [Versioned 'V5 Client] = responseJsonEither r + let resultOrError :: Either String [Versioned 'V6 Client] = responseJsonEither r case resultOrError of Left e -> do Log.err $ msg ("Error parsing client response: " ++ e) @@ -877,7 +829,7 @@ getUserClients uid = do getUserProperties :: UserId -> Handler UserProperties getUserProperties uid = do info $ msg "Getting user properties" - b <- view brig + b <- asks (.brig) r <- catchRpcErrors $ rpc' @@ -927,7 +879,7 @@ getUserNotifications uid maxNotifs = do else pure (batch ++ xs) fetchBatch :: Maybe NotificationId -> Int -> Handler (Maybe QueuedNotificationList) fetchBatch start batchSize = do - baseReq <- view gundeck + baseReq <- asks (.gundeck) r <- catchRpcErrors $ rpc' @@ -965,7 +917,7 @@ getSsoDomainRedirect :: Text -> Handler (Maybe CustomBackend) getSsoDomainRedirect domain = do info $ msg "getSsoDomainRedirect" -- curl -XGET ${CLOUD_BACKEND}/custom-backend/by-domain/${DOMAIN_EXAMPLE} - g <- view galley + g <- asks (.galley) r <- catchRpcErrors $ rpc' @@ -988,7 +940,7 @@ putSsoDomainRedirect domain config welcome = do -- "webapp_welcome_url": "https://app.wire.example.com/" \ -- }' -- curl -XPUT http://localhost/i/custom-backend/by-domain/${DOMAIN_EXAMPLE} -d "${DOMAIN_ENTRY}" - g <- view galley + g <- asks (.galley) void . catchRpcErrors $ rpc' @@ -1009,7 +961,7 @@ deleteSsoDomainRedirect :: Text -> Handler () deleteSsoDomainRedirect domain = do info $ msg "deleteSsoDomainRedirect" -- curl -XDELETE http://localhost/i/custom-backend/by-domain/${DOMAIN_EXAMPLE} - g <- view galley + g <- asks (.galley) void . catchRpcErrors $ rpc' @@ -1027,7 +979,7 @@ deleteSsoDomainRedirect domain = do registerOAuthClient :: OAuthClientConfig -> Handler OAuthClientCredentials registerOAuthClient conf = do - b <- view brig + b <- asks (.brig) r <- catchRpcErrors $ rpc' @@ -1043,14 +995,15 @@ registerOAuthClient conf = do getOAuthClient :: OAuthClientId -> Handler OAuthClient getOAuthClient cid = do - b <- view brig + b <- asks (.brig) r <- - rpc' - "brig" - b - ( method GET - . Bilge.paths ["i", "oauth", "clients", toByteString' cid] - ) + lift $ + rpc' + "brig" + b + ( method GET + . Bilge.paths ["i", "oauth", "clients", toByteString' cid] + ) case statusCode r of 200 -> parseResponse (mkError status502 "bad-upstream") r 404 -> throwE (mkError status404 "bad-upstream" "not-found") @@ -1058,7 +1011,7 @@ getOAuthClient cid = do updateOAuthClient :: OAuthClientId -> OAuthClientConfig -> Handler OAuthClient updateOAuthClient cid conf = do - b <- view brig + b <- asks (.brig) r <- catchRpcErrors $ rpc' @@ -1074,7 +1027,7 @@ updateOAuthClient cid conf = do deleteOAuthClient :: OAuthClientId -> Handler () deleteOAuthClient cid = do - b <- view brig + b <- asks (.brig) r <- catchRpcErrors $ rpc' diff --git a/tools/stern/stern.cabal b/tools/stern/stern.cabal index e7572f7c330..b7e04c9de2b 100644 --- a/tools/stern/stern.cabal +++ b/tools/stern/stern.cabal @@ -101,7 +101,6 @@ library , types-common >=0.4.13 , unliftio , utf8-string - , uuid >=1.3 , wai >=3.0 , wai-utilities >=0.9 , wire-api >=0.1 @@ -187,11 +186,8 @@ test-suite stern-tests executable stern-integration main-is: Main.hs - - -- cabal-fmt: expand test/integration other-modules: API - Main TestSetup Util @@ -252,6 +248,7 @@ executable stern-integration , bytestring-conversion , containers , cookie + , data-default , exceptions , extra >=1.3 , HsOpenSSL diff --git a/tools/stern/test/integration/API.hs b/tools/stern/test/integration/API.hs index 69b351cabdb..0e8b84cb5f1 100644 --- a/tools/stern/test/integration/API.hs +++ b/tools/stern/test/integration/API.hs @@ -1,7 +1,7 @@ {-# LANGUAGE OverloadedRecordDot #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} +{-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -Wno-redundant-constraints #-} -{-# OPTIONS_GHC -fno-warn-incomplete-patterns #-} -{-# OPTIONS_GHC -fno-warn-orphans #-} -- This file is part of the Wire Server implementation. -- @@ -30,6 +30,7 @@ import Control.Lens hiding ((.=)) import Data.Aeson (ToJSON, Value) import Data.Aeson qualified as A import Data.ByteString.Conversion +import Data.Default import Data.Handle import Data.Id import Data.Range (unsafeRange) @@ -85,7 +86,7 @@ tests s = test s "/teams/:tid/features/validateSamlEmails" $ testFeatureStatus @ValidateSAMLEmailsConfig, test s "/teams/:tid/features/digitalSignatures" $ testFeatureStatus @DigitalSignaturesConfig, test s "/teams/:tid/features/fileSharing" $ testFeatureStatus @FileSharingConfig, - test s "/teams/:tid/features/conference-calling" $ testFeatureStatusOptTtl @ConferenceCallingConfig (Just FeatureTTLUnlimited), + test s "/teams/:tid/features/conference-calling" $ testFeatureStatusOptTtl defConfCalling (Just FeatureTTLUnlimited), test s "/teams/:tid/searchVisibility" $ testFeatureStatus @SearchVisibilityAvailableConfig, test s "/teams/:tid/features/appLock" $ testFeatureConfig @AppLockConfig, test s "/teams/:tid/features/mls" $ testFeatureConfig @MLSConfig, @@ -105,6 +106,9 @@ tests s = -- - `POST /teams/:tid/billing` ] +defConfCalling :: LockableFeature ConferenceCallingConfig +defConfCalling = def {status = FeatureStatusDisabled} + testRudSsoDomainRedirect :: TestM () testRudSsoDomainRedirect = do testGet 1 Nothing @@ -178,7 +182,7 @@ testDeleteUser = do (uid, email) <- randomEmailUser do [ua] <- getUsersByIds [uid] - liftIO $ ua.accountStatus @?= Active + liftIO $ ua.userStatus @?= Active deleteUser uid (Left email) do uas <- getUsersByIds [uid] @@ -211,7 +215,7 @@ testDeleteTeam :: TestM () testDeleteTeam = do (uid, tid, _) <- createTeamWithNMembers 10 [ua] <- getUsersByIds [uid] - let email = fromMaybe (error "user has no email") $ emailIdentity =<< ua.accountUser.userIdentity + let email = fromMaybe (error "user has no email") $ emailIdentity =<< ua.userIdentity do info <- getTeamInfo tid liftIO $ info.tiData.tdStatus @?= Team.Active @@ -241,7 +245,7 @@ testGetTeamInfoByMemberEmail :: TestM () testGetTeamInfoByMemberEmail = do (_, tid, member : _) <- createTeamWithNMembers 10 [ua] <- getUsersByIds [member] - let email = fromMaybe (error "user has no email") $ emailIdentity =<< ua.accountUser.userIdentity + let email = fromMaybe (error "user has no email") $ emailIdentity =<< ua.userIdentity info <- getTeamInfoByMemberEmail email liftIO $ (info.tiData.tdTeam ^. teamId) @?= tid @@ -258,7 +262,7 @@ testLegalholdConfig :: TestM () testLegalholdConfig = do (_, tid, _) <- createTeamWithNMembers 10 cfg <- getFeatureConfig @LegalholdConfig tid - liftIO $ cfg @?= defFeatureStatus @LegalholdConfig + liftIO $ cfg @?= def -- Legal hold is enabled for teams via server config and cannot be changed here putFeatureStatus @LegalholdConfig tid FeatureStatusEnabled Nothing !!! const 403 === statusCode @@ -275,11 +279,11 @@ testFeatureConfig :: testFeatureConfig = do (_, tid, _) <- createTeamWithNMembers 10 cfg <- getFeatureConfig @cfg tid - liftIO $ cfg @?= defFeatureStatus @cfg - let newStatus = if wsStatus cfg == FeatureStatusEnabled then FeatureStatusDisabled else FeatureStatusEnabled - putFeatureConfig @cfg tid (setStatus newStatus cfg) !!! const 200 === statusCode + liftIO $ cfg @?= def + let newStatus = if cfg.status == FeatureStatusEnabled then FeatureStatusDisabled else FeatureStatusEnabled + putFeatureConfig @cfg tid cfg {status = newStatus} !!! const 200 === statusCode cfg' <- getFeatureConfig @cfg tid - liftIO $ wsStatus cfg' @?= newStatus + liftIO $ cfg'.status @?= newStatus testGetFeatureConfig :: forall cfg. @@ -295,7 +299,7 @@ testGetFeatureConfig :: testGetFeatureConfig mDef = do (_, tid, _) <- createTeamWithNMembers 10 cfg <- getFeatureConfig @cfg tid - liftIO $ wsStatus cfg @?= fromMaybe (wsStatus $ defFeatureStatus @cfg) mDef + liftIO $ cfg.status @?= fromMaybe (def @(Feature cfg)).status mDef testFeatureStatus :: forall cfg. @@ -307,7 +311,7 @@ testFeatureStatus :: Show cfg ) => TestM () -testFeatureStatus = testFeatureStatusOptTtl @cfg Nothing +testFeatureStatus = testFeatureStatusOptTtl @cfg def Nothing testFeatureStatusOptTtl :: forall cfg. @@ -318,17 +322,18 @@ testFeatureStatusOptTtl :: Eq cfg, Show cfg ) => + LockableFeature cfg -> Maybe FeatureTTL -> TestM () -testFeatureStatusOptTtl mTtl = do +testFeatureStatusOptTtl defValue mTtl = do (_, tid, _) <- createTeamWithNMembers 10 cfg <- getFeatureConfig @cfg tid - liftIO $ cfg @?= defFeatureStatus @cfg - when (wsLockStatus cfg == LockStatusLocked) $ unlockFeature @cfg tid - let newStatus = if wsStatus cfg == FeatureStatusEnabled then FeatureStatusDisabled else FeatureStatusEnabled + liftIO $ cfg @?= defValue + when (cfg.lockStatus == LockStatusLocked) $ unlockFeature @cfg tid + let newStatus = if cfg.status == FeatureStatusEnabled then FeatureStatusDisabled else FeatureStatusEnabled putFeatureStatus @cfg tid newStatus mTtl !!! const 200 === statusCode cfg' <- getFeatureConfig @cfg tid - liftIO $ wsStatus cfg' @?= newStatus + liftIO $ cfg'.status @?= newStatus testFeatureStatusWithLock :: forall cfg. @@ -344,31 +349,31 @@ testFeatureStatusWithLock = do let mTtl = Nothing -- this function can become a variant of `testFeatureStatusOptTtl` if we need one. (_, tid, _) <- createTeamWithNMembers 10 getFeatureConfig @cfg tid >>= \cfg -> liftIO $ do - cfg @?= defFeatureStatus @cfg + cfg @?= def -- if either of these two lines fails, it's probably because the default is surprising. -- in that case, make the text more flexible. - wsLockStatus cfg @?= LockStatusLocked - wsStatus cfg @?= FeatureStatusDisabled + cfg.lockStatus @?= LockStatusLocked + cfg.status @?= FeatureStatusDisabled void $ putFeatureStatusLock @cfg tid LockStatusUnlocked mTtl getFeatureConfig @cfg tid >>= \cfg -> liftIO $ do - wsLockStatus cfg @?= LockStatusUnlocked - wsStatus cfg @?= FeatureStatusDisabled + cfg.lockStatus @?= LockStatusUnlocked + cfg.status @?= FeatureStatusDisabled void $ putFeatureStatus @cfg tid FeatureStatusEnabled Nothing getFeatureConfig @cfg tid >>= \cfg -> liftIO $ do - wsLockStatus cfg @?= LockStatusUnlocked - wsStatus cfg @?= FeatureStatusEnabled + cfg.lockStatus @?= LockStatusUnlocked + cfg.status @?= FeatureStatusEnabled void $ putFeatureStatusLock @cfg tid LockStatusLocked mTtl getFeatureConfig @cfg tid >>= \cfg -> liftIO $ do - wsLockStatus cfg @?= LockStatusLocked - wsStatus cfg @?= FeatureStatusDisabled + cfg.lockStatus @?= LockStatusLocked + cfg.status @?= FeatureStatusDisabled void $ putFeatureStatusLock @cfg tid LockStatusUnlocked mTtl getFeatureConfig @cfg tid >>= \cfg -> liftIO $ do - wsLockStatus cfg @?= LockStatusUnlocked - wsStatus cfg @?= FeatureStatusEnabled + cfg.lockStatus @?= LockStatusUnlocked + cfg.status @?= FeatureStatusEnabled testGetConsentLog :: TestM () testGetConsentLog = do @@ -394,13 +399,13 @@ testGetUsersByHandles = do h <- randomHandle void $ setHandle uid h [ua] <- getUsersByHandles h - liftIO $ userId ua.accountUser @?= uid + liftIO $ userId ua @?= uid testGetUsersByEmail :: TestM () testGetUsersByEmail = do (uid, email) <- randomEmailUser [ua] <- getUsersByEmail email - liftIO $ userId ua.accountUser @?= uid + liftIO $ userId ua @?= uid testUnsuspendUser :: TestM () testUnsuspendUser = do @@ -408,18 +413,18 @@ testUnsuspendUser = do void $ postSupendUser uid do [ua] <- getUsersByIds [uid] - liftIO $ ua.accountStatus @?= Suspended + liftIO $ ua.userStatus @?= Suspended void $ postUnsuspendUser uid do [ua] <- getUsersByIds [uid] - liftIO $ ua.accountStatus @?= Active + liftIO $ ua.userStatus @?= Active testSuspendUser :: TestM () testSuspendUser = do uid <- randomUser void $ postSupendUser uid [ua] <- getUsersByIds [uid] - liftIO $ ua.accountStatus @?= Suspended + liftIO $ ua.userStatus @?= Suspended testGetStatus :: TestM () testGetStatus = do @@ -434,7 +439,7 @@ testGetUsersByIds = do uas <- getUsersByIds [uid1, uid2] liftIO $ do length uas @?= 2 - Set.fromList (userId . (.accountUser) <$> uas) @?= Set.fromList [uid1, uid2] + Set.fromList (userId <$> uas) @?= Set.fromList [uid1, uid2] testGetTeamInfo :: TestM () testGetTeamInfo = do @@ -455,14 +460,14 @@ testRevokeIdentity = do do [ua] <- getUsersByEmail email liftIO $ do - ua.accountStatus @?= Active - isJust ua.accountUser.userIdentity @?= True + ua.userStatus @?= Active + isJust ua.userIdentity @?= True void $ revokeIdentity (Left email) do [ua] <- getUsersByEmail email liftIO $ do - ua.accountStatus @?= Active - isJust ua.accountUser.userIdentity @?= False + ua.userStatus @?= Active + isJust ua.userIdentity @?= False testPutEmail :: TestM () testPutEmail = do @@ -489,13 +494,13 @@ getConnections uid = do r <- get (s . paths ["users", toByteString' uid, "connections"] . expect2xx) pure $ responseJsonUnsafe r -getUsersByHandles :: Text -> TestM [UserAccount] +getUsersByHandles :: Text -> TestM [User] getUsersByHandles h = do stern <- view tsStern r <- get (stern . paths ["users", "by-handles"] . queryItem "handles" (cs h) . expect2xx) pure $ responseJsonUnsafe r -getUsersByEmail :: Email -> TestM [UserAccount] +getUsersByEmail :: EmailAddress -> TestM [User] getUsersByEmail email = do stern <- view tsStern r <- get (stern . paths ["users", "by-email"] . queryItem "email" (toByteString' email) . expect2xx) @@ -516,7 +521,7 @@ getStatus = do stern <- view tsStern get (stern . paths ["i", "status"] . expect2xx) -getUsersByIds :: [UserId] -> TestM [UserAccount] +getUsersByIds :: [UserId] -> TestM [User] getUsersByIds uids = do stern <- view tsStern r <- get (stern . paths ["users", "by-ids"] . queryItem "ids" (toByteString' uids) . expect2xx) @@ -534,12 +539,12 @@ searchUsers uid = do r <- get (s . paths ["users", toByteString' uid, "search"] . expect2xx) pure $ responseJsonUnsafe r -revokeIdentity :: Either Email Phone -> TestM () +revokeIdentity :: Either EmailAddress Phone -> TestM () revokeIdentity emailOrPhone = do s <- view tsStern void $ post (s . paths ["users", "revoke-identity"] . mkQueryParam emailOrPhone . expect2xx) -mkQueryParam :: Either Email Phone -> Request -> Request +mkQueryParam :: Either EmailAddress Phone -> Request -> Request mkQueryParam = \case Left email -> queryItem "email" (toByteString' email) Right phone -> queryItem "phone" (toByteString' phone) @@ -549,7 +554,7 @@ putEmail uid emailUpdate = do s <- view tsStern void $ put (s . paths ["users", toByteString' uid, "email"] . json emailUpdate . expect2xx) -deleteUser :: UserId -> Either Email Phone -> TestM () +deleteUser :: UserId -> Either EmailAddress Phone -> TestM () deleteUser uid emailOrPhone = do s <- view tsStern void $ delete (s . paths ["users", toByteString' uid] . mkQueryParam emailOrPhone . expect2xx) @@ -564,7 +569,7 @@ unsuspendTeam tid = do s <- view tsStern void $ put (s . paths ["teams", toByteString' tid, "unsuspend"] . expect2xx) -deleteTeam :: TeamId -> Bool -> Email -> TestM () +deleteTeam :: TeamId -> Bool -> EmailAddress -> TestM () deleteTeam tid force email = do s <- view tsStern void $ delete (s . paths ["teams", toByteString' tid] . queryItem "force" (toByteString' force) . queryItem "email" (toByteString' email) . expect2xx) @@ -575,22 +580,22 @@ ejpdInfo includeContacts handles = do r <- get (s . paths ["ejpd-info"] . queryItem "include_contacts" (toByteString' includeContacts) . queryItem "handles" (toByteString' handles) . expect2xx) pure $ responseJsonUnsafe r -userBlacklistHead :: Either Email Phone -> TestM ResponseLBS +userBlacklistHead :: Either EmailAddress Phone -> TestM ResponseLBS userBlacklistHead emailOrPhone = do s <- view tsStern Bilge.get (s . paths ["users", "blacklist"] . mkQueryParam emailOrPhone) -postUserBlacklist :: Either Email Phone -> TestM () +postUserBlacklist :: Either EmailAddress Phone -> TestM () postUserBlacklist emailOrPhone = do s <- view tsStern void $ post (s . paths ["users", "blacklist"] . mkQueryParam emailOrPhone . expect2xx) -deleteUserBlacklist :: Either Email Phone -> TestM () +deleteUserBlacklist :: Either EmailAddress Phone -> TestM () deleteUserBlacklist emailOrPhone = do s <- view tsStern void $ delete (s . paths ["users", "blacklist"] . mkQueryParam emailOrPhone . expect2xx) -getTeamInfoByMemberEmail :: Email -> TestM TeamInfo +getTeamInfoByMemberEmail :: EmailAddress -> TestM TeamInfo getTeamInfoByMemberEmail email = do s <- view tsStern r <- get (s . paths ["teams"] . queryItem "email" (toByteString' email) . expect2xx) @@ -610,7 +615,7 @@ getFeatureConfig :: IsFeatureConfig cfg ) => TeamId -> - TestM (WithStatus cfg) + TestM (LockableFeature cfg) getFeatureConfig tid = do s <- view tsStern r <- get (s . paths ["teams", toByteString' tid, "features", Public.featureNameBS @cfg] . expect2xx) @@ -665,10 +670,10 @@ putFeatureConfig :: ToSchema cfg, Typeable cfg, IsFeatureConfig cfg, - ToJSON (WithStatus cfg) + ToJSON (LockableFeature cfg) ) => TeamId -> - WithStatus cfg -> + LockableFeature cfg -> TestM ResponseLBS putFeatureConfig tid cfg = do s <- view tsStern @@ -685,7 +690,7 @@ putSearchVisibility tid vis = do s <- view tsStern void $ put (s . paths ["teams", toByteString' tid, "search-visibility"] . json vis . expect2xx) -getConsentLog :: Email -> TestM ResponseLBS +getConsentLog :: EmailAddress -> TestM ResponseLBS getConsentLog email = do s <- view tsStern get (s . paths ["i", "consent"] . queryItem "email" (toByteString' email)) @@ -702,7 +707,7 @@ unlockFeature :: ToSchema cfg, Typeable cfg, IsFeatureConfig cfg, - ToJSON (WithStatus cfg) + ToJSON (LockableFeature cfg) ) => TeamId -> TestM () diff --git a/tools/stern/test/integration/Util.hs b/tools/stern/test/integration/Util.hs index 2c26c513d13..d151434f164 100644 --- a/tools/stern/test/integration/Util.hs +++ b/tools/stern/test/integration/Util.hs @@ -87,7 +87,7 @@ randomUser'' isCreator hasPassword hasEmail = selfUser <$> randomUserProfile' is randomUserProfile' :: (HasCallStack) => Bool -> Bool -> Bool -> TestM SelfProfile randomUserProfile' isCreator hasPassword hasEmail = randomUserProfile'' isCreator hasPassword hasEmail <&> fst -randomUserProfile'' :: (HasCallStack) => Bool -> Bool -> Bool -> TestM (SelfProfile, Email) +randomUserProfile'' :: (HasCallStack) => Bool -> Bool -> Bool -> TestM (SelfProfile, EmailAddress) randomUserProfile'' isCreator hasPassword hasEmail = do b <- view tsBrig e <- liftIO randomEmail @@ -96,19 +96,19 @@ randomUserProfile'' isCreator hasPassword hasEmail = do ["name" .= fromEmail e] <> ["password" .= defPassword | hasPassword] <> ["email" .= fromEmail e | hasEmail] - <> ["team" .= BindingNewTeam (newNewTeam (unsafeRange "teamName") DefaultIcon) | isCreator] + <> ["team" .= newNewTeam (unsafeRange "teamName") DefaultIcon | isCreator] (,e) . responseJsonUnsafe <$> (post (b . path "/i/users" . Bilge.json pl) TestM (UserId, Email) +randomEmailUser :: (HasCallStack) => TestM (UserId, EmailAddress) randomEmailUser = randomUserProfile'' False False True <&> first (User.userId . selfUser) defPassword :: PlainTextPassword8 defPassword = plainTextPassword8Unsafe "topsecretdefaultpassword" -randomEmail :: (MonadIO m) => m Email +randomEmail :: (MonadIO m) => m EmailAddress randomEmail = do uid <- liftIO nextRandom - pure $ Email ("success+" <> UUID.toText uid) "simulator.amazonses.com" + pure $ unsafeEmailAddress ("success+" <> UUID.toASCIIBytes uid) "simulator.amazonses.com" setHandle :: UserId -> Text -> TestM () setHandle uid h = do @@ -139,7 +139,7 @@ addUserToTeamWithRole role inviter tid = do (inv, rsp2) <- addUserToTeamWithRole' role inviter tid let invitee :: User = responseJsonUnsafe rsp2 inviteeId = User.userId invitee - let invmeta = Just (inviter, inCreatedAt inv) + let invmeta = Just (inviter, inv.createdAt) mem <- getTeamMember inviter tid inviteeId liftIO $ assertEqual "Member has no/wrong invitation metadata" invmeta (mem ^. Team.invitation) let zuid = parseSetCookie <$> getHeader "Set-Cookie" rsp2 @@ -149,21 +149,21 @@ addUserToTeamWithRole role inviter tid = do addUserToTeamWithRole' :: (HasCallStack) => Maybe Role -> UserId -> TeamId -> TestM (Invitation, ResponseLBS) addUserToTeamWithRole' role inviter tid = do brig <- view tsBrig - inviteeEmail <- randomEmail - let invite = InvitationRequest Nothing role Nothing inviteeEmail Nothing + email <- randomEmail + let invite = InvitationRequest Nothing role Nothing email invResponse <- postInvitation tid inviter invite inv <- responseJsonError invResponse - inviteeCode <- getInvitationCode tid (inInvitation inv) + inviteeCode <- getInvitationCode tid inv.invitationId r <- post ( brig . path "/register" . contentJson - . body (acceptInviteBody inviteeEmail inviteeCode) + . body (acceptInviteBody email inviteeCode) ) pure (inv, r) -acceptInviteBody :: Email -> InvitationCode -> RequestBody +acceptInviteBody :: EmailAddress -> InvitationCode -> RequestBody acceptInviteBody email code = RequestBodyLBS . encode $ object diff --git a/weeder.toml b/weeder.toml index 5e2042081e4..64669d11e89 100644 --- a/weeder.toml +++ b/weeder.toml @@ -1,5 +1,194 @@ # weeder intro and further reading: https://github.com/ocharles/weeder?tab=readme-ov-file#weeder -roots = ["^Main.main$", "^Paths_.*", "^Testlib.RunServices.main$", "^Testlib.Run.main$", "^Test.Wire.API.Golden.Run.main$"] + +roots = [ # may of the entries here are about general-purpose module + # interfaces that make sense as a whole, but are *currently* + # only used in part. it's possible that we should remove + # those entries here and extend the tests to cover them. + + "^API.Cargohold.getFederationAsset", # FUTUREWORK: write tests that need this! + "^API.Cargohold.uploadAssetV3", # FUTUREWORK: write tests that need this! + "^API.Galley.consentToLegalHold", # FUTUREWORK: write tests that need this! + "^API.Galley.enableLegalHold", # FUTUREWORK: write tests that need this! + "^API.Galley.getLegalHoldStatus", # FUTUREWORK: write tests that need this! + "^API.GalleyInternal.putTeamMember", + "^API.MLS.Util.clientKeyPair", + "^API.MLS.Util.getCurrentGroupId", + "^API.MLS.Util.getKeyPackageCount", + "^API.MLS.Util.getKeyPair", + "^API.Nginz.*$", # FUTUREWORK: consider using everything or cleaning up. + "^API.Search._testOrderName", + "^API.Team.Util.*$", # FUTUREWORK: Consider whether unused utility functions should be kept. + "^Bilge.*$", + "^Cannon.run$", + "^Cassandra.Helpers.toOptionFieldName", + "^Cassandra.QQ.sql$", + "^Data.ETag._OpaqueDigest", + "^Data.ETag._StrictETag", + "^Data.ETag._WeakETag", + "^Data.ETag.opaqueDigest", + "^Data.ETag.opaqueMD5", + "^Data.ETag.opaqueSHA1", + "^Data.ETag.strictETag", + "^Data.ETag.weakETag", + "^Data.Qualified.isLocal", + "^Data.Range.*$", + "^Data.Range.rappend", + "^Data.Range.rcons", + "^Data.Range.rinc", + "^Data.Range.rsingleton", + "^Data.Schema.fieldWithDocModifierF$", + "^Data.ZAuth.Validation.*$", + "^Galley.Cassandra.FeatureTH.generateSOPInstances$", + "^Galley.Cassandra.FeatureTH.generateTupleP$", + "^Galley.Types.Teams.canSeePermsOf", # TODO: figure out why weeder is confused by let bindings with curried infix notation + "^Galley.Types.UserList.ulDiff$", + "^Gundeck.Monad.runGundeck$", + "^Gundeck.run$", + "^HTTP2.Client.Manager.*$", + "^Imports.getChar", + "^Imports.getContents", + "^Imports.interact", + "^Imports.putChar", + "^Imports.readIO", + "^Imports.readLn", + "^Imports.todo", + "^Main.debugMainDebugExportFull", # move-team + "^Main.debugMainExport", # move-team + "^Main.debugMainImport", # move-team + "^Main.main$", + "^Network.Wai.Utilities.Request.parseBody$", + "^Network.Wai.Utilities.Server.route$", + "^Network.Wai.Utilities.ZAuth.*$", + "^Notifications.*$", # new integration tests + "^ParseSchema._printAllTables", + "^ParseSchema.debug", + "^ParseSchema.debugwrite", + "^ParseSchema.projectFile", + "^Paths_.*", + "^Proto.Mls_Fields.commit", + "^Proto.Mls_Fields.groupInfo", + "^Proto.Mls_Fields.groupInfoBundle", + "^Proto.Mls_Fields.groupInfoType", + "^Proto.Mls_Fields.ignoreAll", + "^Proto.Mls_Fields.ignoreOnly", + "^Proto.Mls_Fields.isInline", + "^Proto.Mls_Fields.maybe'blob", + "^Proto.Mls_Fields.maybe'ignoreAll", + "^Proto.Mls_Fields.maybe'ignoreOnly", + "^Proto.Mls_Fields.maybe'isInline", + "^Proto.Mls_Fields.maybe'nativePush", + "^Proto.Mls_Fields.maybe'reportAll", + "^Proto.Mls_Fields.maybe'reportOnly", + "^Proto.Mls_Fields.maybe'transient", + "^Proto.Mls_Fields.maybe'welcome", + "^Proto.Mls_Fields.nativePriority", + "^Proto.Mls_Fields.ratchetTreeType", + "^Proto.Mls_Fields.reportAll", + "^Proto.Mls_Fields.reportMissing", + "^Proto.Mls_Fields.reportOnly", + "^Proto.Mls_Fields.vec'clients", + "^Proto.Mls_Fields.vec'entries", + "^Proto.Mls_Fields.vec'recipients", + "^Proto.Mls_Fields.vec'reportMissing", + "^Proto.Mls_Fields.vec'userIds", + "^Proto.Mls_Fields.welcome", + "^Proto.Otr._QualifiedNewOtrMessage'IgnoreAll", + "^Proto.Otr._QualifiedNewOtrMessage'IgnoreOnly", + "^Proto.Otr._QualifiedNewOtrMessage'ReportAll", + "^Proto.Otr._QualifiedNewOtrMessage'ReportOnly", + "^Proto.Otr_Fields.ignoreAll", + "^Proto.Otr_Fields.ignoreOnly", + "^Proto.Otr_Fields.isInline", + "^Proto.Otr_Fields.maybe'blob", + "^Proto.Otr_Fields.maybe'ignoreAll", + "^Proto.Otr_Fields.maybe'ignoreOnly", + "^Proto.Otr_Fields.maybe'isInline", + "^Proto.Otr_Fields.maybe'nativePush", + "^Proto.Otr_Fields.maybe'reportAll", + "^Proto.Otr_Fields.maybe'reportOnly", + "^Proto.Otr_Fields.maybe'transient", + "^Proto.Otr_Fields.nativePriority", + "^Proto.Otr_Fields.reportAll", + "^Proto.Otr_Fields.reportMissing", + "^Proto.Otr_Fields.reportOnly", + "^Proto.Otr_Fields.vec'clients", + "^Proto.Otr_Fields.vec'entries", + "^Proto.Otr_Fields.vec'recipients", + "^Proto.Otr_Fields.vec'reportMissing", + "^Proto.Otr_Fields.vec'userIds", + "^Proto.TeamEvents_Fields.currency", + "^Proto.TeamEvents_Fields.vec'billingUser", + "^Proxy.run$", + "^Run.main$", + "^Run.main$", + "^Spar.Sem.AReqIDStore.Mem.*$", # FUTUREWORK: @fisx can we delete this? + "^Spar.Sem.AssIDStore.Mem.*$", # FUTUREWORK: @fisx can we delete this? + "^Spar.Sem.ScimTokenStore.Mem.*$", # FUTUREWORK: @fisx can we delete this? + "^Spar.Sem.VerdictFormatStore.Mem.*$", # FUTUREWORK: @fisx can we delete this? + "^Spec.main$", + "^Stern.App.runHandler$", + "^System.Logger.Extended.runWithLogger$", + "^Test.Cargohold.API.Util.shouldMatchALittle", + "^Test.Cargohold.API.Util.shouldMatchLeniently", + "^Test.Cargohold.API.Util.shouldMatchSloppily", + "^Test.Data.Schema.detailSchema", + "^Test.Data.Schema.userSchemaWithDefaultName", + "^Test.Data.Schema.userSchemaWithDefaultName'", + "^Test.Federator.JSON.deriveJSONOptions", # This is used inside an instance derivation via TH + "^Test.Wire.API.Golden.Run.main$", + "^TestSetup.runFederationClient", + "^TestSetup.viewCargohold", + "^Testlib.App.*$", # FUTUREWORK: See how we can have weeder parse operators in the config file. + "^Testlib.Cannon.awaitAtLeastNMatches", + "^Testlib.Cannon.awaitAtLeastNMatchesResult", + "^Testlib.Cannon.awaitNToMMatches", + "^Testlib.Cannon.awaitNToMMatchesResult", + "^Testlib.Cannon.prettyAwaitAtLeastResult", + "^Testlib.Cannon.printAwaitAtLeastResult", + "^Testlib.Cannon.printAwaitResult", + "^Testlib.JSON.member", + "^Testlib.JSON.printJSON", + "^Testlib.JSON.traceJSON", + "^Testlib.Prelude.*$", # FUTUREWORK: consider making them individually + "^Testlib.Printing.gray", + "^Testlib.Printing.hline", + "^Testlib.Run.main$", + "^Testlib.Run.mainI$", + "^Testlib.RunServices.main$", + "^ThreadBudget.extractLogHistory", + "^Util.SuffixNamer.*", + "^Util.assertOne", + "^Util.randomActivationCode", + "^Util.zClient", + "^Web.Scim.*$", + "^Web.Scim.Client.deleteGroup", + "^Web.Scim.Client.deleteUser", + "^Web.Scim.Client.getGroup", + "^Web.Scim.Client.getGroups", + "^Web.Scim.Client.getSchemas", + "^Web.Scim.Client.getUser", + "^Web.Scim.Client.getUsers", + "^Web.Scim.Client.patchGroup", + "^Web.Scim.Client.patchUser", + "^Web.Scim.Client.postGroup", + "^Web.Scim.Client.postUser", + "^Web.Scim.Client.putGroup", + "^Web.Scim.Client.resourceTypes", + "^Web.Scim.Client.schema", + "^Web.Scim.Client.scimClients", + "^Web.Scim.Client.scimClients", + "^Web.Scim.Client.spConfig", + "^Web.Scim.Test.Util.getField", + "^Web.Scim.Test.Util.put'", + "^Web.Scim.Test.Util.scim", + "^Web.Scim.Test.Util.shouldEventuallyRespondWith", + "^Wire.API.MLS.Serialisation.traceMLS", # Debug + "^Wire.API.Password.hashPasswordScrypt$", # Used in testing + "^Wire.API.Password.mkSafePasswordScrypt$", # Used in testing + "^Wire.Sem.Concurrency.IO.performConcurrency", + "^Wire.Sem.Logger.fatal", + "^Wire.Sem.Metrics.incGauge$" # Used in wai-utilities + ] type-class-roots = true # `root-instances` is more precise, but requires more config maintenance. # FUTUREWORK: unused-types = true