From ea4bfc110318bca78fabae41000e3a7e698a6bbb Mon Sep 17 00:00:00 2001 From: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:26:12 +0200 Subject: [PATCH 01/15] [WPB-10092] open telemetry instrumentation (#3901) * [feat] add initial otel instrumentatoin for brig and galley - add wai and rpc instrumentation for brig - add wai and rpc instrumentation for galley - create new package wire-otel that houses otel related utils * [wip] start building http2 request and response instrumentatoin * [feat] minimal support for instrumenting http/2 clients * [fix] append headers to the end * [feat] add some surrounding span context in wire api federation * [wip] instrument cannon and gundeck * [chore] add developer documentation for open telemetry instrumentation * [chore] remove http2 directive spam * [chore] revert instrumentation of http/2 requests and responeses in fed * [chore] remove spans in services that are not on our request paths * [chore] changelog entry * [chore] don't instrument requests twice (in galley and in bilge) * [chore] remove http2 stub instrumentation and add futurework instead * [chore] keep vertical export lists Co-authored-by: Igor Ranieri <54423+elland@users.noreply.github.com> * Update services/brig/src/Brig/Run.hs Co-authored-by: Igor Ranieri <54423+elland@users.noreply.github.com> * Revert "Update services/brig/src/Brig/Run.hs" This reverts commit 4c782755fa87b8a709c2cbd2d67c666f13781186. * regenerate cabal.configs, default.nixs * Fix deps in brig.cabal * Fixup * hi ci --------- Co-authored-by: Igor Ranieri <54423+elland@users.noreply.github.com> Co-authored-by: Matthias Fischmann --- cabal.project | 1 + ...instrumentation-brig-galley-gundeck-cannon | 1 + .../3-bug-fixes/remove-spam-from-nginx | 1 + .../federation-v0/nginz/conf/integration.conf | 8 +- .../federation-v1/nginz/conf/integration.conf | 8 +- .../src/developer/developer/open-telemetry.md | 47 ++ integration/test/Testlib/ModService.hs | 7 +- libs/bilge/bilge.cabal | 2 + libs/bilge/default.nix | 2 + libs/bilge/src/Bilge/RPC.hs | 11 +- .../src/Wire/API/Federation/Client.hs | 4 +- .../wire-api-federation.cabal | 1 + libs/wire-otel/CHANGELOG.md | 5 + libs/wire-otel/LICENSE | 661 ++++++++++++++++++ libs/wire-otel/default.nix | 34 + libs/wire-otel/src/Wire/OpenTelemetry.hs | 52 ++ libs/wire-otel/test/Main.hs | 4 + libs/wire-otel/wire-otel.cabal | 46 ++ libs/wire-subsystems/default.nix | 2 + libs/wire-subsystems/src/Wire/Rpc.hs | 3 +- libs/wire-subsystems/wire-subsystems.cabal | 1 + nix/haskell-pins.nix | 18 + nix/local-haskell-packages.nix | 1 + services/brig/brig.cabal | 147 ++-- services/brig/default.nix | 6 + services/brig/src/Brig/IO/Intra.hs | 2 +- services/brig/src/Brig/Provider/API.hs | 1 + services/brig/src/Brig/Provider/RPC.hs | 4 +- services/brig/src/Brig/RPC.hs | 8 +- services/brig/src/Brig/Run.hs | 26 +- services/cannon/cannon.cabal | 58 +- services/cannon/default.nix | 6 + services/cannon/src/Cannon/Run.hs | 10 +- services/galley/default.nix | 6 + services/galley/galley.cabal | 100 +-- services/galley/src/Galley/Intra/Util.hs | 2 +- services/galley/src/Galley/Run.hs | 20 +- services/gundeck/default.nix | 6 + services/gundeck/gundeck.cabal | 96 +-- .../gundeck/src/Gundeck/Push/Websocket.hs | 2 +- services/gundeck/src/Gundeck/Run.hs | 31 +- .../conf/nginz/integration.conf | 8 +- tools/stern/src/Stern/Intra.hs | 13 +- 43 files changed, 1210 insertions(+), 262 deletions(-) create mode 100644 changelog.d/2-features/open-telemetry-instrumentation-brig-galley-gundeck-cannon create mode 100644 changelog.d/3-bug-fixes/remove-spam-from-nginx create mode 100644 docs/src/developer/developer/open-telemetry.md create mode 100644 libs/wire-otel/CHANGELOG.md create mode 100644 libs/wire-otel/LICENSE create mode 100644 libs/wire-otel/default.nix create mode 100644 libs/wire-otel/src/Wire/OpenTelemetry.hs create mode 100644 libs/wire-otel/test/Main.hs create mode 100644 libs/wire-otel/wire-otel.cabal diff --git a/cabal.project b/cabal.project index d490134c847..ed3bbc74931 100644 --- a/cabal.project +++ b/cabal.project @@ -29,6 +29,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/ diff --git a/changelog.d/2-features/open-telemetry-instrumentation-brig-galley-gundeck-cannon b/changelog.d/2-features/open-telemetry-instrumentation-brig-galley-gundeck-cannon new file mode 100644 index 00000000000..9212911e115 --- /dev/null +++ b/changelog.d/2-features/open-telemetry-instrumentation-brig-galley-gundeck-cannon @@ -0,0 +1 @@ +added open telemetry instrumentation for brig, galley, gundeck and cannon diff --git a/changelog.d/3-bug-fixes/remove-spam-from-nginx b/changelog.d/3-bug-fixes/remove-spam-from-nginx new file mode 100644 index 00000000000..7167a858f0a --- /dev/null +++ b/changelog.d/3-bug-fixes/remove-spam-from-nginx @@ -0,0 +1 @@ +removed spam from nginx (nginz) by using the new style http/2 directive diff --git a/deploy/dockerephemeral/federation-v0/nginz/conf/integration.conf b/deploy/dockerephemeral/federation-v0/nginz/conf/integration.conf index 12c49ccfe88..fa168d16f4d 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 ############## # @@ -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/deploy/dockerephemeral/federation-v1/nginz/conf/integration.conf b/deploy/dockerephemeral/federation-v1/nginz/conf/integration.conf index 12c49ccfe88..fa168d16f4d 100644 --- a/deploy/dockerephemeral/federation-v1/nginz/conf/integration.conf +++ b/deploy/dockerephemeral/federation-v1/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 ############## # @@ -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/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/integration/test/Testlib/ModService.hs b/integration/test/Testlib/ModService.hs index 341c770e5fc..385a410b10b 100644 --- a/integration/test/Testlib/ModService.hs +++ b/integration/test/Testlib/ModService.hs @@ -456,9 +456,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/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 e07324e172a..182bd303488 100644 --- a/libs/bilge/src/Bilge/RPC.hs +++ b/libs/bilge/src/Bilge/RPC.hs @@ -36,9 +36,11 @@ import Control.Monad.Catch (MonadCatch, MonadThrow (..), try) 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 @@ -69,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)) @@ -81,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 -> @@ -89,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 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 bc3c56362f4..1a83e8c9adb 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Client.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Client.hs @@ -138,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/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index dc7ea88d9e8..86207e36a72 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 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/wire-otel/LICENSE b/libs/wire-otel/LICENSE new file mode 100644 index 00000000000..dba13ed2ddf --- /dev/null +++ b/libs/wire-otel/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. 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 29e6263437f..24a8758783a 100644 --- a/libs/wire-subsystems/default.nix +++ b/libs/wire-subsystems/default.nix @@ -79,6 +79,7 @@ , wai-utilities , wire-api , wire-api-federation +, wire-otel , witherable }: mkDerivation { @@ -152,6 +153,7 @@ mkDerivation { wai-utilities wire-api wire-api-federation + wire-otel witherable ]; testHaskellDepends = [ diff --git a/libs/wire-subsystems/src/Wire/Rpc.hs b/libs/wire-subsystems/src/Wire/Rpc.hs index 99f52727867..bd7b3c682ef 100644 --- a/libs/wire-subsystems/src/Wire/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/Rpc.hs @@ -43,7 +43,7 @@ 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) @@ -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/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index a544025aa7b..db0cb43facb 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -195,6 +195,7 @@ library , wai-utilities , wire-api , wire-api-federation + , wire-otel , witherable default-language: GHC2021 diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 044b1f62879..810bdbaa8f0 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -281,6 +281,24 @@ let 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 = { diff --git a/nix/local-haskell-packages.nix b/nix/local-haskell-packages.nix index 133fcd9afae..38c381258a4 100644 --- a/nix/local-haskell-packages.nix +++ b/nix/local-haskell-packages.nix @@ -31,6 +31,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; }; diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index c723695019b..6d8885cfb71 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -215,124 +215,127 @@ library -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 + , conduit >=1.2.8 + , containers >=0.5 + , cookie >=0.4 , cql - , cryptobox-haskell >=0.1.1 + , cryptobox-haskell >=0.1.1 , crypton - , currency-codes >=2.0 + , 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 + , gundeck-types >=1.32.1 + , 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 + , lens >=3.8 + , lens-aeson >=1.0 , memory - , metrics-core >=0.3 - , metrics-wai >=0.3 + , metrics-core >=0.3 + , metrics-wai >=0.3 , mime - , mime-mail >=0.4 + , mime-mail >=0.4 , mmorph - , MonadRandom >=0.5 - , mtl >=2.1 - , 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 + , resourcet >=1.1 + , retry >=0.7 + , safe-exceptions >=0.1 , saml2-web-sso , schema-profunctor , 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 + , text-icu-translit >=0.1 + , time >=1.1 , time-out , time-units - , tinylog >=0.10 - , transformers >=0.3 + , tinylog >=0.10 + , transformers >=0.3 , transitive-anns - , types-common >=0.16 + , 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 - , zauth >=0.10.3 + , zauth >=0.10.3 executable brig import: common-all diff --git a/services/brig/default.nix b/services/brig/default.nix index 03a8b688aa3..61ebf704692 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -52,6 +52,8 @@ , gitignoreSource , gundeck-types , hashable +, hs-opentelemetry-instrumentation-wai +, hs-opentelemetry-sdk , hscim , HsOpenSSL , http-api-data @@ -153,6 +155,7 @@ , warp-tls , wire-api , wire-api-federation +, wire-otel , wire-subsystems , yaml , zauth @@ -206,6 +209,8 @@ mkDerivation { galley-types gundeck-types hashable + hs-opentelemetry-instrumentation-wai + hs-opentelemetry-sdk HsOpenSSL http-client http-client-openssl @@ -280,6 +285,7 @@ mkDerivation { wai-utilities wire-api wire-api-federation + wire-otel wire-subsystems zauth ]; diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index d833def04fe..f92e18bef38 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -565,7 +565,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/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 6a36c4f5a1a..b7ed00018e1 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -825,6 +825,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/RPC.hs b/services/brig/src/Brig/Provider/RPC.hs index f8abba06133..c41193cee69 100644 --- a/services/brig/src/Brig/Provider/RPC.hs +++ b/services/brig/src/Brig/Provider/RPC.hs @@ -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/RPC.hs b/services/brig/src/Brig/RPC.hs index c421ad468d2..23105e055fe 100644 --- a/services/brig/src/Brig/RPC.hs +++ b/services/brig/src/Brig/RPC.hs @@ -41,21 +41,21 @@ 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 -> StdMethod -> @@ -66,7 +66,7 @@ serviceRequest nm svc m r = do 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 eda8c9fe88f..08486c43d31 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 @@ -59,6 +55,8 @@ 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 @@ -73,6 +71,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 @@ -81,7 +80,7 @@ import Wire.UserStore -- thread terminates for any reason. -- https://github.com/zinfra/backend-issues/issues/1647 run :: Opts -> IO () -run o = do +run o = withTracer \tracer -> do (app, e) <- mkApp o s <- Server.newSettings (server e) internalEventListener <- @@ -100,13 +99,11 @@ run o = do 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 @@ -115,7 +112,8 @@ run o = do mkApp :: Opts -> IO (Wai.Application, Env) mkApp o = do e <- newEnv o - pure (middleware e $ servantApp e, e) + otelMiddleware <- Otel.newOpenTelemetryWaiMiddleware + pure (otelMiddleware . middleware e $ servantApp e, e) where middleware :: Env -> Wai.Middleware middleware e = diff --git a/services/cannon/cannon.cabal b/services/cannon/cannon.cabal index d0af6581163..b85fd137f9d 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,47 @@ 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..db254e8b235 100644 --- a/services/cannon/default.nix +++ b/services/cannon/default.nix @@ -19,6 +19,8 @@ , gitignoreSource , gundeck-types , hashable +, hs-opentelemetry-instrumentation-wai +, hs-opentelemetry-sdk , http-types , imports , lens @@ -50,6 +52,7 @@ , warp , websockets , wire-api +, wire-otel }: mkDerivation { pname = "cannon"; @@ -72,6 +75,8 @@ mkDerivation { extra gundeck-types hashable + hs-opentelemetry-instrumentation-wai + hs-opentelemetry-sdk http-types imports lens @@ -96,6 +101,7 @@ mkDerivation { warp websockets wire-api + wire-otel ]; executableHaskellDepends = [ base imports types-common ]; testHaskellDepends = [ diff --git a/services/cannon/src/Cannon/Run.hs b/services/cannon/src/Cannon/Run.hs index fe86d2c2d8b..eefd22f4af5 100644 --- a/services/cannon/src/Cannon/Run.hs +++ b/services/cannon/src/Cannon/Run.hs @@ -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) $ @@ -77,11 +81,13 @@ run o = do 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/galley/default.nix b/services/galley/default.nix index 3cadf669e2f..38bf97f86de 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -44,6 +44,8 @@ , gitignoreSource , gundeck-types , hex +, hs-opentelemetry-instrumentation-wai +, hs-opentelemetry-sdk , HsOpenSSL , http-api-data , http-client @@ -124,6 +126,7 @@ , warp-tls , wire-api , wire-api-federation +, wire-otel , wire-subsystems , yaml }: @@ -166,6 +169,8 @@ mkDerivation { generics-sop gundeck-types hex + hs-opentelemetry-instrumentation-wai + hs-opentelemetry-sdk HsOpenSSL http-client http-client-openssl @@ -218,6 +223,7 @@ mkDerivation { wai-utilities wire-api wire-api-federation + wire-otel wire-subsystems ]; executableHaskellDepends = [ diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 790607e9bf6..f0b36539a9b 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 @@ -285,90 +286,93 @@ library 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 + , 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 + , cassandra-util >=0.16.2 + , cassava >=0.5.2 , comonad - , containers >=0.5 + , containers >=0.5 , crypton , crypton-x509 - , currency-codes >=2.0 + , currency-codes >=2.0 , data-default , data-timeout - , 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 + , extra >=1.3 + , galley-types >=0.65.0 , generics-sop - , gundeck-types >=1.35.2 + , gundeck-types >=1.35.2 , 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 + , proto-lens >=0.2 + , raw-strings-qq >=1.0 + , resourcet >=1.1 + , retry >=0.5 + , safe-exceptions >=0.1 + , saml2-web-sso >=0.20 , servant , servant-client , servant-server , singletons , singletons-base , sop-core - , split >=0.2 - , ssl-util >=0.1 - , stm >=2.4 + , split >=0.2 + , ssl-util >=0.1 + , stm >=2.4 , tagged , template-haskell - , text >=0.11 - , time >=1.4 - , tinylog >=0.10 - , tls >=1.7.0 + , 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 diff --git a/services/galley/src/Galley/Intra/Util.hs b/services/galley/src/Galley/Intra/Util.hs index 8947d4a7a4a..c7e1de20920 100644 --- a/services/galley/src/Galley/Intra/Util.hs +++ b/services/galley/src/Galley/Intra/Util.hs @@ -23,7 +23,7 @@ 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.Retry diff --git a/services/galley/src/Galley/Run.hs b/services/galley/src/Galley/Run.hs index 7d78fdcdbdf..f9374903b2d 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,9 +66,11 @@ 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 $ @@ -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/gundeck/default.nix b/services/gundeck/default.nix index dedd9cc1dab..79051e52a81 100644 --- a/services/gundeck/default.nix +++ b/services/gundeck/default.nix @@ -30,6 +30,8 @@ , gitignoreSource , gundeck-types , hedis +, hs-opentelemetry-instrumentation-wai +, hs-opentelemetry-sdk , HsOpenSSL , http-client , http-client-tls @@ -83,6 +85,7 @@ , wai-utilities , websockets , wire-api +, wire-otel , yaml }: mkDerivation { @@ -114,6 +117,8 @@ mkDerivation { foldl gundeck-types hedis + hs-opentelemetry-instrumentation-wai + hs-opentelemetry-sdk http-client http-client-tls http-types @@ -147,6 +152,7 @@ mkDerivation { wai-routing wai-utilities wire-api + wire-otel yaml ]; executableHaskellDepends = [ diff --git a/services/gundeck/gundeck.cabal b/services/gundeck/gundeck.cabal index 2c4777a19b2..cce46169df8 100644 --- a/services/gundeck/gundeck.cabal +++ b/services/gundeck/gundeck.cabal @@ -66,6 +66,7 @@ library default-extensions: AllowAmbiguousTypes BangPatterns + BlockArguments ConstraintKinds DataKinds DefaultSignatures @@ -111,62 +112,65 @@ 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 + , gundeck-types >=1.0 + , 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 >=0.5.7 + , 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-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-predicates >=0.8 + , wai-routing >=0.12 + , wai-utilities >=0.16 , wire-api - , yaml >=0.8 + , wire-otel + , yaml >=0.8 default-language: GHC2021 diff --git a/services/gundeck/src/Gundeck/Push/Websocket.hs b/services/gundeck/src/Gundeck/Push/Websocket.hs index 2a6ff64e406..1c9d4730c51 100644 --- a/services/gundeck/src/Gundeck/Push/Websocket.hs +++ b/services/gundeck/src/Gundeck/Push/Websocket.hs @@ -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/Run.hs b/services/gundeck/src/Gundeck/Run.hs index c63daf3cf68..a896171a13c 100644 --- a/services/gundeck/src/Gundeck/Run.hs +++ b/services/gundeck/src/Gundeck/Run.hs @@ -41,12 +41,15 @@ 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 @@ -55,9 +58,10 @@ 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 lastSchemaVersion @@ -69,8 +73,8 @@ run o = do 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 @@ -81,14 +85,17 @@ 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 + middleware :: Env -> IO Middleware + middleware e = do + otelMiddleWare <- newOpenTelemetryWaiMiddleware + pure $ + versionMiddleware (foldMap expandVersionExp (o ^. settings . disabledAPIVersions)) + . otelMiddleWare + . requestIdMiddleware (e ^. applog) defaultRequestIdHeaderName + . waiPrometheusMiddleware sitemap + . GZip.gunzip + . GZip.gzip GZip.def + . catchErrors (e ^. applog) defaultRequestIdHeaderName type CombinedAPI = GundeckAPI :<|> Servant.Raw 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/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index 62a230730fa..916dbd43e52 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -996,12 +996,13 @@ getOAuthClient :: OAuthClientId -> Handler OAuthClient getOAuthClient cid = do b <- view 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") From f0466088872e4bf5ef0ca146bdf52e14eebb8443 Mon Sep 17 00:00:00 2001 From: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> Date: Wed, 18 Sep 2024 13:38:07 +0200 Subject: [PATCH 02/15] [WPB-10772] Make it impossible for a user under legalhold to join an MLS conversation (#4242) * [feat] add two tests for scenarios with MLS + legalhold * [feat] don't allow claiming key packages of users under legalhold - new client error which returns 409 if someone (client or federating backend) tries to claim a key package of a user under legalhold (including pending) - add Fail effect to Brig and interpret to IO Error (server error 500) * [feat] cater for the scenario when a user doesn't claim key packages - if a user doesn't claim key packages (because they create the group by themselves) the local backend checks whether that user is legalheld and rejects the commit in that case - also adds some utilities (e.g. pattern synonyms fo Local and Remote) and propagates the Fail constraint - introduce new type RelativeTo --- changelog.d/2-features/WPB-10772 | 5 ++ integration/test/Test/LegalHold.hs | 70 +++++++++++++++++ libs/types-common/src/Data/Qualified.hs | 24 ++++-- .../src/Wire/API/Federation/API.hs | 3 +- libs/wire-api/src/Wire/API/Error.hs | 5 ++ libs/wire-api/src/Wire/API/Error/Galley.hs | 5 ++ .../src/Wire/API/Routes/Public/Galley/MLS.hs | 57 +++++++------- .../src/Wire/GalleyAPIAccess.hs | 3 + .../src/Wire/GalleyAPIAccess/Rpc.hs | 20 +++++ services/brig/src/Brig/API/Error.hs | 1 + services/brig/src/Brig/API/Federation.hs | 4 +- services/brig/src/Brig/API/MLS/KeyPackages.hs | 33 ++++++++ services/brig/src/Brig/API/Public.hs | 2 + services/brig/src/Brig/API/Types.hs | 3 + services/brig/src/Brig/App.hs | 4 + .../brig/src/Brig/CanonicalInterpreter.hs | 3 + services/galley/galley.cabal | 1 + services/galley/src/Galley/API/Federation.hs | 23 +++--- services/galley/src/Galley/API/LegalHold.hs | 41 +--------- .../galley/src/Galley/API/LegalHold/Get.hs | 78 +++++++++++++++++++ services/galley/src/Galley/API/MLS/Message.hs | 35 ++++++++- services/galley/src/Galley/App.hs | 3 + .../galley/src/Galley/Effects/TeamStore.hs | 2 + 23 files changed, 340 insertions(+), 85 deletions(-) create mode 100644 changelog.d/2-features/WPB-10772 create mode 100644 services/galley/src/Galley/API/LegalHold/Get.hs diff --git a/changelog.d/2-features/WPB-10772 b/changelog.d/2-features/WPB-10772 new file mode 100644 index 00000000000..97dd0b3286b --- /dev/null +++ b/changelog.d/2-features/WPB-10772 @@ -0,0 +1,5 @@ +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). diff --git a/integration/test/Test/LegalHold.hs b/integration/test/Test/LegalHold.hs index c948bccb649..f359d54d2c3 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) @@ -904,3 +905,72 @@ testLHDisableBeforeApproval = do disableLegalHold tid alice bob defPassword >>= assertStatus 200 getBob'sStatus `shouldMatch` "disabled" + +-- --------- +-- 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 +testLegalholdThenMLSThirdParty :: (HasCallStack) => App () +testLegalholdThenMLSThirdParty = 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 +testLegalholdThenMLSSelf :: (HasCallStack) => App () +testLegalholdThenMLSSelf = do + (alice, tid, [charlie]) <- createTeam OwnDomain 2 + [alice1, charlie1] <- traverse (createMLSClient def) [alice, charlie] + _ <- uploadNewKeyPackage 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" + + -- 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" diff --git a/libs/types-common/src/Data/Qualified.hs b/libs/types-common/src/Data/Qualified.hs index 8b06c4ea58f..0dbc73f99ec 100644 --- a/libs/types-common/src/Data/Qualified.hs +++ b/libs/types-common/src/Data/Qualified.hs @@ -31,6 +31,7 @@ module Data.Qualified tSplit, qTagUnsafe, Remote, + RelativeTo (Remote, Local, RelativeTo), toRemoteUnsafe, Local, toLocalUnsafe, @@ -121,11 +122,24 @@ qualifyAs :: QualifiedWithTag t x -> a -> QualifiedWithTag t a qualifyAs = ($>) foldQualified :: Local x -> (Local a -> b) -> (Remote a -> b) -> Qualified a -> b -foldQualified loc f g q - | tDomain loc == qDomain q = - f (qTagUnsafe q) - | otherwise = - g (qTagUnsafe q) +foldQualified loc kLocal kRemote q = case q `RelativeTo` loc of + Local l -> kLocal l + Remote r -> kRemote r + +data a `RelativeTo` x = Qualified a `RelativeTo` Local x + +checkRelative :: a `RelativeTo` x -> Either (Local a) (Remote a) +checkRelative (q `RelativeTo` loc) + | tDomain loc == qDomain q = Left (qTagUnsafe q) + | otherwise = Right (qTagUnsafe q) + +pattern Local :: forall a x. Local a -> a `RelativeTo` x +pattern Local loc <- (checkRelative -> Left loc) + +pattern Remote :: forall a x. Remote a -> a `RelativeTo` x +pattern Remote rem <- (checkRelative -> Right rem) + +{-# COMPLETE Local, Remote #-} -- Partition a collection of qualified values into locals and remotes. -- 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 1c45da47edf..ba46921b04c 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API.hs @@ -29,7 +29,7 @@ module Wire.API.Federation.API fedQueueClient, sendBundle, fedClientIn, - module Wire.API.MakesFederatedCall, + module X, -- * Re-exports Component (..), @@ -59,6 +59,7 @@ import Wire.API.Federation.Endpoint import Wire.API.Federation.HasNotificationEndpoint import Wire.API.Federation.Version import Wire.API.MakesFederatedCall +import Wire.API.MakesFederatedCall as X hiding (Location (..)) import Wire.API.Routes.Named -- Note: this type family being injective means that in most cases there is no need diff --git a/libs/wire-api/src/Wire/API/Error.hs b/libs/wire-api/src/Wire/API/Error.hs index f37711ac06f..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 @@ -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/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index 22ad24e1c2d..a7bd372b9f1 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -105,6 +105,9 @@ data GalleyError | 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 @@ -256,6 +259,8 @@ type instance MapError 'MLSMigrationCriteriaNotSatisfied = 'StaticError 400 "mls 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" 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 ccace964c21..347bc01158d 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 @@ -72,33 +72,34 @@ 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 + :> 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 + :> CanThrow MLSLegalholdIncompatible :> CanThrow MLSProposalFailure :> CanThrow NonFederatingBackends :> CanThrow UnreachableBackends @@ -107,7 +108,7 @@ type MLSMessagingAPI = :> ZClient :> ZConn :> ReqBody '[MLS] (RawMLS CommitBundle) - :> MultiVerb1 'POST '[JSON] (Respond 201 "Commit accepted and forwarded" MLSMessageSendingStatus) + :> MultiVerb1 POST '[JSON] (Respond 201 "Commit accepted and forwarded" MLSMessageSendingStatus) ) :<|> Named "mls-public-keys" diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs index e129fb5bc2c..cbb4f769837 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 @@ -94,6 +95,8 @@ data GalleyAPIAccess m a where GetTeamLegalHoldStatus :: TeamId -> GalleyAPIAccess m (LockableFeature LegalholdConfig) + GetUserLegalholdStatus :: + Local UserId -> TeamId -> GalleyAPIAccess m UserLegalHoldStatusResponse GetTeamSearchVisibility :: TeamId -> GalleyAPIAccess m TeamSearchVisibility diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs index aa9dcb4dc9e..dcafabedce8 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 @@ -80,6 +81,7 @@ interpretGalleyAPIAccessToRpc disabledVersions galleyEndpoint = GetTeamName id' -> getTeamName id' GetTeamLegalHoldStatus id' -> getTeamLegalHoldStatus id' GetTeamSearchVisibility id' -> getTeamSearchVisibility id' + GetUserLegalholdStatus id' tid -> getUserLegalholdStatus id' tid ChangeTeamStatus id' ts m_al -> changeTeamStatus id' ts m_al MemberIsTeamOwner id' id'' -> memberIsTeamOwner id' id'' GetAllTeamFeaturesForUser m_id' -> getAllTeamFeaturesForUser m_id' @@ -89,6 +91,24 @@ interpretGalleyAPIAccessToRpc disabledVersions galleyEndpoint = 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 diff --git a/services/brig/src/Brig/API/Error.hs b/services/brig/src/Brig/API/Error.hs index 5f01495d8de..019e3786c1b 100644 --- a/services/brig/src/Brig/API/Error.hs +++ b/services/brig/src/Brig/API/Error.hs @@ -133,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) diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index 58af99451bf..370761fb73f 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -52,6 +52,7 @@ import Gundeck.Types.Push qualified as Push import Imports hiding ((\\)) import Network.Wai.Utilities.Error ((!>>)) import Polysemy +import Polysemy.Fail (Fail) import Servant (ServerT) import Servant.API import Wire.API.Connection @@ -87,6 +88,7 @@ federationSitemap :: Member NotificationSubsystem r, Member UserSubsystem r, Member UserStore r, + Member Fail r, Member DeleteQueue r ) => ServerT FederationAPI (Handler r) @@ -193,7 +195,7 @@ claimMultiPrekeyBundle :: Handler r UserClientPrekeyMap claimMultiPrekeyBundle _ uc = API.claimLocalMultiPrekeyBundles LegalholdPlusFederationNotImplemented uc !>> clientError -fedClaimKeyPackages :: Domain -> ClaimKeyPackageRequest -> Handler r (Maybe KeyPackageBundle) +fedClaimKeyPackages :: (Member Fail r, Member GalleyAPIAccess r, Member UserStore r) => Domain -> ClaimKeyPackageRequest -> Handler r (Maybe KeyPackageBundle) fedClaimKeyPackages domain ckpr = isMLSEnabled >>= \case True -> do diff --git a/services/brig/src/Brig/API/MLS/KeyPackages.hs b/services/brig/src/Brig/API/MLS/KeyPackages.hs index d27fc78db78..33dcbcc90a1 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages.hs @@ -40,9 +40,12 @@ 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 Polysemy.Fail (Fail) import Wire.API.Federation.API import Wire.API.Federation.API.Brig import Wire.API.MLS.CipherSuite @@ -51,6 +54,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, getUsers) uploadKeyPackages :: Local UserId -> ClientId -> KeyPackageUpload -> Handler r () uploadKeyPackages lusr cid kps = do @@ -60,6 +66,7 @@ uploadKeyPackages lusr cid kps = do lift . wrapClient $ Data.insertKeyPackages (tUnqualified lusr) cid kps' claimKeyPackages :: + (Member GalleyAPIAccess r, Member UserStore r, Member Fail r) => Local UserId -> Maybe ClientId -> Qualified UserId -> @@ -67,6 +74,7 @@ claimKeyPackages :: Handler r KeyPackageBundle claimKeyPackages lusr mClient target mSuite = do assertMLSEnabled + suite <- getCipherSuite mSuite foldQualified lusr @@ -75,12 +83,22 @@ claimKeyPackages lusr mClient target mSuite = do target claimLocalKeyPackages :: + forall r. + (Member GalleyAPIAccess r, Member UserStore r, Member Fail 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 legalholded user to an MLS + -- conversation + assertUserNotLegalholded + -- 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 +121,21 @@ claimLocalKeyPackages qusr skipOwn suite target = do uncurry (KeyPackageBundleEntry (tUntagged target) c) <$> wrapClientM (Data.claimKeyPackage target c suite) + assertUserNotLegalholded :: ExceptT ClientError (AppT r) () + assertUserNotLegalholded = do + -- this is okay because there can only be one StoredUser per UserId + [su] <- lift $ liftSem $ getUsers [tUnqualified target] + 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 -> diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 2ab050fc306..f3dc6426492 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -93,6 +93,7 @@ import Imports hiding (head) import Network.Socket (PortNumber) import Network.Wai.Utilities as Utilities import Polysemy +import Polysemy.Fail (Fail) import Polysemy.Input (Input) import Polysemy.TinyLog (TinyLog) import Servant hiding (Handler, JSON, addHeader, respond) @@ -268,6 +269,7 @@ servantSitemap :: Member (ConnectionStore InternalPaging) r, Member (Embed HttpClientIO) r, Member (Embed IO) r, + Member Fail r, Member FederationConfigStore r, Member (Input (Local ())) r, Member AuthenticationSubsystem r, diff --git a/services/brig/src/Brig/API/Types.hs b/services/brig/src/Brig/API/Types.hs index 076b844e1fc..8eac26e9861 100644 --- a/services/brig/src/Brig/API/Types.hs +++ b/services/brig/src/Brig/API/Types.hs @@ -176,6 +176,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/App.hs b/services/brig/src/Brig/App.hs index f08502c94cc..3d85954f241 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -138,6 +138,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 @@ -488,6 +489,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 diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index fb6d1643cfe..65cf6132c0d 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -28,6 +28,7 @@ import Polysemy.Async import Polysemy.Conc import Polysemy.Embed (runEmbedded) import Polysemy.Error (Error, errorToIOFinal, mapError, runError) +import Polysemy.Fail import Polysemy.Input (Input, runInputConst, runInputSem) import Polysemy.TinyLog (TinyLog) import Wire.API.Allowlists (AllowlistEmailDomains) @@ -143,6 +144,7 @@ type BrigCanonicalEffects = Error SomeException, TinyLog, Embed HttpClientIO, + Fail, Embed IO, Race, Async, @@ -176,6 +178,7 @@ runBrigToIO e (AppT ma) = do . asyncToIOFinal . interpretRace . embedToFinal + . failToEmbed @IO -- if a fallible pattern fails, we throw a hard IO error . runEmbedded (runHttpClientIO e) . loggerToTinyLogReqId (e ^. App.requestId) (e ^. applog) . runError @SomeException diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index f0b36539a9b..0fb76ff1384 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -82,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 diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 8ca7057686a..5d23e68b499 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -67,6 +67,7 @@ import Gundeck.Types.Push.V2 (RecipientClients (..)) import Imports import Polysemy import Polysemy.Error +import Polysemy.Fail (Fail) import Polysemy.Input import Polysemy.Internal.Kind (Append) import Polysemy.Resource @@ -605,6 +606,7 @@ sendMLSCommitBundle :: Member (Input UTCTime) r, Member LegalHoldStore r, Member MemberStore r, + Member Fail r, Member Resource r, Member TeamStore r, Member P.TinyLog r, @@ -626,15 +628,18 @@ 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 + Just resp <- + runErrorS @MLSLegalholdIncompatible $ + postMLSCommitBundle + loc + (tUntagged @QRemote sender) + msr.senderClient + ctype + qConvOrSub + Nothing + ibundle + pure $ MLSMessageResponseUpdates $ map lcuUpdate resp sendMLSMessage :: ( Member BackendNotificationQueueAccess r, diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index 75eceeec319..cd2227ff4e3 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) @@ -290,44 +291,6 @@ removeSettings' tid = LHService.removeLegalHold tid (tUnqualified 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). 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/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index b5eef0766c4..f3c43774b29 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 (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,9 +60,11 @@ import Galley.Effects.ConversationStore import Galley.Effects.FederatorAccess import Galley.Effects.MemberStore import Galley.Effects.SubConversationStore +import Galley.Effects.TeamStore (getUserTeams) import Imports import Polysemy import Polysemy.Error +import Polysemy.Fail import Polysemy.Input import Polysemy.Internal import Polysemy.Output @@ -81,6 +85,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 @@ -148,6 +153,8 @@ postMLSMessageFromLocalUser lusr c conn smsg = do postMLSCommitBundle :: ( HasProposalEffects r, Members MLSBundleStaticErrors r, + Member Fail r, + Member (ErrorS MLSLegalholdIncompatible) r, Member Random r, Member Resource r, Member SubConversationStore r @@ -171,6 +178,8 @@ postMLSCommitBundleFromLocalUser :: ( HasProposalEffects r, Members MLSBundleStaticErrors r, Member Random r, + Member Fail r, + Member (ErrorS MLSLegalholdIncompatible) r, Member Resource r, Member SubConversationStore r ) => @@ -193,8 +202,10 @@ postMLSCommitBundleToLocalConv :: ( HasProposalEffects r, Members MLSBundleStaticErrors r, Member Resource r, + Member (ErrorS MLSLegalholdIncompatible) r, Member SubConversationStore r, - Member Random r + Member Random r, + Member Fail r ) => Qualified UserId -> ClientId -> @@ -211,6 +222,26 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do note (mlsProtocolError "Unsupported ciphersuite") $ cipherSuiteTag bundle.groupInfo.value.groupContext.cipherSuite + -- when a user tries to join any mls conversation while being legalholded + -- 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 <- 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 + Right resp <- runError @(Tagged TeamMemberNotFound ()) $ getUserStatus luid tid (tUnqualified luid) + case resp.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 -> pure True diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index a7488032814..baa3284e861 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -92,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 @@ -124,6 +125,7 @@ type GalleyEffects0 = Error FederationError, Async, Delay, + Fail, Embed IO, Error JSONResponse, Resource, @@ -243,6 +245,7 @@ evalGalley e = . resourceToIOFinal . runError . embedToFinal @IO + . failToEmbed @IO . runDelay . asyncToIOFinal . mapError toResponse diff --git a/services/galley/src/Galley/Effects/TeamStore.hs b/services/galley/src/Galley/Effects/TeamStore.hs index 6c6eca8de17..6ce47062720 100644 --- a/services/galley/src/Galley/Effects/TeamStore.hs +++ b/services/galley/src/Galley/Effects/TeamStore.hs @@ -125,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) From a72c70a9a9b9d71af1b864384e80b6d3eb0827a9 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Wed, 18 Sep 2024 14:40:34 +0200 Subject: [PATCH 03/15] Work around legacy integration test resource leak. (#4244) --- services/brig/test/integration/Run.hs | 28 +++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/services/brig/test/integration/Run.hs b/services/brig/test/integration/Run.hs index 1b3e0cd563d..f804d364896 100644 --- a/services/brig/test/integration/Run.hs +++ b/services/brig/test/integration/Run.hs @@ -54,6 +54,7 @@ import Options.Applicative hiding (action) import SMTP qualified import System.Environment (withArgs) import System.Logger qualified as Logger +import System.Mem (performGC) import Test.Tasty import Test.Tasty.Ingredients import Test.Tasty.Runners @@ -150,16 +151,14 @@ runTests iConf brigOpts otherArgs = do let smtp = SMTP.tests mg lg oauthAPI = API.OAuth.tests mg db b n brigOpts + -- run the tests in two parts, with a gc in between. i did this on a hunch, and for some + -- reason this reduces the hunger for open file handles at run time significantly, and makes + -- the suite pass with my ulimit settings. (fisx) + withArgs otherArgs . defaultMainWithIngredients (listingTests : (composeReporters antXMLRunner consoleTestReporter) : defaultIngredients) $ testGroup - "Brig API Integration" - $ [ userApi, - providerApi, - searchApis, - teamApis, - turnApi, - metricsApi, - systemSettingsApi, + "Brig API Integration, part 1" + $ [ systemSettingsApi, settingsApi, createIndex, userPendingActivation, @@ -170,6 +169,19 @@ runTests iConf brigOpts otherArgs = do oauthAPI, federationEnd2End ] + + performGC + + withArgs otherArgs . defaultMainWithIngredients (listingTests : (composeReporters antXMLRunner consoleTestReporter) : defaultIngredients) + $ testGroup + "Brig API Integration, part 2" + $ [ userApi, + providerApi, + searchApis, + teamApis, + turnApi, + metricsApi + ] where mkRequest (Endpoint h p) = Bilge.host (encodeUtf8 h) . Bilge.port p From 094d6be91339fab57212a7fe54c845f3c508b91a Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Wed, 18 Sep 2024 14:49:28 +0200 Subject: [PATCH 04/15] Fix FromJSON AmqpEndpoint error message (#4248) Use the name defined by our convention to get better error messages. --- changelog.d/5-internal/background-worker | 2 +- libs/extended/src/Network/AMQP/Extended.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.d/5-internal/background-worker b/changelog.d/5-internal/background-worker index d699ef088a6..35afaff745f 100644 --- a/changelog.d/5-internal/background-worker +++ b/changelog.d/5-internal/background-worker @@ -1 +1 @@ -charts/wire-server: Deploy background-worker even when tags.federation is `false` +charts/wire-server: Deploy background-worker even when tags.federation is `false` (#4342, #4248) diff --git a/libs/extended/src/Network/AMQP/Extended.hs b/libs/extended/src/Network/AMQP/Extended.hs index 3d1e79a218b..955e54c0a33 100644 --- a/libs/extended/src/Network/AMQP/Extended.hs +++ b/libs/extended/src/Network/AMQP/Extended.hs @@ -114,7 +114,7 @@ data AmqpEndpoint = AmqpEndpoint deriving (Show) instance FromJSON AmqpEndpoint where - parseJSON = withObject "RabbitMqAdminOpts" $ \v -> + parseJSON = withObject "AmqpEndpoint" $ \v -> AmqpEndpoint <$> v .: "host" <*> v .: "port" From 708c07af82366edeea01ac180520c4aa193bf19c Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Wed, 18 Sep 2024 15:23:18 +0200 Subject: [PATCH 05/15] [WPB-1228] Servantify gundeck internal api (#4246) * [WPB-1228] Servantify gundeck internal api * Fix: make cannon use Content-Type header when posting presence. * Make gundeck backwards compatible with previous cannon behavior. --- .../WPB-1228-servantify-gundeck-internal-api | 1 + libs/gundeck-types/default.nix | 2 + libs/gundeck-types/gundeck-types.cabal | 1 + .../gundeck-types/src/Gundeck/Types/Common.hs | 8 + services/brig/src/Brig/Run.hs | 1 - services/cannon/src/Cannon/WS.hs | 2 +- services/gundeck/default.nix | 8 +- services/gundeck/gundeck.cabal | 12 +- services/gundeck/src/Gundeck/API.hs | 29 ---- services/gundeck/src/Gundeck/API/Internal.hs | 157 +++++++++++------- services/gundeck/src/Gundeck/Presence.hs | 41 ++--- services/gundeck/src/Gundeck/Run.hs | 23 +-- services/gundeck/src/Gundeck/Util.hs | 3 - services/gundeck/test/unit/Main.hs | 12 +- 14 files changed, 139 insertions(+), 161 deletions(-) create mode 100644 changelog.d/5-internal/WPB-1228-servantify-gundeck-internal-api delete mode 100644 services/gundeck/src/Gundeck/API.hs diff --git a/changelog.d/5-internal/WPB-1228-servantify-gundeck-internal-api b/changelog.d/5-internal/WPB-1228-servantify-gundeck-internal-api new file mode 100644 index 00000000000..477a424b664 --- /dev/null +++ b/changelog.d/5-internal/WPB-1228-servantify-gundeck-internal-api @@ -0,0 +1 @@ +Servantify gundeck internal api diff --git a/libs/gundeck-types/default.nix b/libs/gundeck-types/default.nix index 522b4e84b10..6c440616b9a 100644 --- a/libs/gundeck-types/default.nix +++ b/libs/gundeck-types/default.nix @@ -14,6 +14,7 @@ , lens , lib , network-uri +, servant , text , types-common , wire-api @@ -32,6 +33,7 @@ mkDerivation { imports lens network-uri + servant text types-common wire-api diff --git a/libs/gundeck-types/gundeck-types.cabal b/libs/gundeck-types/gundeck-types.cabal index 26e75b33b7f..86c33f01a4a 100644 --- a/libs/gundeck-types/gundeck-types.cabal +++ b/libs/gundeck-types/gundeck-types.cabal @@ -78,6 +78,7 @@ library , imports , lens >=4.11 , network-uri >=2.6 + , servant , text >=0.11 , types-common >=0.16 , wire-api diff --git a/libs/gundeck-types/src/Gundeck/Types/Common.hs b/libs/gundeck-types/src/Gundeck/Types/Common.hs index 1158830d1c8..6649db94ba3 100644 --- a/libs/gundeck-types/src/Gundeck/Types/Common.hs +++ b/libs/gundeck-types/src/Gundeck/Types/Common.hs @@ -24,8 +24,10 @@ import Data.Attoparsec.ByteString (takeByteString) import Data.ByteString.Char8 qualified as Bytes import Data.ByteString.Conversion import Data.Text qualified as Text +import Data.Text.Encoding (decodeUtf8) import Imports import Network.URI qualified as Net +import Servant.API (FromHttpApiData (parseUrlPiece), ToHttpApiData (toUrlPiece)) newtype CannonId = CannonId { cannonId :: Text @@ -40,6 +42,9 @@ newtype CannonId = CannonId ToByteString ) +instance FromHttpApiData CannonId where + parseUrlPiece = pure . CannonId + newtype URI = URI { fromURI :: Net.URI } @@ -57,5 +62,8 @@ instance ToByteString URI where instance FromByteString URI where parser = takeByteString >>= parse . Bytes.unpack +instance ToHttpApiData URI where + toUrlPiece = decodeUtf8 . toByteString' + parse :: (MonadFail m) => String -> m URI parse = maybe (fail "Invalid URI") (pure . URI) . Net.parseURI diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index 08486c43d31..3be27a1c937 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -126,7 +126,6 @@ mkApp o = do . GZip.gzip GZip.def . catchErrors (e ^. applog) defaultRequestIdHeaderName - -- the servant API wraps the one defined using wai-routing servantApp :: Env -> Wai.Application servantApp e0 req cont = do let rid = getRequestId defaultRequestIdHeaderName req diff --git a/services/cannon/src/Cannon/WS.hs b/services/cannon/src/Cannon/WS.hs index 2b9a816df20..f551434b599 100644 --- a/services/cannon/src/Cannon/WS.hs +++ b/services/cannon/src/Cannon/WS.hs @@ -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/gundeck/default.nix b/services/gundeck/default.nix index 79051e52a81..d797346cf43 100644 --- a/services/gundeck/default.nix +++ b/services/gundeck/default.nix @@ -61,6 +61,7 @@ , safe , safe-exceptions , scientific +, servant , servant-server , string-conversions , tagged @@ -80,8 +81,6 @@ , wai , wai-extra , wai-middleware-gunzip -, wai-predicates -, wai-routing , wai-utilities , websockets , wire-api @@ -135,6 +134,7 @@ mkDerivation { resourcet retry safe-exceptions + servant servant-server text time @@ -148,8 +148,6 @@ mkDerivation { wai wai-extra wai-middleware-gunzip - wai-predicates - wai-routing wai-utilities wire-api wire-otel @@ -210,7 +208,6 @@ mkDerivation { HsOpenSSL imports lens - metrics-wai MonadRandom mtl multiset @@ -226,7 +223,6 @@ mkDerivation { text tinylog types-common - wai-utilities wire-api ]; benchmarkHaskellDepends = [ diff --git a/services/gundeck/gundeck.cabal b/services/gundeck/gundeck.cabal index cce46169df8..b2e14c44908 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 @@ -143,7 +142,7 @@ library , lens >=4.4 , lens-aeson >=1.0 , metrics-core >=0.2.1 - , metrics-wai >=0.5.7 + , metrics-wai , mtl >=2.2 , network-uri >=2.6 , prometheus-client @@ -152,6 +151,7 @@ library , resourcet >=1.1 , retry >=0.5 , safe-exceptions + , servant , servant-server , text >=1.1 , time >=1.4 @@ -165,8 +165,6 @@ library , 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 , wire-api , wire-otel @@ -297,7 +295,7 @@ executable gundeck-integration build-depends: , aeson , async - , base >=4 && <5 + , base >=4 && <5 , base16-bytestring >=0.1 , bilge , bytestring @@ -328,7 +326,7 @@ executable gundeck-integration , tinylog , types-common , uuid - , wai-utilities + , wai-utilities >=0.16 , websockets >=0.8 , wire-api , yaml @@ -546,7 +544,6 @@ test-suite gundeck-tests , HsOpenSSL , imports , lens - , metrics-wai , MonadRandom , mtl , multiset @@ -562,7 +559,6 @@ test-suite gundeck-tests , text , tinylog , types-common - , wai-utilities , wire-api default-language: GHC2021 diff --git a/services/gundeck/src/Gundeck/API.hs b/services/gundeck/src/Gundeck/API.hs deleted file mode 100644 index eca27d6cfed..00000000000 --- a/services/gundeck/src/Gundeck/API.hs +++ /dev/null @@ -1,29 +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 Gundeck.API - ( sitemap, - ) -where - -import Gundeck.API.Internal qualified as Internal -import Gundeck.Monad (Gundeck) -import Network.Wai.Routing (Routes) - -sitemap :: Routes () Gundeck () -sitemap = do - Internal.sitemap diff --git a/services/gundeck/src/Gundeck/API/Internal.hs b/services/gundeck/src/Gundeck/API/Internal.hs index 357d49bfe83..49c2448bd3a 100644 --- a/services/gundeck/src/Gundeck/API/Internal.hs +++ b/services/gundeck/src/Gundeck/API/Internal.hs @@ -16,76 +16,113 @@ -- with this program. If not, see . module Gundeck.API.Internal - ( sitemap, + ( type GundeckInternalAPI, + servantSitemap, ) where import Cassandra qualified import Control.Lens (view) +import Data.Aeson (eitherDecode) +import Data.CommaSeparatedList import Data.Id +import Data.Metrics.Servant +import Data.Typeable import Gundeck.Client qualified as Client import Gundeck.Monad 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 Gundeck.Types.Presence as GD +import Gundeck.Types.Push.V2 +import Imports +import Network.Wai (lazyRequestBody) +import Servant +import Servant.Server.Internal.Delayed +import Servant.Server.Internal.DelayedIO +import Servant.Server.Internal.ErrorFormatter 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.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. +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 rest) => + RoutesToPaths (ReqBodyHack :> rest) + where + getRoutes = getRoutes @rest + +type GundeckInternalAPI = + "i" + :> ( ("status" :> Get '[JSON] NoContent) + :<|> ("push" :> "v2" :> ReqBody '[JSON] [Push] :> Post '[JSON] NoContent) + :<|> ( "presences" + :> ( (QueryParam' [Required, Strict] "ids" (CommaSeparatedList UserId) :> Get '[JSON] [Presence]) + :<|> (Capture "uid" UserId :> Get '[JSON] [Presence]) + :<|> (ReqBodyHack :> Verb 'POST 201 '[JSON] (Headers '[Header "Location" GD.URI] NoContent)) + :<|> (Capture "uid" UserId :> "devices" :> Capture "did" ConnId :> "cannons" :> Capture "cannon" CannonId :> Delete '[JSON] NoContent) + ) + ) + :<|> (ZUser :> "clients" :> Capture "cid" ClientId :> Delete '[JSON] NoContent) + :<|> (ZUser :> "user" :> Delete '[JSON] NoContent) + :<|> ("push-tokens" :> Capture "uid" UserId :> Get '[JSON] PushTokenList) + ) + +servantSitemap :: ServerT GundeckInternalAPI Gundeck +servantSitemap = + statusH + :<|> pushH + :<|> ( Presence.listAllH + :<|> Presence.listH + :<|> Presence.addH + :<|> Presence.removeH + ) + :<|> unregisterClientH + :<|> removeUserH + :<|> 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/Presence.hs b/services/gundeck/src/Gundeck/Presence.hs index 4c626fe35ee..ed69bb50515 100644 --- a/services/gundeck/src/Gundeck/Presence.hs +++ b/services/gundeck/src/Gundeck/Presence.hs @@ -16,42 +16,31 @@ -- 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 -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" Gundeck.Types.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/Run.hs b/services/gundeck/src/Gundeck/Run.hs index a896171a13c..0f8c7a13fd4 100644 --- a/services/gundeck/src/Gundeck/Run.hs +++ b/services/gundeck/src/Gundeck/Run.hs @@ -27,12 +27,12 @@ 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 (GundeckInternalAPI, servantSitemap) +import Gundeck.API.Public as Public (servantSitemap) import Gundeck.Aws qualified as Aws import Gundeck.Env import Gundeck.Env qualified as Env @@ -92,28 +92,19 @@ run o = withTracer \tracer -> do versionMiddleware (foldMap expandVersionExp (o ^. settings . disabledAPIVersions)) . otelMiddleWare . requestIdMiddleware (e ^. applog) defaultRequestIdHeaderName - . waiPrometheusMiddleware sitemap + . Metrics.servantPrometheusMiddleware (Proxy @(GundeckAPI :<|> GundeckInternalAPI)) . GZip.gunzip . GZip.gzip GZip.def . catchErrors (e ^. applog) defaultRequestIdHeaderName -type CombinedAPI = GundeckAPI :<|> Servant.Raw - 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 :<|> GundeckInternalAPI)) (servantSitemap' env) req cont -servantSitemap' :: Env -> Servant.Server GundeckAPI -servantSitemap' env = Servant.hoistServer (Proxy @GundeckAPI) toServantHandler servantSitemap +servantSitemap' :: Env -> Servant.Server (GundeckAPI :<|> GundeckInternalAPI) +servantSitemap' env = Servant.hoistServer (Proxy @(GundeckAPI :<|> GundeckInternalAPI)) 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 5bc0e77f724..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 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, From dee9f3fcca9e478492f86ab2bb6ae48bb5cd6d3b Mon Sep 17 00:00:00 2001 From: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:06:01 +0200 Subject: [PATCH 06/15] [WPB-10783] Prevent MLS-Legalhold interactions (#4245) Co-authored-by: Akshay Mankar Co-authored-by: Igor Ranieri --- changelog.d/2-features/block-lh-for-mls-users | 1 + integration/test/Test/LegalHold.hs | 26 ++++++++++++++++--- .../API/Routes/Public/Galley/LegalHold.hs | 1 + services/galley/src/Galley/API/LegalHold.hs | 9 +++++++ 4 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 changelog.d/2-features/block-lh-for-mls-users diff --git a/changelog.d/2-features/block-lh-for-mls-users b/changelog.d/2-features/block-lh-for-mls-users new file mode 100644 index 00000000000..cc86b5c4512 --- /dev/null +++ b/changelog.d/2-features/block-lh-for-mls-users @@ -0,0 +1 @@ +Deny requests for a legalhold device for users who are part of any MLS conversations \ No newline at end of file diff --git a/integration/test/Test/LegalHold.hs b/integration/test/Test/LegalHold.hs index f359d54d2c3..4b70fd0d454 100644 --- a/integration/test/Test/LegalHold.hs +++ b/integration/test/Test/LegalHold.hs @@ -906,6 +906,24 @@ testLHDisableBeforeApproval = do >>= 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 -- --------- @@ -913,8 +931,8 @@ testLHDisableBeforeApproval = do -- | 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 -testLegalholdThenMLSThirdParty :: (HasCallStack) => App () -testLegalholdThenMLSThirdParty = do +testBlockClaimingKeyPackageForLHUsers :: (HasCallStack) => App () +testBlockClaimingKeyPackageForLHUsers = do (alice, tid, [charlie]) <- createTeam OwnDomain 2 [alice1, charlie1] <- traverse (createMLSClient def) [alice, charlie] _ <- uploadNewKeyPackage charlie1 @@ -937,8 +955,8 @@ testLegalholdThenMLSThirdParty = do -- 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 -testLegalholdThenMLSSelf :: (HasCallStack) => App () -testLegalholdThenMLSSelf = do +testBlockCreateMLSConvForLHUsers :: (HasCallStack) => App () +testBlockCreateMLSConvForLHUsers = do (alice, tid, [charlie]) <- createTeam OwnDomain 2 [alice1, charlie1] <- traverse (createMLSClient def) [alice, charlie] _ <- uploadNewKeyPackage alice1 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 a9d7ebe219d..f4506b7fcfb 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 @@ -133,6 +133,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\ diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index cd2227ff4e3..3c15d5c1e53 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -68,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 @@ -345,6 +346,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, @@ -392,6 +394,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 @@ -401,6 +409,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) From 7bad42cf4590bc9fb972bb73d573c5bc5d860e9c Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:23:35 +0200 Subject: [PATCH 07/15] Replace pattern synomyn with ADT. (#4252) --- libs/types-common/src/Data/Qualified.hs | 50 +++++++++---------- services/galley/src/Galley/API/MLS/Message.hs | 2 +- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/libs/types-common/src/Data/Qualified.hs b/libs/types-common/src/Data/Qualified.hs index 0dbc73f99ec..18407f557fd 100644 --- a/libs/types-common/src/Data/Qualified.hs +++ b/libs/types-common/src/Data/Qualified.hs @@ -25,13 +25,14 @@ module Data.Qualified Qualified (..), qToPair, QualifiedWithTag, + RelativeTo (..), + relativeTo, tUnqualified, tDomain, tUntagged, tSplit, qTagUnsafe, Remote, - RelativeTo (Remote, Local, RelativeTo), toRemoteUnsafe, Local, toLocalUnsafe, @@ -111,35 +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 = ($>) -foldQualified :: Local x -> (Local a -> b) -> (Remote a -> b) -> Qualified a -> b -foldQualified loc kLocal kRemote q = case q `RelativeTo` loc of - Local l -> kLocal l - Remote r -> kRemote r - -data a `RelativeTo` x = Qualified a `RelativeTo` Local x - -checkRelative :: a `RelativeTo` x -> Either (Local a) (Remote a) -checkRelative (q `RelativeTo` loc) - | tDomain loc == qDomain q = Left (qTagUnsafe q) - | otherwise = Right (qTagUnsafe q) +data RelativeTo a + = Local (Local a) + | Remote (Remote a) -pattern Local :: forall a x. Local a -> a `RelativeTo` x -pattern Local loc <- (checkRelative -> Left loc) +foldQualified :: Local x -> (Local a -> b) -> (Remote a -> b) -> Qualified a -> b +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 = + Local (qTagUnsafe q) + | otherwise = + Remote (qTagUnsafe q) -pattern Remote :: forall a x. Remote a -> a `RelativeTo` x -pattern Remote rem <- (checkRelative -> Right rem) +isLocal :: Local x -> Qualified a -> Bool +isLocal loc q = case q `relativeTo` loc of + Local _ -> True + Remote _ -> False -{-# COMPLETE Local, Remote #-} +-- | 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. -- @@ -174,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/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index f3c43774b29..75de5388c22 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -224,7 +224,7 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do -- when a user tries to join any mls conversation while being legalholded -- they receive a 409 stating that mls and legalhold are incompatible - case qusr `RelativeTo` lConvOrSubId of + case qusr `relativeTo` lConvOrSubId of Local luid -> when (isNothing convOrSub.mlsMeta.cnvmlsActiveData) do usrTeams <- getUserTeams (tUnqualified luid) From f184788c3704b92ba63245732711efa33762f5c0 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Wed, 18 Sep 2024 17:30:48 +0200 Subject: [PATCH 08/15] brig: Make `GET /services/tags` work again (#4250) --- changelog.d/3-bug-fixes/services-tags | 1 + libs/wire-api/src/Wire/API/Routes/Public/Brig/Services.hs | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 changelog.d/3-bug-fixes/services-tags diff --git a/changelog.d/3-bug-fixes/services-tags b/changelog.d/3-bug-fixes/services-tags new file mode 100644 index 00000000000..9d0ef1900f7 --- /dev/null +++ b/changelog.d/3-bug-fixes/services-tags @@ -0,0 +1 @@ +brig: Make `GET /services/tags` work again \ No newline at end of file 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 From c04e58321ffb29a93ea48ea0f9fee176328f338c Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 19 Sep 2024 06:00:26 +0200 Subject: [PATCH 09/15] Move search operations to UserSubsystem (#4188) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * UserStore.Cassandra: Dedup embed call * Move all indexing operations to UserSearchSubsystem Pending: - Index Management - Search - Metrics - Update brig to use the new code (currently, brig is just broken) * Add metrics to UserSearchSubsystem and IndexedUserStore Some metrics have been deleted as they are for bulk operations and there is no way for us to get those metrics because these operations don't run in an http request. * Run user serach index data migrations using subsystems * Remove explicit creds from ESConn * Delete leftover code from Brig.Index.Eval * Move UserDoc tests to subsystem Delete test for indexToDoc because it was only used in the test * indexUserRow -> indexUser * brig: Delete bulk reindex operations from internal API * brig: Use the UserSearchSubsystem for syncing with index Query operations are still pending * regen nix * Rename an effect action * WIP: Move search code to subsystem * Move FederationConfigStore.Cassandra out of Brig * Remove dead code from Brig.User.API.Search * Move browseTeam to wire-subsystem * wire-subsystems: Fix compile errors with MiniBackend Also accomodates some changes from future commits * Merge UserSearchSubsystem into UserSubsystem * Wire.BlockListStore.Cassandra: Take ClientState as an arg instead of putting MonadClient constraint * brig: Untangle sending user notifs and maintaining internal state Brig.IO.Intra.onUserEvent used to keep the search index up to date, send user events and send journal events. The keeping the search index up to date part is now bubbled up to all the places which were calling this function. This commit also deals with there no longer being UserSearchSubsystem. * Fix merge mistakes * Delete leftover comment * UserStore.GetIndexUsersPaginated: Allow specifying page size Use 1000 as the size while running index migrations * wire-subsystems: Fix mock interpreters * Dedup IndexError * Remove TODO * Reorganize Bulk operations * UserSearch.Types: Remove comment The JSON instances are already not compatible the conversion happens explicitly in application code * Move expectedMigrationVersion to IndexedUserStore.Bulk * regen nix * Removed duplicated function. * Brig.API.User.onActivated: update the index when email changes * Promote suspected bug to confirmed bug, to be solved in a separate ticket * Another bug reported for another ticket * Resolve todo: Moved a function, drive-by clean-up of lenses overuse. * Update user search index on team changes. * Pass casClient instead of embed. * Error for searcher doesn't exist. * Removed TODO, out of scope. * Upgraded TODO to FUTUREWORK. * Added changelogs. * UserSubsystem: simplify folding over a domain * Bubble up liftSem'ing * Remove commented out code * Error messages for mocks/uninterpreted actions * Remove a UserSubsystemError * Remove unusued MapError instance --------- Co-authored-by: Marko Dimjašević Co-authored-by: Igor Ranieri --- changelog.d/5-internal/WPB-888-2 | 1 + changelog.d/5-internal/WPB-8888 | 1 + integration/test/Test/Teams.hs | 8 + libs/brig-types/brig-types.cabal | 7 +- libs/brig-types/default.nix | 7 - libs/brig-types/src/Brig/Types/Search.hs | 107 ---- .../test/unit/Test/Brig/Types/User.hs | 2 - libs/cassandra-util/src/Cassandra/Exec.hs | 24 + libs/cassandra-util/src/Cassandra/Util.hs | 1 + libs/polysemy-wire-zoo/default.nix | 2 + .../polysemy-wire-zoo/polysemy-wire-zoo.cabal | 6 +- .../polysemy-wire-zoo/src/Wire/Sem/Metrics.hs | 21 + .../src/Wire/Sem/Metrics/IO.hs | 16 + libs/wire-api/src/Wire/API/Error/Brig.hs | 1 + .../API/Routes/Internal/Brig/SearchIndex.hs | 18 - .../src/Wire/API/Routes/Public/Brig.hs | 4 +- libs/wire-api/src/Wire/API/Team/Feature.hs | 3 +- libs/wire-api/src/Wire/API/Team/Member.hs | 8 +- libs/wire-api/src/Wire/API/Team/Permission.hs | 16 +- .../Wire/API/Golden/Generated/Event_team.hs | 8 +- .../Golden/Generated/NewTeamMember_team.hs | 68 +- .../API/Golden/Generated/Permissions_team.hs | 78 +-- .../Golden/Generated/TeamMemberList_team.hs | 234 +++---- .../API/Golden/Generated/TeamMember_team.hs | 70 +-- .../test/unit/Test/Wire/API/Team/Member.hs | 23 +- libs/wire-subsystems/default.nix | 13 + .../src/Wire/BlockListStore/Cassandra.hs | 12 +- .../Wire/FederationAPIAccess/Interpreter.hs | 9 + .../src/Wire}/FederationConfigStore.hs | 4 +- .../Wire}/FederationConfigStore/Cassandra.hs | 15 +- .../src/Wire/GalleyAPIAccess.hs | 6 + .../src/Wire/GalleyAPIAccess/Rpc.hs | 20 + .../src/Wire/IndexedUserStore.hs | 43 ++ .../src/Wire/IndexedUserStore/Bulk.hs | 22 + .../IndexedUserStore/Bulk/ElasticSearch.hs | 133 ++++ .../Wire/IndexedUserStore/ElasticSearch.hs | 500 +++++++++++++++ .../Wire/IndexedUserStore/MigrationStore.hs | 13 + .../MigrationStore/ElasticSearch.hs | 73 +++ .../src/Wire/UserSearch/Metrics.hs | 44 ++ .../src/Wire/UserSearch/Migration.hs | 30 + .../src/Wire/UserSearch/Types.hs | 207 +++++++ libs/wire-subsystems/src/Wire/UserStore.hs | 5 +- .../src/Wire/UserStore/Cassandra.hs | 58 +- .../src/Wire/UserStore/IndexUser.hs | 200 ++++++ .../wire-subsystems/src/Wire/UserSubsystem.hs | 76 +++ .../src/Wire/UserSubsystem/Error.hs | 2 + .../src/Wire/UserSubsystem/Interpreter.hs | 296 ++++++++- .../test/unit/Wire/MiniBackend.hs | 10 + .../test/unit/Wire/MockInterpreters.hs | 2 + .../MockInterpreters/FederationConfigStore.hs | 36 ++ .../Wire/MockInterpreters/GalleyAPIAccess.hs | 6 + .../Wire/MockInterpreters/IndexedUserStore.hs | 15 + .../unit/Wire/MockInterpreters/UserStore.hs | 28 + .../test/unit/Wire/UserSearch/TypesSpec.hs | 54 ++ .../Wire/UserSubsystem/InterpreterSpec.hs | 55 +- libs/wire-subsystems/wire-subsystems.cabal | 22 + services/brig/brig.cabal | 17 - services/brig/default.nix | 5 - services/brig/src/Brig/API/Auth.hs | 45 +- services/brig/src/Brig/API/Client.hs | 51 +- services/brig/src/Brig/API/Connection.hs | 2 +- .../brig/src/Brig/API/Connection/Remote.hs | 2 +- services/brig/src/Brig/API/Federation.hs | 13 +- services/brig/src/Brig/API/Internal.hs | 192 +++--- services/brig/src/Brig/API/Public.hs | 143 ++--- services/brig/src/Brig/API/User.hs | 130 ++-- services/brig/src/Brig/App.hs | 1 + .../brig/src/Brig/CanonicalInterpreter.hs | 35 +- services/brig/src/Brig/IO/Intra.hs | 42 +- services/brig/src/Brig/Index/Eval.hs | 164 +++-- services/brig/src/Brig/Index/Migrations.hs | 173 ------ .../brig/src/Brig/Index/Migrations/Types.hs | 100 --- .../brig/src/Brig/InternalEvent/Process.hs | 15 +- services/brig/src/Brig/Provider/API.hs | 42 +- services/brig/src/Brig/Team/API.hs | 136 ++-- services/brig/src/Brig/Team/Util.hs | 68 -- services/brig/src/Brig/User/API/Search.hs | 190 ------ services/brig/src/Brig/User/Auth.hs | 52 +- services/brig/src/Brig/User/Search/Index.hs | 584 +----------------- .../brig/src/Brig/User/Search/Index/Types.hs | 230 ------- .../brig/src/Brig/User/Search/SearchIndex.hs | 18 +- .../brig/src/Brig/User/Search/TeamSize.hs | 1 + .../src/Brig/User/Search/TeamUserSearch.hs | 175 ------ services/brig/test/unit/Run.hs | 4 +- .../unit/Test/Brig/User/Search/Index/Types.hs | 84 --- services/galley/galley.cabal | 1 - .../src/V1_BackfillBillingTeamMembers.hs | 3 +- services/galley/src/Galley/API/Teams.hs | 4 +- services/galley/src/Galley/Cassandra/Team.hs | 2 +- 89 files changed, 2828 insertions(+), 2639 deletions(-) create mode 100644 changelog.d/5-internal/WPB-888-2 create mode 100644 changelog.d/5-internal/WPB-8888 delete mode 100644 libs/brig-types/src/Brig/Types/Search.hs create mode 100644 libs/polysemy-wire-zoo/src/Wire/Sem/Metrics.hs create mode 100644 libs/polysemy-wire-zoo/src/Wire/Sem/Metrics/IO.hs rename {services/brig/src/Brig/Effects => libs/wire-subsystems/src/Wire}/FederationConfigStore.hs (90%) rename {services/brig/src/Brig/Effects => libs/wire-subsystems/src/Wire}/FederationConfigStore/Cassandra.hs (97%) create mode 100644 libs/wire-subsystems/src/Wire/IndexedUserStore.hs create mode 100644 libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk.hs create mode 100644 libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs create mode 100644 libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs create mode 100644 libs/wire-subsystems/src/Wire/IndexedUserStore/MigrationStore.hs create mode 100644 libs/wire-subsystems/src/Wire/IndexedUserStore/MigrationStore/ElasticSearch.hs create mode 100644 libs/wire-subsystems/src/Wire/UserSearch/Metrics.hs create mode 100644 libs/wire-subsystems/src/Wire/UserSearch/Migration.hs create mode 100644 libs/wire-subsystems/src/Wire/UserSearch/Types.hs create mode 100644 libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs create mode 100644 libs/wire-subsystems/test/unit/Wire/MockInterpreters/FederationConfigStore.hs create mode 100644 libs/wire-subsystems/test/unit/Wire/MockInterpreters/IndexedUserStore.hs create mode 100644 libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs delete mode 100644 services/brig/src/Brig/Index/Migrations.hs delete mode 100644 services/brig/src/Brig/Index/Migrations/Types.hs delete mode 100644 services/brig/src/Brig/Team/Util.hs delete mode 100644 services/brig/src/Brig/User/API/Search.hs delete mode 100644 services/brig/src/Brig/User/Search/Index/Types.hs delete mode 100644 services/brig/src/Brig/User/Search/TeamUserSearch.hs delete mode 100644 services/brig/test/unit/Test/Brig/User/Search/Index/Types.hs diff --git a/changelog.d/5-internal/WPB-888-2 b/changelog.d/5-internal/WPB-888-2 new file mode 100644 index 00000000000..b898071cea8 --- /dev/null +++ b/changelog.d/5-internal/WPB-888-2 @@ -0,0 +1 @@ +Removed `indexReindex` and `indexReindexIfSameOrNewer` from internal Brig/SearchIndex. diff --git a/changelog.d/5-internal/WPB-8888 b/changelog.d/5-internal/WPB-8888 new file mode 100644 index 00000000000..f5d3655308a --- /dev/null +++ b/changelog.d/5-internal/WPB-8888 @@ -0,0 +1 @@ +Introduced ElasticSearch effects related to user search. diff --git a/integration/test/Test/Teams.hs b/integration/test/Test/Teams.hs index e3394fa769c..c54a7b18b46 100644 --- a/integration/test/Test/Teams.hs +++ b/integration/test/Test/Teams.hs @@ -49,31 +49,37 @@ testInvitePersonalUserToTeam = 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 <- 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 <- 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 + void $ withWebSockets [user] $ \wss -> do acceptTeamInvitation user code (Just defPassword) >>= assertSuccess for wss $ \ws -> do n <- awaitMatch isUserUpdatedNotif ws n %. "payload.0.user.team" `shouldMatch` tid + 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 @@ -82,12 +88,14 @@ testInvitePersonalUserToTeam = do 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] + refreshIndex domain -- a team member can now search for the former personal user bindResponse (searchContacts tm (user %. "name") domain) $ \resp -> do diff --git a/libs/brig-types/brig-types.cabal b/libs/brig-types/brig-types.cabal index 2f67b800eb5..161a81ce30c 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 diff --git a/libs/brig-types/default.nix b/libs/brig-types/default.nix index 50587cd4eeb..290305e7c13 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 @@ -18,7 +16,6 @@ , tasty , tasty-hunit , tasty-quickcheck -, text , types-common , wire-api }: @@ -27,16 +24,12 @@ mkDerivation { version = "1.35.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ - aeson - attoparsec base - bytestring bytestring-conversion cassandra-util containers imports QuickCheck - text types-common wire-api ]; 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/test/unit/Test/Brig/Types/User.hs b/libs/brig-types/test/unit/Test/Brig/Types/User.hs index 6ca50562cb4..e345eb8e9b0 100644 --- a/libs/brig-types/test/unit/Test/Brig/Types/User.hs +++ b/libs/brig-types/test/unit/Test/Brig/Types/User.hs @@ -27,7 +27,6 @@ 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.User (ManagedByUpdate (..), RichInfoUpdate (..)) import Data.Aeson import Imports @@ -50,7 +49,6 @@ roundtripTests = testRoundTripWithSwagger @EJPDRequestBody, testRoundTripWithSwagger @EJPDResponseBody, testRoundTrip @UpdateConnectionsInternal, - testRoundTrip @SearchVisibilityInbound, testRoundTripWithSwagger @UserAccount, testGroup "golden tests" $ [testCaseUserAccount] diff --git a/libs/cassandra-util/src/Cassandra/Exec.hs b/libs/cassandra-util/src/Cassandra/Exec.hs index c7d4c352a99..8ef7d64337c 100644 --- a/libs/cassandra-util/src/Cassandra/Exec.hs +++ b/libs/cassandra-util/src/Cassandra/Exec.hs @@ -27,6 +27,7 @@ module Cassandra.Exec paginateC, PageWithState (..), paginateWithState, + paginateWithStateC, paramsPagingState, pwsHasMore, module C, @@ -115,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/Util.hs b/libs/cassandra-util/src/Cassandra/Util.hs index f8b793f77db..4331da819c5 100644 --- a/libs/cassandra-util/src/Cassandra/Util.hs +++ b/libs/cassandra-util/src/Cassandra/Util.hs @@ -109,6 +109,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 (Functor) instance Cql (Writetime a) where ctype = Tagged BigIntColumn 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/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/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index 16efe68b803..416e5fecaa2 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 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/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index aee06b492ad..7394673620e 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -1178,7 +1178,7 @@ type ConnectionAPI = ( 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 @@ -1380,7 +1380,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, diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 4aba62549c9..533bbd8837f 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -182,7 +182,8 @@ class ( Default cfg, ToSchema cfg, Default (LockableFeature cfg), - KnownSymbol (FeatureSymbol cfg) + KnownSymbol (FeatureSymbol cfg), + NpProject cfg Features ) => IsFeatureConfig cfg where diff --git a/libs/wire-api/src/Wire/API/Team/Member.hs b/libs/wire-api/src/Wire/API/Team/Member.hs index d94cbfecc32..98720fab69b 100644 --- a/libs/wire-api/src/Wire/API/Team/Member.hs +++ b/libs/wire-api/src/Wire/API/Team/Member.hs @@ -585,10 +585,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..26ddd8865ba 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,8 @@ import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) -- Permissions data Permissions = Permissions - { _self :: Set Perm, - _copy :: Set Perm + { self :: Set Perm, + copy :: Set Perm } deriving stock (Eq, Ord, Show, Generic) deriving (FromJSON, ToJSON, S.ToSchema) via (Schema Permissions) @@ -71,8 +69,8 @@ permissionsSchema :: ValueSchema NamedSwaggerDoc Permissions permissionsSchema = objectWithDocModifier "Permissions" (description ?~ docs) $ Permissions - <$> (permsToInt . _self) .= field "self" (intToPerms <$> schema) - <*> (permsToInt . _copy) .= field "copy" (intToPerms <$> schema) + <$> (permsToInt . self) .= field "self" (intToPerms <$> schema) + <*> (permsToInt . copy) .= field "copy" (intToPerms <$> schema) where docs = "This is just a complicated way of representing a team role. self and copy \ @@ -198,14 +196,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/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/NewTeamMember_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewTeamMember_team.hs index 34294b4bd00..0c5db95a9d3 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewTeamMember_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewTeamMember_team.hs @@ -41,14 +41,14 @@ import Wire.API.Team.Permission SetMemberPermissions, SetTeamData ), - Permissions (Permissions, _copy, _self), + Permissions (Permissions, copy, self), ) testObject_NewTeamMember_team_1 :: NewTeamMember testObject_NewTeamMember_team_1 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000002-0000-0007-0000-000200000002"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000004")), fromJust (readUTCTimeMillis "1864-05-04T12:59:54.182Z") @@ -60,7 +60,7 @@ testObject_NewTeamMember_team_2 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000004-0000-0000-0000-000200000003"))) ( Permissions - { _self = + { self = fromList [ CreateConversation, DeleteConversation, @@ -69,7 +69,7 @@ testObject_NewTeamMember_team_2 = AddRemoveConvMember, ModifyConvName ], - _copy = fromList [DeleteConversation, AddRemoveConvMember] + copy = fromList [DeleteConversation, AddRemoveConvMember] } ) ( Just @@ -83,10 +83,10 @@ testObject_NewTeamMember_team_3 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000008-0000-0008-0000-000700000005"))) ( Permissions - { _self = + { self = fromList [CreateConversation, DeleteConversation, RemoveTeamMember, GetBilling, DeleteTeam], - _copy = fromList [CreateConversation, DeleteConversation, GetBilling] + copy = fromList [CreateConversation, DeleteConversation, GetBilling] } ) ( Just @@ -100,8 +100,8 @@ testObject_NewTeamMember_team_4 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000700000005"))) ( Permissions - { _self = fromList [CreateConversation, AddTeamMember, SetTeamData], - _copy = fromList [CreateConversation, SetTeamData] + { self = fromList [CreateConversation, AddTeamMember, SetTeamData], + copy = fromList [CreateConversation, SetTeamData] } ) Nothing @@ -110,7 +110,7 @@ testObject_NewTeamMember_team_5 :: NewTeamMember testObject_NewTeamMember_team_5 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000002"))) - (Permissions {_self = fromList [AddTeamMember, SetBilling, GetTeamConversations], _copy = fromList [AddTeamMember]}) + (Permissions {self = fromList [AddTeamMember, SetBilling, GetTeamConversations], copy = fromList [AddTeamMember]}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000600000006")), fromJust (readUTCTimeMillis "1864-05-12T23:29:05.832Z") @@ -122,10 +122,10 @@ testObject_NewTeamMember_team_6 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000002-0000-0006-0000-000400000003"))) ( Permissions - { _self = + { self = fromList [CreateConversation, DeleteConversation, GetBilling, SetTeamData, SetMemberPermissions], - _copy = fromList [CreateConversation, GetBilling] + copy = fromList [CreateConversation, GetBilling] } ) ( Just @@ -139,10 +139,10 @@ testObject_NewTeamMember_team_7 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000007-0000-0004-0000-000500000005"))) ( Permissions - { _self = + { self = fromList [AddTeamMember, RemoveTeamMember, ModifyConvName, GetTeamConversations, DeleteTeam], - _copy = fromList [AddTeamMember] + copy = fromList [AddTeamMember] } ) ( Just @@ -156,8 +156,8 @@ testObject_NewTeamMember_team_8 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000008-0000-0003-0000-000200000003"))) ( Permissions - { _self = fromList [ModifyConvName], - _copy = fromList [ModifyConvName] + { self = fromList [ModifyConvName], + copy = fromList [ModifyConvName] } ) ( Just @@ -170,7 +170,7 @@ testObject_NewTeamMember_team_9 :: NewTeamMember testObject_NewTeamMember_team_9 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0008-0000-000300000004"))) - (Permissions {_self = fromList [SetBilling], _copy = fromList []}) + (Permissions {self = fromList [SetBilling], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000700000000")), fromJust (readUTCTimeMillis "1864-05-08T10:27:23.240Z") @@ -181,7 +181,7 @@ testObject_NewTeamMember_team_10 :: NewTeamMember testObject_NewTeamMember_team_10 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000008-0000-0003-0000-000600000003"))) - (Permissions {_self = fromList [GetBilling], _copy = fromList []}) + (Permissions {self = fromList [GetBilling], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000004-0000-0006-0000-000600000008")), fromJust (readUTCTimeMillis "1864-05-15T10:49:54.418Z") @@ -193,8 +193,8 @@ testObject_NewTeamMember_team_11 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000006-0000-0005-0000-000000000002"))) ( Permissions - { _self = fromList [CreateConversation, ModifyConvName, SetTeamData], - _copy = fromList [] + { self = fromList [CreateConversation, ModifyConvName, SetTeamData], + copy = fromList [] } ) ( Just @@ -207,7 +207,7 @@ testObject_NewTeamMember_team_12 :: NewTeamMember testObject_NewTeamMember_team_12 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0004-0000-000000000007"))) - (Permissions {_self = fromList [SetBilling, SetTeamData, GetTeamConversations], _copy = fromList []}) + (Permissions {self = fromList [SetBilling, SetTeamData, GetTeamConversations], copy = fromList []}) Nothing testObject_NewTeamMember_team_13 :: NewTeamMember @@ -215,8 +215,8 @@ testObject_NewTeamMember_team_13 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000002-0000-0004-0000-000600000001"))) ( Permissions - { _self = fromList [AddTeamMember, AddRemoveConvMember, SetTeamData, GetTeamConversations], - _copy = fromList [AddTeamMember, AddRemoveConvMember, GetTeamConversations] + { self = fromList [AddTeamMember, AddRemoveConvMember, SetTeamData, GetTeamConversations], + copy = fromList [AddTeamMember, AddRemoveConvMember, GetTeamConversations] } ) Nothing @@ -226,10 +226,10 @@ testObject_NewTeamMember_team_14 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000500000004"))) ( Permissions - { _self = + { self = fromList [CreateConversation, DeleteConversation, ModifyConvName, GetBilling], - _copy = fromList [] + copy = fromList [] } ) ( Just @@ -243,8 +243,8 @@ testObject_NewTeamMember_team_15 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0008-0000-000800000007"))) ( Permissions - { _self = fromList [RemoveTeamMember, GetMemberPermissions, DeleteTeam], - _copy = fromList [RemoveTeamMember, GetMemberPermissions] + { self = fromList [RemoveTeamMember, GetMemberPermissions, DeleteTeam], + copy = fromList [RemoveTeamMember, GetMemberPermissions] } ) ( Just @@ -258,8 +258,8 @@ testObject_NewTeamMember_team_16 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0006-0000-000300000005"))) ( Permissions - { _self = fromList [CreateConversation, RemoveTeamMember, GetBilling, GetTeamConversations, DeleteTeam], - _copy = fromList [] + { self = fromList [CreateConversation, RemoveTeamMember, GetBilling, GetTeamConversations, DeleteTeam], + copy = fromList [] } ) Nothing @@ -268,7 +268,7 @@ testObject_NewTeamMember_team_17 :: NewTeamMember testObject_NewTeamMember_team_17 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0008-0000-000400000005"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000004-0000-0008-0000-000800000007")), fromJust (readUTCTimeMillis "1864-05-07T21:53:30.897Z") @@ -279,7 +279,7 @@ testObject_NewTeamMember_team_18 :: NewTeamMember testObject_NewTeamMember_team_18 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000006-0000-0003-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000500000002")), fromJust (readUTCTimeMillis "1864-05-11T12:32:01.417Z") @@ -291,8 +291,8 @@ testObject_NewTeamMember_team_19 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000004-0000-0005-0000-000100000008"))) ( Permissions - { _self = fromList [DeleteConversation, RemoveTeamMember, SetBilling, SetMemberPermissions], - _copy = fromList [DeleteConversation, SetBilling] + { self = fromList [DeleteConversation, RemoveTeamMember, SetBilling, SetMemberPermissions], + copy = fromList [DeleteConversation, SetBilling] } ) Nothing @@ -302,7 +302,7 @@ testObject_NewTeamMember_team_20 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000008-0000-0000-0000-000000000004"))) ( Permissions - { _self = + { self = fromList [ AddTeamMember, AddRemoveConvMember, @@ -311,7 +311,7 @@ testObject_NewTeamMember_team_20 = GetMemberPermissions, GetTeamConversations ], - _copy = fromList [] + copy = fromList [] } ) ( Just 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/TeamMemberList_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamMemberList_team.hs index b540677bd5e..55838c4d159 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamMemberList_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamMemberList_team.hs @@ -47,7 +47,7 @@ import Wire.API.Team.Permission SetMemberPermissions, SetTeamData ), - Permissions (Permissions, _copy, _self), + Permissions (Permissions, copy, self), ) testObject_TeamMemberList_team_1 :: TeamMemberList @@ -58,7 +58,7 @@ testObject_TeamMemberList_team_2 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000000000002"))) - (Permissions {_self = fromList [GetBilling, SetMemberPermissions], _copy = fromList []}) + (Permissions {self = fromList [GetBilling, SetMemberPermissions], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000002")), fromJust (readUTCTimeMillis "1864-05-10T10:05:44.332Z") @@ -73,7 +73,7 @@ testObject_TeamMemberList_team_3 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T06:07:36.175Z") @@ -82,7 +82,7 @@ testObject_TeamMemberList_team_3 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T14:28:10.448Z") @@ -91,7 +91,7 @@ testObject_TeamMemberList_team_3 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T16:05:37.642Z") @@ -100,12 +100,12 @@ testObject_TeamMemberList_team_3 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T13:06:20.504Z") @@ -114,7 +114,7 @@ testObject_TeamMemberList_team_3 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T16:37:10.774Z") @@ -123,7 +123,7 @@ testObject_TeamMemberList_team_3 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T04:36:55.388Z") @@ -138,7 +138,7 @@ testObject_TeamMemberList_team_4 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [GetTeamConversations], _copy = fromList []}) + (Permissions {self = fromList [GetTeamConversations], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-08T16:05:11.696Z") @@ -147,7 +147,7 @@ testObject_TeamMemberList_team_4 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-08T07:09:26.753Z") @@ -162,7 +162,7 @@ testObject_TeamMemberList_team_5 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T23:10:04.963Z") @@ -171,7 +171,7 @@ testObject_TeamMemberList_team_5 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T15:40:17.119Z") @@ -180,7 +180,7 @@ testObject_TeamMemberList_team_5 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T00:40:38.004Z") @@ -189,7 +189,7 @@ testObject_TeamMemberList_team_5 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T07:30:49.028Z") @@ -204,7 +204,7 @@ testObject_TeamMemberList_team_6 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T17:07:48.156Z") @@ -213,7 +213,7 @@ testObject_TeamMemberList_team_6 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T00:04:10.559Z") @@ -222,7 +222,7 @@ testObject_TeamMemberList_team_6 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T10:39:19.860Z") @@ -231,7 +231,7 @@ testObject_TeamMemberList_team_6 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T13:40:56.648Z") @@ -240,7 +240,7 @@ testObject_TeamMemberList_team_6 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T12:13:40.273Z") @@ -249,7 +249,7 @@ testObject_TeamMemberList_team_6 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T13:28:04.561Z") @@ -258,7 +258,7 @@ testObject_TeamMemberList_team_6 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T02:59:55.584Z") @@ -267,7 +267,7 @@ testObject_TeamMemberList_team_6 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T22:57:33.947Z") @@ -276,7 +276,7 @@ testObject_TeamMemberList_team_6 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T01:02:39.691Z") @@ -285,7 +285,7 @@ testObject_TeamMemberList_team_6 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T13:39:38.488Z") @@ -300,12 +300,12 @@ testObject_TeamMemberList_team_7 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [SetTeamData], _copy = fromList []}) + (Permissions {self = fromList [SetTeamData], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-10T03:11:36.961Z") @@ -314,7 +314,7 @@ testObject_TeamMemberList_team_7 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldEnabled ] @@ -325,7 +325,7 @@ testObject_TeamMemberList_team_8 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T07:35:03.629Z") @@ -334,7 +334,7 @@ testObject_TeamMemberList_team_8 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T00:48:38.818Z") @@ -343,7 +343,7 @@ testObject_TeamMemberList_team_8 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T06:12:10.151Z") @@ -352,7 +352,7 @@ testObject_TeamMemberList_team_8 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T03:45:53.520Z") @@ -361,7 +361,7 @@ testObject_TeamMemberList_team_8 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T17:14:59.798Z") @@ -370,7 +370,7 @@ testObject_TeamMemberList_team_8 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T17:51:55.340Z") @@ -379,7 +379,7 @@ testObject_TeamMemberList_team_8 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T01:38:35.880Z") @@ -388,7 +388,7 @@ testObject_TeamMemberList_team_8 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T18:06:10.660Z") @@ -397,7 +397,7 @@ testObject_TeamMemberList_team_8 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T07:30:46.880Z") @@ -406,12 +406,12 @@ testObject_TeamMemberList_team_8 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldPending ] @@ -422,7 +422,7 @@ testObject_TeamMemberList_team_9 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [AddTeamMember], _copy = fromList []}) + (Permissions {self = fromList [AddTeamMember], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-08T22:16:59.050Z") @@ -431,7 +431,7 @@ testObject_TeamMemberList_team_9 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [CreateConversation], _copy = fromList []}) + (Permissions {self = fromList [CreateConversation], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-08T21:43:37.550Z") @@ -446,7 +446,7 @@ testObject_TeamMemberList_team_10 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T04:44:28.366Z") @@ -455,7 +455,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T06:22:04.036Z") @@ -464,7 +464,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T12:10:11.701Z") @@ -473,7 +473,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T21:54:05.305Z") @@ -482,7 +482,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T00:26:06.221Z") @@ -491,12 +491,12 @@ testObject_TeamMemberList_team_10 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T20:12:04.856Z") @@ -505,7 +505,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T23:35:44.986Z") @@ -514,7 +514,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T07:36:17.730Z") @@ -523,7 +523,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T19:36:57.529Z") @@ -532,12 +532,12 @@ testObject_TeamMemberList_team_10 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T19:45:56.914Z") @@ -546,7 +546,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T13:42:17.107Z") @@ -555,7 +555,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T03:42:46.106Z") @@ -564,7 +564,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T09:41:44.679Z") @@ -573,7 +573,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T09:26:44.717Z") @@ -582,7 +582,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T00:40:00.056Z") @@ -591,12 +591,12 @@ testObject_TeamMemberList_team_10 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T07:47:20.635Z") @@ -605,7 +605,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T15:58:21.895Z") @@ -614,7 +614,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T19:25:51.873Z") @@ -623,7 +623,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T03:19:55.569Z") @@ -638,7 +638,7 @@ testObject_TeamMemberList_team_11 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T06:08:50.626Z") @@ -647,12 +647,12 @@ testObject_TeamMemberList_team_11 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T08:23:53.653Z") @@ -661,12 +661,12 @@ testObject_TeamMemberList_team_11 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T16:28:42.815Z") @@ -675,17 +675,17 @@ testObject_TeamMemberList_team_11 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T11:47:57.498Z") @@ -694,7 +694,7 @@ testObject_TeamMemberList_team_11 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T17:22:07.538Z") @@ -703,7 +703,7 @@ testObject_TeamMemberList_team_11 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T19:14:48.836Z") @@ -712,7 +712,7 @@ testObject_TeamMemberList_team_11 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T14:53:49.059Z") @@ -721,7 +721,7 @@ testObject_TeamMemberList_team_11 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T10:44:04.209Z") @@ -730,7 +730,7 @@ testObject_TeamMemberList_team_11 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T23:34:24.831Z") @@ -745,12 +745,12 @@ testObject_TeamMemberList_team_12 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T15:59:09.462Z") @@ -759,12 +759,12 @@ testObject_TeamMemberList_team_12 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T00:27:17.631Z") @@ -779,12 +779,12 @@ testObject_TeamMemberList_team_13 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [GetMemberPermissions], _copy = fromList []}) + (Permissions {self = fromList [GetMemberPermissions], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-10T04:37:19.686Z") @@ -793,7 +793,7 @@ testObject_TeamMemberList_team_13 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T13:22:20.368Z") @@ -808,12 +808,12 @@ testObject_TeamMemberList_team_14 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T07:01:56.077Z") @@ -822,7 +822,7 @@ testObject_TeamMemberList_team_14 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T09:34:46.900Z") @@ -831,7 +831,7 @@ testObject_TeamMemberList_team_14 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T10:40:24.034Z") @@ -840,7 +840,7 @@ testObject_TeamMemberList_team_14 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T10:17:53.056Z") @@ -849,7 +849,7 @@ testObject_TeamMemberList_team_14 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T18:37:38.894Z") @@ -858,12 +858,12 @@ testObject_TeamMemberList_team_14 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T06:25:10.534Z") @@ -872,7 +872,7 @@ testObject_TeamMemberList_team_14 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T02:42:16.433Z") @@ -881,7 +881,7 @@ testObject_TeamMemberList_team_14 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T07:25:18.248Z") @@ -890,12 +890,12 @@ testObject_TeamMemberList_team_14 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T15:31:36.237Z") @@ -904,7 +904,7 @@ testObject_TeamMemberList_team_14 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T15:23:38.616Z") @@ -913,12 +913,12 @@ testObject_TeamMemberList_team_14 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldEnabled ] @@ -929,7 +929,7 @@ testObject_TeamMemberList_team_15 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T20:33:17.912Z") @@ -938,7 +938,7 @@ testObject_TeamMemberList_team_15 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T09:03:59.579Z") @@ -947,17 +947,17 @@ testObject_TeamMemberList_team_15 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldDisabled ] @@ -971,7 +971,7 @@ testObject_TeamMemberList_team_17 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T10:04:36.715Z") @@ -980,12 +980,12 @@ testObject_TeamMemberList_team_17 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T03:02:37.641Z") @@ -994,7 +994,7 @@ testObject_TeamMemberList_team_17 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T23:21:44.944Z") @@ -1003,7 +1003,7 @@ testObject_TeamMemberList_team_17 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T08:47:48.774Z") @@ -1018,7 +1018,7 @@ testObject_TeamMemberList_team_18 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T17:44:12.611Z") @@ -1027,7 +1027,7 @@ testObject_TeamMemberList_team_18 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T05:14:06.040Z") @@ -1036,7 +1036,7 @@ testObject_TeamMemberList_team_18 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T05:24:40.864Z") @@ -1045,7 +1045,7 @@ testObject_TeamMemberList_team_18 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T20:09:48.156Z") @@ -1054,7 +1054,7 @@ testObject_TeamMemberList_team_18 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T20:09:31.059Z") @@ -1070,8 +1070,8 @@ testObject_TeamMemberList_team_19 = [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000200000000"))) ( Permissions - { _self = fromList [CreateConversation, SetTeamData, SetMemberPermissions], - _copy = fromList [] + { self = fromList [CreateConversation, SetTeamData, SetMemberPermissions], + copy = fromList [] } ) ( Just @@ -1088,12 +1088,12 @@ testObject_TeamMemberList_team_20 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-08T15:41:51.601Z") diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamMember_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamMember_team.hs index 358b5cf8810..b810a1cc093 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamMember_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamMember_team.hs @@ -48,7 +48,7 @@ import Wire.API.Team.Permission SetMemberPermissions, SetTeamData ), - Permissions (Permissions, _copy, _self), + Permissions (Permissions, copy, self), ) testObject_TeamMember_team_1 :: TeamMember @@ -56,8 +56,8 @@ testObject_TeamMember_team_1 = mkTeamMember (Id (fromJust (UUID.fromString "00000007-0000-0005-0000-000500000002"))) ( Permissions - { _self = fromList [GetBilling, GetMemberPermissions, SetMemberPermissions, DeleteTeam], - _copy = fromList [GetBilling] + { self = fromList [GetBilling, GetMemberPermissions, SetMemberPermissions, DeleteTeam], + copy = fromList [GetBilling] } ) ( Just @@ -71,7 +71,7 @@ testObject_TeamMember_team_2 :: TeamMember testObject_TeamMember_team_2 = mkTeamMember (Id (fromJust (UUID.fromString "00000003-0000-0000-0000-000500000005"))) - (Permissions {_self = fromList [ModifyConvName, SetMemberPermissions], _copy = fromList []}) + (Permissions {self = fromList [ModifyConvName, SetMemberPermissions], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000004")), fromJust (readUTCTimeMillis "1864-05-03T14:56:52.508Z") @@ -84,10 +84,10 @@ testObject_TeamMember_team_3 = mkTeamMember (Id (fromJust (UUID.fromString "00000005-0000-0003-0000-000400000003"))) ( Permissions - { _self = + { self = fromList [DeleteConversation, AddTeamMember, AddRemoveConvMember, GetBilling], - _copy = fromList [GetBilling] + copy = fromList [GetBilling] } ) ( Just @@ -102,8 +102,8 @@ testObject_TeamMember_team_4 = mkTeamMember (Id (fromJust (UUID.fromString "00000008-0000-0005-0000-000100000006"))) ( Permissions - { _self = fromList [ModifyConvName, SetMemberPermissions], - _copy = fromList [SetMemberPermissions] + { self = fromList [ModifyConvName, SetMemberPermissions], + copy = fromList [SetMemberPermissions] } ) ( Just @@ -118,8 +118,8 @@ testObject_TeamMember_team_5 = mkTeamMember (Id (fromJust (UUID.fromString "00000007-0000-0000-0000-000200000001"))) ( Permissions - { _self = fromList [DeleteConversation, GetBilling, SetBilling, GetMemberPermissions], - _copy = fromList [DeleteConversation, GetMemberPermissions] + { self = fromList [DeleteConversation, GetBilling, SetBilling, GetMemberPermissions], + copy = fromList [DeleteConversation, GetMemberPermissions] } ) ( Just @@ -134,10 +134,10 @@ testObject_TeamMember_team_6 = mkTeamMember (Id (fromJust (UUID.fromString "00000006-0000-0007-0000-000800000005"))) ( Permissions - { _self = + { self = fromList [CreateConversation, AddTeamMember, AddRemoveConvMember, SetBilling, SetTeamData], - _copy = fromList [] + copy = fromList [] } ) ( Just @@ -152,7 +152,7 @@ testObject_TeamMember_team_7 = mkTeamMember (Id (fromJust (UUID.fromString "00000007-0000-0000-0000-000200000001"))) ( Permissions - { _self = + { self = fromList [ DeleteConversation, AddRemoveConvMember, @@ -160,7 +160,7 @@ testObject_TeamMember_team_7 = SetMemberPermissions, GetTeamConversations ], - _copy = fromList [] + copy = fromList [] } ) Nothing @@ -171,7 +171,7 @@ testObject_TeamMember_team_8 = mkTeamMember (Id (fromJust (UUID.fromString "00000005-0000-0007-0000-000300000000"))) ( Permissions - { _self = + { self = fromList [ AddRemoveConvMember, ModifyConvName, @@ -179,7 +179,7 @@ testObject_TeamMember_team_8 = SetMemberPermissions, DeleteTeam ], - _copy = fromList [] + copy = fromList [] } ) ( Just @@ -194,8 +194,8 @@ testObject_TeamMember_team_9 = mkTeamMember (Id (fromJust (UUID.fromString "00000008-0000-0006-0000-000300000003"))) ( Permissions - { _self = fromList [AddTeamMember, ModifyConvName], - _copy = fromList [ModifyConvName] + { self = fromList [AddTeamMember, ModifyConvName], + copy = fromList [ModifyConvName] } ) Nothing @@ -205,7 +205,7 @@ testObject_TeamMember_team_10 :: TeamMember testObject_TeamMember_team_10 = mkTeamMember (Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000100000006"))) - (Permissions {_self = fromList [DeleteConversation, AddTeamMember], _copy = fromList []}) + (Permissions {self = fromList [DeleteConversation, AddTeamMember], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000008-0000-0005-0000-000000000002")), fromJust (readUTCTimeMillis "1864-05-03T19:02:13.669Z") @@ -218,9 +218,9 @@ testObject_TeamMember_team_11 = mkTeamMember (Id (fromJust (UUID.fromString "00000004-0000-0001-0000-000400000007"))) ( Permissions - { _self = + { self = fromList [CreateConversation, DeleteConversation, SetTeamData, SetMemberPermissions], - _copy = fromList [] + copy = fromList [] } ) ( Just @@ -234,7 +234,7 @@ testObject_TeamMember_team_12 :: TeamMember testObject_TeamMember_team_12 = mkTeamMember (Id (fromJust (UUID.fromString "00000002-0000-0006-0000-000200000005"))) - (Permissions {_self = fromList [GetTeamConversations], _copy = fromList []}) + (Permissions {self = fromList [GetTeamConversations], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000005-0000-0001-0000-000300000003")), fromJust (readUTCTimeMillis "1864-05-10T22:34:18.259Z") @@ -246,7 +246,7 @@ testObject_TeamMember_team_13 :: TeamMember testObject_TeamMember_team_13 = mkTeamMember (Id (fromJust (UUID.fromString "00000006-0000-0001-0000-000800000006"))) - (Permissions {_self = fromList [CreateConversation, GetMemberPermissions], _copy = fromList [CreateConversation]}) + (Permissions {self = fromList [CreateConversation, GetMemberPermissions], copy = fromList [CreateConversation]}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0003-0000-000400000007")), fromJust (readUTCTimeMillis "1864-05-06T08:18:27.514Z") @@ -259,8 +259,8 @@ testObject_TeamMember_team_14 = mkTeamMember (Id (fromJust (UUID.fromString "00000004-0000-0000-0000-000300000007"))) ( Permissions - { _self = fromList [DeleteConversation, AddTeamMember, GetBilling, GetMemberPermissions], - _copy = fromList [GetBilling, GetMemberPermissions] + { self = fromList [DeleteConversation, AddTeamMember, GetBilling, GetMemberPermissions], + copy = fromList [GetBilling, GetMemberPermissions] } ) ( Just @@ -274,7 +274,7 @@ testObject_TeamMember_team_15 :: TeamMember testObject_TeamMember_team_15 = mkTeamMember (Id (fromJust (UUID.fromString "00000005-0000-0006-0000-000800000006"))) - (Permissions {_self = fromList [DeleteTeam], _copy = fromList [DeleteTeam]}) + (Permissions {self = fromList [DeleteTeam], copy = fromList [DeleteTeam]}) ( Just ( Id (fromJust (UUID.fromString "00000008-0000-0000-0000-000500000003")), fromJust (readUTCTimeMillis "1864-05-04T06:15:13.870Z") @@ -286,7 +286,7 @@ testObject_TeamMember_team_16 :: TeamMember testObject_TeamMember_team_16 = mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0008-0000-000200000008"))) - (Permissions {_self = fromList [DeleteConversation, GetTeamConversations], _copy = fromList []}) + (Permissions {self = fromList [DeleteConversation, GetTeamConversations], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000006-0000-0000-0000-000400000002")), fromJust (readUTCTimeMillis "1864-05-10T04:27:37.101Z") @@ -299,7 +299,7 @@ testObject_TeamMember_team_17 = mkTeamMember (Id (fromJust (UUID.fromString "00000006-0000-0006-0000-000500000007"))) ( Permissions - { _self = + { self = fromList [ AddRemoveConvMember, ModifyConvName, @@ -307,7 +307,7 @@ testObject_TeamMember_team_17 = SetTeamData, GetTeamConversations ], - _copy = fromList [AddRemoveConvMember] + copy = fromList [AddRemoveConvMember] } ) ( Just @@ -322,9 +322,9 @@ testObject_TeamMember_team_18 = mkTeamMember (Id (fromJust (UUID.fromString "00000005-0000-0005-0000-000200000008"))) ( Permissions - { _self = + { self = fromList [RemoveTeamMember, ModifyConvName, GetMemberPermissions, SetMemberPermissions], - _copy = fromList [SetMemberPermissions] + copy = fromList [SetMemberPermissions] } ) ( Just @@ -339,9 +339,9 @@ testObject_TeamMember_team_19 = mkTeamMember (Id (fromJust (UUID.fromString "00000003-0000-0002-0000-000200000008"))) ( Permissions - { _self = + { self = fromList [AddTeamMember, ModifyConvName, GetBilling, SetBilling, SetMemberPermissions], - _copy = fromList [SetMemberPermissions] + copy = fromList [SetMemberPermissions] } ) ( Just @@ -356,8 +356,8 @@ testObject_TeamMember_team_20 = mkTeamMember (Id (fromJust (UUID.fromString "00000005-0000-0007-0000-000100000005"))) ( Permissions - { _self = fromList [CreateConversation, AddTeamMember, ModifyConvName, GetBilling], - _copy = fromList [] + { self = fromList [CreateConversation, AddTeamMember, ModifyConvName, GetBilling], + copy = fromList [] } ) ( Just 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-subsystems/default.nix b/libs/wire-subsystems/default.nix index 24a8758783a..17d3f312a20 100644 --- a/libs/wire-subsystems/default.nix +++ b/libs/wire-subsystems/default.nix @@ -8,9 +8,11 @@ , amazonka-core , amazonka-ses , async +, attoparsec , base , base16-bytestring , bilge +, bloodhound , bytestring , bytestring-conversion , cassandra-util @@ -51,12 +53,15 @@ , 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 @@ -65,6 +70,7 @@ , string-conversions , template , text +, text-icu-translit , time , time-out , time-units @@ -92,9 +98,11 @@ mkDerivation { amazonka-core amazonka-ses async + attoparsec base base16-bytestring bilge + bloodhound bytestring bytestring-conversion cassandra-util @@ -130,15 +138,19 @@ 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 @@ -162,6 +174,7 @@ mkDerivation { base bilge bytestring + cassandra-util containers crypton data-default diff --git a/libs/wire-subsystems/src/Wire/BlockListStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/BlockListStore/Cassandra.hs index d8e0e0f077a..f00431d8201 100644 --- a/libs/wire-subsystems/src/Wire/BlockListStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/BlockListStore/Cassandra.hs @@ -10,13 +10,13 @@ import Wire.BlockListStore (BlockListStore (..)) import Wire.UserKeyStore interpretBlockListStoreToCassandra :: - forall m r a. - (MonadClient m, Member (Embed m) r) => - Sem (BlockListStore ': r) a -> - Sem r a -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 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 cbb4f769837..07003fed93c 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs @@ -112,6 +112,12 @@ data GalleyAPIAccess m a where GetAllTeamFeaturesForUser :: Maybe UserId -> 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 dcafabedce8..340018628f7 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs @@ -81,6 +81,7 @@ 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'' @@ -453,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, diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore.hs new file mode 100644 index 00000000000..92e3c7ea97e --- /dev/null +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore.hs @@ -0,0 +1,43 @@ +{-# 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.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) + +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..f299017ce2b --- /dev/null +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs @@ -0,0 +1,500 @@ +{-# 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.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 + +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/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..fc4d15e434e --- /dev/null +++ b/libs/wire-subsystems/src/Wire/UserSearch/Types.hs @@ -0,0 +1,207 @@ +{-# 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) diff --git a/libs/wire-subsystems/src/Wire/UserStore.hs b/libs/wire-subsystems/src/Wire/UserStore.hs index 6429d60c597..1c33abd7e42 100644 --- a/libs/wire-subsystems/src/Wire/UserStore.hs +++ b/libs/wire-subsystems/src/Wire/UserStore.hs @@ -1,8 +1,8 @@ {-# LANGUAGE TemplateHaskell #-} -{-# OPTIONS_GHC -Wno-ambiguous-fields #-} module Wire.UserStore where +import Cassandra (PageWithState (..), PagingState) import Data.Default import Data.Handle import Data.Id @@ -12,6 +12,7 @@ import Polysemy.Error import Wire.API.User 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). @@ -46,6 +47,8 @@ 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 + 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 ()) diff --git a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs index 9ff0e903abf..ee7f51bab8c 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -1,6 +1,7 @@ module Wire.UserStore.Cassandra (interpretUserStoreCassandra) where import Cassandra +import Cassandra.Exec (prepared) import Data.Handle import Data.Id import Database.CQL.Protocol @@ -11,27 +12,64 @@ import Polysemy.Error import Wire.API.User hiding (DeleteUser) 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 - GetUsers uids -> embed $ getUsersImpl uids - 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 + 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 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) + FROM user + |] + updateUserImpl :: UserId -> StoredUserUpdate -> Client () updateUserImpl uid update = retry x5 $ batch do 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..2334260f447 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs @@ -0,0 +1,200 @@ +{-# 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} + +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) + } + +{- 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) + ) + +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 + ) + + asRecord + ( u, + mTeam, tTeam, + name, tName, + status, tStatus, + handle, tHandle, + email, tEmail, + colour, tColour, + activated, tActivated, + service, tService, + managedBy, tManagedBy, + ssoId, tSsoId, + emailUnvalidated, tEmailUnvalidated + ) = 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 + } +{- 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 + ] + +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 b8c6256122f..95cfcc4ad6e 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem.hs @@ -8,16 +8,29 @@ module Wire.UserSubsystem where import Data.Default +import Data.Domain import Data.Handle (Handle) import Data.HavePendingInvitations import Data.Id import Data.Qualified +import Data.Range +import Data.Set qualified as Set import Imports import Polysemy +import Polysemy.Error import Wire.API.Federation.Error +import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti (TeamStatus) +import Wire.API.Team.Feature +import Wire.API.Team.Member (IsPerm (..), TeamMember) +import Wire.API.Team.Permission import Wire.API.User +import Wire.API.User.Search import Wire.Arbitrary +import Wire.GalleyAPIAccess (GalleyAPIAccess) +import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.UserKeyStore (EmailKey, emailKeyOrig) +import Wire.UserSearch.Types +import Wire.UserSubsystem.Error (UserSubsystemError (..)) -- | 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.) @@ -111,6 +124,22 @@ data UserSubsystem m a where 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) + -- | This function exists to support migration in this susbystem, after the + -- migration this would just be an internal detail of the subsystem + InternalUpdateSearchIndex :: UserId -> UserSubsystem m () -- | the return type of 'CheckHandle' data CheckHandleResp @@ -132,6 +161,9 @@ getLocalUserProfile :: (Member UserSubsystem r) => Local UserId -> Sem r (Maybe getLocalUserProfile targetUser = listToMaybe <$> getLocalUserProfiles ((: []) <$> targetUser) +getLocalUser :: (Member UserSubsystem r) => Local UserId -> Sem r (Maybe User) +getLocalUser = (selfUser <$$>) . getSelfProfile + getLocalAccountBy :: (Member UserSubsystem r) => HavePendingInvitations -> @@ -150,3 +182,47 @@ getLocalAccountBy includePendingInvitations uid = getLocalUserAccountByUserKey :: (Member UserSubsystem r) => Local EmailKey -> Sem r (Maybe UserAccount) getLocalUserAccountByUserKey q@(tUnqualified -> ek) = listToMaybe . fmap (.account) <$> getExtendedAccountsByEmailNoFilter (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 + +-- | 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 UserSubsystemError) r + ) => + UserId -> + TeamId -> + Permissions -> + Sem r () +ensurePermissionToAddUser u t inviteePerms = do + minviter <- GalleyAPIAccess.getTeamMember u t + unless (check minviter) $ + throw UserSubsystemInsufficientTeamPermissions + where + check :: Maybe TeamMember -> Bool + check (Just inviter) = + hasPermission inviter AddTeamMember + && all (mayGrantPermission inviter) (Set.toList (inviteePerms.self)) + 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..22b1a8e44ec 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs @@ -16,6 +16,7 @@ data UserSubsystemError | UserSubsystemHandleExists | UserSubsystemInvalidHandle | UserSubsystemProfileNotFound + | UserSubsystemInsufficientTeamPermissions deriving (Eq, Show) userSubsystemErrorToHttpError :: UserSubsystemError -> HttpError @@ -28,5 +29,6 @@ userSubsystemErrorToHttpError = UserSubsystemHandleExists -> errorToWai @E.HandleExists UserSubsystemInvalidHandle -> errorToWai @E.InvalidHandle UserSubsystemHandleManagedByScim -> errorToWai @E.HandleManagedByScim + UserSubsystemInsufficientTeamPermissions -> errorToWai @'E.InsufficientTeamPermissions instance Exception UserSubsystemError diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index d91c6c33dd0..769be28ee74 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, @@ -8,6 +9,7 @@ where import Control.Lens (view) import Control.Monad.Trans.Maybe +import Data.Domain import Data.Handle (Handle) import Data.Handle qualified as Handle import Data.Id @@ -15,32 +17,52 @@ import Data.Json.Util import Data.LegalHold import Data.List.Extra (nubOrd) import Data.Qualified +import Data.Range import Data.Time.Clock +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 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.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.SearchVisibility +import Wire.API.User as User +import Wire.API.User.Search import Wire.API.UserEvent import Wire.Arbitrary 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.InvitationCodeStore (InvitationCodeStore, lookupInvitationByEmail) 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.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 @@ -48,12 +70,11 @@ import Witherable (wither) data UserSubsystemConfig = UserSubsystemConfig { emailVisibilityConfig :: EmailVisibilityConfig, - defaultLocale :: Locale + defaultLocale :: Locale, + searchSameTeamOnly :: Bool } - deriving (Show) - -instance Arbitrary UserSubsystemConfig where - arbitrary = UserSubsystemConfig <$> arbitrary <*> arbitrary + deriving (Show, Generic) + deriving (Arbitrary) via (GenericUniform UserSubsystemConfig) runUserSubsystem :: ( Member GalleyAPIAccess r, @@ -70,6 +91,9 @@ runUserSubsystem :: RunClient (fedM 'Brig), FederationMonad fedM, Typeable fedM, + Member IndexedUserStore r, + Member FederationConfigStore r, + Member Metrics r, Member (TinyLog) r, Member InvitationCodeStore r ) => @@ -78,9 +102,9 @@ runUserSubsystem :: runUserSubsystem cfg = runInputConst cfg . interpretUserSubsystem . raiseUnder interpretUserSubsystem :: - ( Member GalleyAPIAccess r, - Member UserStore r, + ( Member UserStore r, Member UserKeyStore r, + Member GalleyAPIAccess r, Member BlockListStore r, Member (Concurrency 'Unsafe) r, Member (Error FederationError) r, @@ -93,6 +117,9 @@ interpretUserSubsystem :: RunClient (fedM 'Brig), FederationMonad fedM, Typeable fedM, + Member IndexedUserStore r, + Member FederationConfigStore r, + Member Metrics r, Member InvitationCodeStore r, Member TinyLog r ) => @@ -113,6 +140,14 @@ interpretUserSubsystem = interpret \case 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 isBlockedImpl :: (Member BlockListStore r) => EmailAddress -> Sem r Bool isBlockedImpl = BlockList.exists . mkEmailKey @@ -340,10 +375,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`. @@ -385,7 +420,9 @@ updateUserProfileImpl :: ( Member UserStore r, Member (Error UserSubsystemError) r, Member Events r, - Member GalleyAPIAccess r + Member GalleyAPIAccess r, + Member IndexedUserStore r, + Member Metrics r ) => Local UserId -> Maybe ConnId -> @@ -397,6 +434,8 @@ 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 @@ -435,7 +474,9 @@ updateHandleImpl :: ( Member (Error UserSubsystemError) r, Member GalleyAPIAccess r, Member Events r, - Member UserStore r + Member UserStore r, + Member IndexedUserStore r, + Member Metrics r ) => Local UserId -> Maybe ConnId -> @@ -452,6 +493,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 @@ -493,6 +535,230 @@ checkHandlesImpl check num = reverse <$> collectFree [] check num 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 = + 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, @@ -563,7 +829,7 @@ getExtendedAccountsByImpl (tSplit -> (domain, MkGetBy {includePendingInvitations -- validEmailIdentity, anyEmailIdentity? Just email -> do hasInvitation <- isJust <$> lookupInvitationByEmail email - gcHack hasInvitation (userId account.accountUser) + gcHack hasInvitation (User.userId account.accountUser) pure hasInvitation Nothing -> error "getExtendedAccountsByImpl: should never happen, user invited via scim always has an email" NoPendingInvitations -> pure False diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index d5d4a789261..415b0117d05 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -63,7 +63,9 @@ import Wire.DeleteQueue.InMemory import Wire.Events import Wire.FederationAPIAccess import Wire.FederationAPIAccess.Interpreter as FI +import Wire.FederationConfigStore import Wire.GalleyAPIAccess +import Wire.IndexedUserStore import Wire.InternalEvent hiding (DeleteUser) import Wire.InvitationCodeStore import Wire.MockInterpreters @@ -72,6 +74,8 @@ import Wire.MockInterpreters.InvitationCodeStore (inMemoryInvitationCodeStoreInt import Wire.PasswordResetCodeStore 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.StoredUser import Wire.UserKeyStore @@ -134,6 +138,8 @@ type MiniBackendEffects = State [StoredUser], UserKeyStore, State (Map EmailKey UserId), + IndexedUserStore, + FederationConfigStore, DeleteQueue, Events, State [InternalNotification], @@ -142,6 +148,7 @@ type MiniBackendEffects = Now, Input UserSubsystemConfig, Input (Local ()), + Metrics, FederationAPIAccess MiniFederationMonad, TinyLog, Concurrency 'Unsafe @@ -376,6 +383,7 @@ interpretMaybeFederationStackState maybeFederationAPIAccess localBackend teamMem sequentiallyPerformConcurrency . noOpLogger . maybeFederationAPIAccess + . ignoreMetrics . runInputConst (toLocalUnsafe (Domain "localdomain") ()) . runInputConst cfg . interpretNowConst (UTCTime (ModifiedJulianDay 0) 0) @@ -384,6 +392,8 @@ interpretMaybeFederationStackState maybeFederationAPIAccess localBackend teamMem . evalState [] . miniEventInterpreter . inMemoryDeleteQueueInterpreter + . runFederationConfigStoreInMemory + . inMemoryIndexedUserStoreInterpreter . liftUserKeyStoreState . inMemoryUserKeyStoreInterpreter . liftUserStoreState diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs index ebd8d4d1ee5..e975ac6a06c 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs @@ -7,8 +7,10 @@ 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 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 1cfe41aeaf6..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 @@ -16,4 +18,8 @@ miniGalleyAPIAccess :: miniGalleyAPIAccess member configs = interpret $ \case GetTeamMember _ _ -> pure member 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/IndexedUserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/IndexedUserStore.hs new file mode 100644 index 00000000000..60a186a6d8c --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/IndexedUserStore.hs @@ -0,0 +1,15 @@ +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" diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs index bb3ad07afc6..a1e0e5d96e1 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs @@ -1,7 +1,10 @@ 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,6 +13,7 @@ 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. @@ -31,6 +35,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 @@ -58,6 +66,26 @@ inMemoryUserStoreInterpreter = interpret $ \case IsActivated uid -> isActivatedImpl uid LookupLocale uid -> lookupLocaleImpl uid +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 + } + lookupLocaleImpl :: (Member (State [StoredUser]) r) => UserId -> Sem r (Maybe ((Maybe Language, Maybe Country))) lookupLocaleImpl uid = do users <- get 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/UserSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs index a7975a867a1..ee679f39123 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs @@ -56,8 +56,9 @@ spec = describe "UserSubsystem.Interpreter" do target1 = mkUserIds remoteDomain1 targetUsers1 target2 = mkUserIds remoteDomain2 targetUsers2 localBackend = def {users = [viewer] <> localTargetUsers} + config = UserSubsystemConfig visibility miniLocale False retrievedProfiles = - runFederationStack localBackend federation Nothing (UserSubsystemConfig visibility miniLocale) $ + runFederationStack localBackend federation Nothing config $ getUserProfiles (toLocalUnsafe localDomain viewer.id) (localTargets <> target1 <> target2) @@ -83,7 +84,7 @@ 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 localBackend = def {users = [viewer]} result = run @@ -102,49 +103,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 $ @@ -156,7 +153,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 localBackend = def {users = [viewer]} retrievedProfilesWithErrors :: ([(Qualified UserId, FederationError)], [UserProfile]) = runFederationStack localBackend federation Nothing config $ @@ -177,7 +174,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 localBackend = def {users = [viewer]} retrievedProfilesWithErrors :: ([(Qualified UserId, FederationError)], [UserProfile]) = runFederationStack localBackend online Nothing config $ @@ -197,7 +194,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 localBackend = def {users = [viewer]} retrievedProfilesWithErrors :: ([(Qualified UserId, FederationError)], [UserProfile]) = runFederationStack localBackend online Nothing config $ @@ -282,7 +279,7 @@ 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 + let config = UserSubsystemConfig visibility locale False alice = alice' { email = Just email, @@ -317,7 +314,7 @@ spec = describe "UserSubsystem.Interpreter" do prop "GetBy userId works for pending if explicitly queried" $ \(PendingNotEmptyIdentityStoredUser alice') email teamId invitationInfo localDomain visibility locale -> - let config = UserSubsystemConfig visibility locale + let config = UserSubsystemConfig visibility locale True alice = alice' { email = Just email, @@ -351,7 +348,7 @@ spec = describe "UserSubsystem.Interpreter" do in result === [mkAccountFromStored 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 + let config = UserSubsystemConfig visibility locale True alice = alice' { email = Just email, @@ -387,7 +384,7 @@ spec = describe "UserSubsystem.Interpreter" do prop "GetBy handle works for pending if explicitly queried" $ \(PendingNotEmptyIdentityStoredUser alice') handl email teamId invitationInfo localDomain visibility locale -> - let config = UserSubsystemConfig visibility locale + let config = UserSubsystemConfig visibility locale True alice = alice' { email = Just email, @@ -423,7 +420,7 @@ spec = describe "UserSubsystem.Interpreter" do prop "GetBy email does not filter by pending, missing identity or expired invitations" $ \(alice' :: StoredUser) email localDomain visibility locale -> - let config = UserSubsystemConfig visibility locale + let config = UserSubsystemConfig visibility locale True alice = alice' {email = Just email} localBackend = def @@ -437,7 +434,7 @@ spec = describe "UserSubsystem.Interpreter" do prop "GetBy userId does not return missing identity users, pending invitation off" $ \(NotPendingEmptyIdentityStoredUser alice) localDomain visibility locale -> - let config = UserSubsystemConfig visibility locale + let config = UserSubsystemConfig visibility locale True getBy = toLocalUnsafe localDomain $ def @@ -452,7 +449,7 @@ spec = describe "UserSubsystem.Interpreter" do prop "GetBy userId does not return missing identity users, pending invtation on" $ \(NotPendingEmptyIdentityStoredUser alice) localDomain visibility locale -> - let config = UserSubsystemConfig visibility locale + let config = UserSubsystemConfig visibility locale True getBy = toLocalUnsafe localDomain $ def @@ -467,7 +464,7 @@ spec = describe "UserSubsystem.Interpreter" do 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 + let config = UserSubsystemConfig visibility locale True emailKey = mkEmailKey email getBy = toLocalUnsafe localDomain $ @@ -496,7 +493,7 @@ spec = describe "UserSubsystem.Interpreter" do 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 + let config = UserSubsystemConfig visibility locale True emailKey = mkEmailKey email getBy = toLocalUnsafe localDomain $ @@ -517,7 +514,7 @@ spec = describe "UserSubsystem.Interpreter" do 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 + let config = UserSubsystemConfig visibility locale True emailKey = mkEmailKey email getBy = toLocalUnsafe localDomain $ @@ -551,7 +548,7 @@ spec = describe "UserSubsystem.Interpreter" do 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 + let config = UserSubsystemConfig visibility locale True emailKey = mkEmailKey email getBy = toLocalUnsafe localDomain $ diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index db0cb43facb..294ead8c5de 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -89,10 +89,18 @@ library 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.InvitationCodeStore Wire.InvitationCodeStore.Cassandra @@ -113,8 +121,12 @@ library Wire.StoredUser 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 @@ -134,9 +146,11 @@ library , amazonka-core , amazonka-ses , async + , attoparsec , base , base16-bytestring , bilge + , bloodhound , bytestring , bytestring-conversion , cassandra-util @@ -172,15 +186,19 @@ 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 @@ -219,8 +237,10 @@ test-suite wire-subsystems-tests Wire.MockInterpreters.EmailSubsystem Wire.MockInterpreters.Error Wire.MockInterpreters.Events + Wire.MockInterpreters.FederationConfigStore Wire.MockInterpreters.GalleyAPIAccess Wire.MockInterpreters.HashPassword + Wire.MockInterpreters.IndexedUserStore Wire.MockInterpreters.InvitationCodeStore Wire.MockInterpreters.Now Wire.MockInterpreters.PasswordResetCodeStore @@ -234,6 +254,7 @@ test-suite wire-subsystems-tests Wire.MockInterpreters.VerificationCodeStore Wire.NotificationSubsystem.InterpreterSpec Wire.PropertySubsystem.InterpreterSpec + Wire.UserSearch.TypesSpec Wire.UserStoreSpec Wire.UserSubsystem.InterpreterSpec Wire.VerificationCodeSubsystem.InterpreterSpec @@ -245,6 +266,7 @@ test-suite wire-subsystems-tests , base , bilge , bytestring + , cassandra-util , containers , crypton , data-default diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 6d8885cfb71..14e026face8 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -114,8 +114,6 @@ library Brig.DeleteQueue.Interpreter Brig.Effects.ConnectionStore Brig.Effects.ConnectionStore.Cassandra - Brig.Effects.FederationConfigStore - Brig.Effects.FederationConfigStore.Cassandra Brig.Effects.JwtTools Brig.Effects.PublicKeyBundle Brig.Effects.SFT @@ -123,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 @@ -190,24 +186,19 @@ library Brig.Team.API 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 @@ -238,7 +229,6 @@ library , conduit >=1.2.8 , containers >=0.5 , cookie >=0.4 - , cql , cryptobox-haskell >=0.1.1 , crypton , currency-codes >=2.0 @@ -297,7 +287,6 @@ library , resourcet >=1.1 , retry >=0.7 , safe-exceptions >=0.1 - , saml2-web-sso , schema-profunctor , servant , servant-openapi3 @@ -311,7 +300,6 @@ library , template >=0.2 , template-haskell , text >=0.11 - , text-icu-translit >=0.1 , time >=1.1 , time-out , time-units @@ -340,13 +328,11 @@ library 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 @@ -355,7 +341,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 @@ -506,7 +491,6 @@ executable brig-schema ghc-options: -funbox-strict-fields -Wredundant-constraints -threaded default-extensions: TemplateHaskell build-depends: - , base , brig , cassandra-util , extended @@ -525,7 +509,6 @@ test-suite brig-tests Test.Brig.Effects.Delay Test.Brig.InternalNotification Test.Brig.MLS - Test.Brig.User.Search.Index.Types hs-source-dirs: test/unit ghc-options: -funbox-strict-fields -threaded -with-rtsopts=-N diff --git a/services/brig/default.nix b/services/brig/default.nix index 61ebf704692..a52f5219eb8 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -29,7 +29,6 @@ , conduit , containers , cookie -, cql , cryptobox-haskell , crypton , currency-codes @@ -130,7 +129,6 @@ , template-haskell , temporary , text -, text-icu-translit , time , time-out , time-units @@ -190,7 +188,6 @@ mkDerivation { conduit containers cookie - cql cryptobox-haskell crypton currency-codes @@ -249,7 +246,6 @@ mkDerivation { resourcet retry safe-exceptions - saml2-web-sso schema-profunctor servant servant-openapi3 @@ -263,7 +259,6 @@ mkDerivation { template template-haskell text - text-icu-translit time time-out time-units diff --git a/services/brig/src/Brig/API/Auth.hs b/services/brig/src/Brig/API/Auth.hs index eace1f730de..e61f5409994 100644 --- a/services/brig/src/Brig/API/Auth.hs +++ b/services/brig/src/Brig/API/Auth.hs @@ -23,7 +23,6 @@ import Brig.API.Types import Brig.API.User import Brig.App import Brig.Data.User qualified as User -import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Options import Brig.User.Auth qualified as Auth import Brig.ZAuth hiding (Env, settings) @@ -36,14 +35,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 @@ -54,10 +52,9 @@ import Wire.API.User.Auth.ReAuth import Wire.API.User.Auth.Sso 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,14 +97,11 @@ 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 Events r, + Member (Input (Local ())) r, Member UserSubsystem r, Member VerificationCodeSubsystem r ) => @@ -142,7 +130,8 @@ logout uts (Just at) = Auth.logout (List1 uts) at !>> zauthError changeSelfEmailH :: ( Member BlockListStore r, Member UserKeyStore r, - Member EmailSubsystem r + Member EmailSubsystem r, + Member UserSubsystem r ) => [Either Text SomeUserToken] -> Maybe (Either Text SomeAccessToken) -> @@ -183,12 +172,9 @@ removeCookies lusr (RemoveCookies pw lls ids) = 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 ) => LegalHoldLogin -> Handler r SomeAccess @@ -199,11 +185,8 @@ 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 UserSubsystem r, + Member Events r ) => SsoLogin -> Maybe Bool -> diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index cc513f7bfa8..41b001858f9 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) @@ -84,13 +83,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 @@ -109,13 +105,14 @@ import Wire.API.UserEvent import Wire.API.UserMap (QualifiedUserMap (QualifiedUserMap, qualifiedUserMap), UserMap (userMap)) 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) @@ -165,16 +162,12 @@ lookupLocalPubClientsBulk = lift . wrapClient . Data.lookupPubClientsBulk addClient :: ( Member GalleyAPIAccess r, - Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member UserSubsystem r, - Member TinyLog r, Member DeleteQueue r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member Events r ) => Local UserId -> Maybe ConnId -> @@ -187,14 +180,10 @@ 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 VerificationCodeSubsystem r ) => @@ -224,7 +213,7 @@ addClientWithReAuthPolicy policy luid@(tUnqualified -> u) con new = 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 -> @@ -517,19 +506,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' @@ -538,24 +517,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) => diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index cb3ed7e3dd0..f5efefff8b7 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -41,7 +41,6 @@ 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 @@ -68,6 +67,7 @@ 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 diff --git a/services/brig/src/Brig/API/Connection/Remote.hs b/services/brig/src/Brig/API/Connection/Remote.hs index 03b650731c8..3a812f665d6 100644 --- a/services/brig/src/Brig/API/Connection/Remote.hs +++ b/services/brig/src/Brig/API/Connection/Remote.hs @@ -28,7 +28,6 @@ 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 @@ -51,6 +50,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 diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index 370761fb73f..fd891967200 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -31,8 +31,6 @@ 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 @@ -73,11 +71,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 @@ -169,7 +170,7 @@ getUserByHandle domain handle = do pure Nothing Just ownerId -> do localOwnerId <- qualifyLocal ownerId - liftSem $ getLocalUserProfile localOwnerId + liftSem $ UserSubsystem.getLocalUserProfile localOwnerId getUsersByIds :: (Member UserSubsystem r) => @@ -178,7 +179,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 @@ -254,7 +255,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 [] diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index cdb90eb56a6..d6b5a13235f 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -37,16 +37,7 @@ 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.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 @@ -55,9 +46,8 @@ 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 (preview, to, view, _Just) import Data.ByteString.Conversion (toByteString) @@ -72,7 +62,6 @@ 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 @@ -104,6 +93,15 @@ import Wire.BlockListStore (BlockListStore) import Wire.DeleteQueue (DeleteQueue) import Wire.EmailSending (EmailSending) import Wire.EmailSubsystem (EmailSubsystem) +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.InvitationCodeStore import Wire.NotificationSubsystem @@ -111,7 +109,6 @@ import Wire.PasswordResetCodeStore (PasswordResetCodeStore) import Wire.PropertySubsystem import Wire.Rpc import Wire.Sem.Concurrency -import Wire.Sem.Paging.Cassandra (InternalPaging) import Wire.UserKeyStore import Wire.UserStore import Wire.UserSubsystem @@ -125,13 +122,10 @@ servantSitemap :: ( 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 UserStore r, @@ -140,9 +134,11 @@ servantSitemap :: Member Rpc r, Member TinyLog r, Member (UserPendingActivationStore p) r, + Member (Input (Local ())) r, Member EmailSending r, Member EmailSubsystem r, Member VerificationCodeSubsystem r, + Member Events r, Member PasswordResetCodeStore r, Member PropertySubsystem r, Member (Input TeamTemplates) r @@ -189,14 +185,13 @@ 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 PropertySubsystem r, + Member Events r, Member PasswordResetCodeStore r, Member InvitationCodeStore r ) => @@ -242,21 +237,19 @@ teamsAPI :: Member (UserPendingActivationStore p) r, Member BlockListStore r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member UserKeyStore r, Member (Concurrency 'Unsafe) r, Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, Member InvitationCodeStore r, - Member (ConnectionStore InternalPaging) r, Member EmailSending r, Member UserSubsystem r, - Member (Input TeamTemplates) r + Member Events r, + Member (Input TeamTemplates) r, + Member (Input (Local ())) 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 @"suspend-team" Team.suspendTeam @@ -276,12 +269,8 @@ clientAPI = Named @"update-client-last-active" updateClientLastActive authAPI :: ( Member GalleyAPIAccess r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, + Member Events r, Member UserSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member VerificationCodeSubsystem r ) => ServerT BrigIRoutes.AuthAPI (Handler r) @@ -297,7 +286,10 @@ authAPI = qualifyLocal uid >>= \luid -> reauthenticate luid reauth ) -federationRemotesAPI :: (Member FederationConfigStore r) => ServerT BrigIRoutes.FederationRemotesAPI (Handler r) +federationRemotesAPI :: + ( Member FederationConfigStore r + ) => + ServerT BrigIRoutes.FederationRemotesAPI (Handler r) federationRemotesAPI = Named @"add-federation-remotes" addFederationRemote :<|> Named @"get-federation-remotes" getFederationRemotes @@ -314,7 +306,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 () @@ -329,7 +326,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 () @@ -408,8 +409,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 @@ -417,14 +416,10 @@ 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 Events r, Member UserSubsystem r, Member VerificationCodeSubsystem r ) => @@ -440,31 +435,11 @@ addClientInternalH usr mSkipReAuth new connId = do 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 @@ -482,15 +457,12 @@ createUserNoVerify :: Member GalleyAPIAccess r, Member (UserPendingActivationStore p) r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, + Member Events r, Member InvitationCodeStore r, Member UserKeyStore r, Member UserSubsystem r, Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member PasswordResetCodeStore r, - Member (ConnectionStore InternalPaging) r + Member PasswordResetCodeStore r ) => NewUser -> (Handler r) (Either RegisterError SelfProfile) @@ -508,14 +480,10 @@ createUserNoVerify uData = lift . runExceptT $ do 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 PasswordResetCodeStore r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r, + Member PasswordResetCodeStore r ) => NewUserSpar -> (Handler r) (Either CreateUserSparError SelfProfile) @@ -538,10 +506,8 @@ deleteUserNoAuthH :: Member UserStore r, Member TinyLog r, Member UserKeyStore r, + Member Events r, Member UserSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member PropertySubsystem r ) => UserId -> @@ -554,14 +520,33 @@ deleteUserNoAuthH uid = do AccountAlreadyDeleted -> pure UserResponseAccountAlreadyDeleted AccountDeleted -> pure UserResponseAccountDeleted -changeSelfEmailMaybeSendH :: (Member BlockListStore 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 BlockListStore r, Member UserKeyStore r, Member EmailSubsystem r) => UserId -> MaybeSendEmail -> EmailAddress -> 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 @@ -624,12 +609,8 @@ getPasswordResetCode 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 -> @@ -701,39 +682,34 @@ 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 diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index f3dc6426492..1f313fb01c4 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -41,8 +41,6 @@ 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.ConnectionStore (ConnectionStore) -import Brig.Effects.FederationConfigStore (FederationConfigStore) import Brig.Effects.JwtTools (JwtTools) import Brig.Effects.PublicKeyBundle (PublicKeyBundle) import Brig.Effects.SFT @@ -55,8 +53,6 @@ import Brig.Team.Template (TeamTemplates) import Brig.Types.Activation (ActivationPair) import Brig.Types.Intra (UserAccount (UserAccount, accountUser)) 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 @@ -86,15 +82,16 @@ import Data.Qualified import Data.Range import Data.Schema () import Data.Text.Encoding qualified as Text -import Data.Time.Clock (UTCTime) 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.Fail (Fail) -import Polysemy.Input (Input) +import Polysemy.Input import Polysemy.TinyLog (TinyLog) import Servant hiding (Handler, JSON, addHeader, respond) import Servant qualified @@ -144,6 +141,7 @@ 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.AuthenticationSubsystem (AuthenticationSubsystem, createPasswordResetCode, resetPassword) @@ -152,6 +150,8 @@ import Wire.DeleteQueue import Wire.EmailSending (EmailSending) import Wire.EmailSubsystem import Wire.Error +import Wire.Events (Events) +import Wire.FederationConfigStore (FederationConfigStore) import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.InvitationCodeStore @@ -162,11 +162,12 @@ 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.UserKeyStore +import Wire.UserSearch.Types import Wire.UserStore (UserStore) import Wire.UserSubsystem hiding (checkHandle, checkHandles) import Wire.UserSubsystem qualified as User +import Wire.UserSubsystem.Error import Wire.VerificationCode import Wire.VerificationCodeGen import Wire.VerificationCodeSubsystem @@ -263,37 +264,37 @@ internalEndpointsSwaggerDocsAPI service examplePort swagger Nothing = servantSitemap :: forall r p. - ( Member BlockListStore r, - Member DeleteQueue r, - Member (Concurrency 'Unsafe) r, - Member (ConnectionStore InternalPaging) r, + ( Member (Concurrency 'Unsafe) r, Member (Embed HttpClientIO) r, Member (Embed IO) r, + Member (Error UserSubsystemError) r, Member Fail r, - Member FederationConfigStore r, Member (Input (Local ())) r, + Member (Input TeamTemplates) r, + Member (UserPendingActivationStore p) r, Member AuthenticationSubsystem r, - Member (Input UTCTime) r, - Member Jwk r, + Member DeleteQueue r, + Member EmailSending r, + Member EmailSubsystem r, + Member Events r, + Member FederationConfigStore r, Member GalleyAPIAccess r, + Member InvitationCodeStore 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 UserKeyStore r, + Member UserStore r, + Member UserSubsystem r, Member VerificationCodeSubsystem r, - Member PropertySubsystem r, - Member PasswordResetCodeStore r, - Member InvitationCodeStore r, - Member (Input TeamTemplates) r + Member BlockListStore r ) => ServerT BrigAPI (Handler r) servantSitemap = @@ -403,7 +404,7 @@ servantSitemap = :<|> 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 @"search-contacts" (callsFed (exposeAnnotations searchUsersHandler)) propertiesAPI :: ServerT PropertiesAPI (Handler r) propertiesAPI = @@ -430,7 +431,7 @@ servantSitemap = searchAPI :: ServerT SearchAPI (Handler r) searchAPI = - Named @"browse-team" teamUserSearch + Named @"browse-team" browseTeamHandler authAPI :: ServerT AuthAPI (Handler r) authAPI = @@ -462,6 +463,21 @@ servantSitemap = --------------------------------------------------------------------------- -- Handlers +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 @@ -559,16 +575,12 @@ 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 UserSubsystem r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member Events r, + Member UserSubsystem r ) => Local UserId -> ConnId -> @@ -692,14 +704,11 @@ createUser :: Member GalleyAPIAccess r, Member InvitationCodeStore 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 EmailSending r @@ -916,14 +925,9 @@ removePhone :: UserId -> Handler r (Maybe Public.RemoveIdentityError) 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) @@ -1032,6 +1036,16 @@ sendActivationCode ac = do checkAllowlist email 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 @@ -1182,13 +1196,11 @@ 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 UserSubsystem r, Member VerificationCodeSubsystem r, - Member PropertySubsystem r + Member PropertySubsystem r, + Member Events r ) => Local UserId -> Public.DeleteUser -> @@ -1200,14 +1212,12 @@ verifyDeleteUser :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member UserStore r, - Member UserSubsystem r, Member TinyLog r, - Member (Input (Local ())) r, Member UserKeyStore r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member VerificationCodeSubsystem r, - Member PropertySubsystem r + Member PropertySubsystem r, + Member UserSubsystem r, + Member Events r ) => Public.VerifyDeleteUser -> Handler r () @@ -1218,7 +1228,8 @@ updateUserEmail :: ( Member BlockListStore r, Member UserKeyStore r, Member GalleyAPIAccess r, - Member EmailSubsystem r + Member EmailSubsystem r, + Member UserSubsystem r ) => UserId -> UserId -> @@ -1249,13 +1260,9 @@ updateUserEmail zuserId emailOwnerId (Public.EmailUpdate email) = do activate :: ( Member GalleyAPIAccess r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member UserSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member PasswordResetCodeStore r, - Member (ConnectionStore InternalPaging) r + Member Events r, + Member PasswordResetCodeStore r ) => Public.ActivationKey -> Public.ActivationCode -> @@ -1268,13 +1275,9 @@ activate k c = do activateKey :: ( Member GalleyAPIAccess r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, + Member Events r, Member UserSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member PasswordResetCodeStore r, - Member (ConnectionStore InternalPaging) r + Member PasswordResetCodeStore r ) => Public.Activate -> (Handler r) ActivationRespWithStatus diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index f18ae4b8d30..17331d18ce1 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -81,7 +81,6 @@ import Brig.Data.Connection (countConnections) import Brig.Data.Connection qualified as Data import Brig.Data.User import Brig.Data.User qualified as Data -import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.UserPendingActivationStore (UserPendingActivation (..), UserPendingActivationStore) import Brig.Effects.UserPendingActivationStore qualified as UserPendingActivationStore import Brig.IO.Intra qualified as Intra @@ -89,7 +88,6 @@ import Brig.Options hiding (internalEvents) 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 @@ -108,12 +106,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 (addUTCTime) 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 @@ -138,6 +136,8 @@ 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.InvitationCodeStore (InvitationCodeStore, StoredInvitation, StoredInvitationInfo) import Wire.InvitationCodeStore qualified as InvitationCodeStore @@ -146,7 +146,6 @@ 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.UserKeyStore import Wire.UserStore import Wire.UserSubsystem as User @@ -191,13 +190,9 @@ 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 Events r ) => NewUserSpar -> ExceptT CreateUserSparError (AppT r) CreateUserResult @@ -219,7 +214,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 (accountUser account)) pure account @@ -266,11 +262,8 @@ createUser :: 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 InvitationCodeStore r ) => @@ -335,7 +328,7 @@ createUser new = do 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 (accountUser account)) pure account @@ -405,7 +398,8 @@ createUser new = do 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 do Log.info $ @@ -536,7 +530,16 @@ checkRestrictedUserCreation new = do -- | Call 'changeEmail' and process result: if email changes to itself, succeed, if not, send -- validation email. -changeSelfEmail :: (Member BlockListStore r, Member UserKeyStore r, Member EmailSubsystem r) => UserId -> EmailAddress -> 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 -> @@ -544,7 +547,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 @@ -581,14 +584,9 @@ changeEmail u email updateOrigin = do -- 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) () @@ -598,7 +596,13 @@ 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) + -- FUTUREWORK: This doesn't delete user's email address from the index, + -- which is a bug, reported here: + -- https://wearezeta.atlassian.net/browse/WPB-11122. + -- + -- Calling User.internalUpdateSearchIndex here wouldn't work as explained + -- in the ticket. + liftSem $ Events.generateUserEvent uid Nothing (emailRemoved uid e) Just _ -> throwE LastIdentity Nothing -> throwE NoIdentity @@ -627,12 +631,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 -> @@ -647,15 +648,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 -> @@ -665,7 +663,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 = @@ -684,11 +683,7 @@ 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 ) => @@ -702,13 +697,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 Events r, Member PasswordResetCodeStore r, - Member UserSubsystem r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r ) => ActivationTarget -> ActivationCode -> @@ -751,11 +742,8 @@ 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) @@ -763,10 +751,12 @@ onActivated (AccountActivated account) = liftSem $ do let uid = userId (accountUser 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) + User.internalUpdateSearchIndex uid + Events.generateUserEvent uid Nothing $ UserActivated (accountUser account) pure (uid, userIdentity (accountUser 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) @@ -878,13 +868,11 @@ 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 Events r, Member UserSubsystem r, Member PropertySubsystem r ) => @@ -953,11 +941,9 @@ 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 Events r, Member UserSubsystem r, Member PropertySubsystem r ) => @@ -983,10 +969,8 @@ 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 Events r, Member UserSubsystem r, Member PropertySubsystem r ) => @@ -1034,11 +1018,10 @@ deleteAccount :: 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 PropertySubsystem r, + Member UserSubsystem r, + Member Events r ) => UserAccount -> Sem r () @@ -1056,7 +1039,8 @@ deleteAccount (accountUser -> user) = do 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. diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 3d85954f241..80d38fbb7ea 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -39,6 +39,7 @@ module Brig.App cargoholdEndpoint, federator, casClient, + indexEnv, userTemplates, providerTemplates, teamTemplates, diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 65cf6132c0d..45722ef86df 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -5,8 +5,6 @@ import Brig.App as App import Brig.DeleteQueue.Interpreter as DQ 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) @@ -16,6 +14,7 @@ import Brig.IO.Intra (runEvents) import Brig.Options (ImplicitNoFederationRestriction (federationDomainConfig), 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, (^.)) @@ -50,10 +49,14 @@ 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.InvitationCodeStore (InvitationCodeStore) import Wire.InvitationCodeStore.Cassandra (interpretInvitationCodeStoreToCassandra) import Wire.NotificationSubsystem @@ -73,6 +76,8 @@ 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) @@ -110,6 +115,7 @@ type BrigCanonicalEffects = HashPassword, UserKeyStore, UserStore, + IndexedUserStore, SessionStore, PasswordStore, VerificationCodeStore, @@ -138,6 +144,7 @@ type BrigCanonicalEffects = GalleyAPIAccess, EmailSending, Rpc, + Metrics, Embed Cas.Client, Error ParseException, Error ErrorCall, @@ -157,7 +164,8 @@ runBrigToIO e (AppT ma) = do let userSubsystemConfig = UserSubsystemConfig { emailVisibilityConfig = e ^. settings . Opt.emailVisibility, - defaultLocale = e ^. settings . to Opt.setDefaultUserLocale + defaultLocale = e ^. settings . to Opt.setDefaultUserLocale, + searchSameTeamOnly = e ^. settings . Opt.searchSameTeamOnly . to (fromMaybe False) } federationApiAccessConfig = FederationAPIAccessConfig @@ -172,6 +180,21 @@ runBrigToIO e (AppT ma) = do maxValueLength = fromMaybe Opt.defMaxValueLen $ e ^. settings . Opt.propertyMaxValueLen, maxProperties = 16 } + mainESEnv = e ^. App.indexEnv . to idxElastic + indexedUserStoreConfig = + IndexedUserStoreConfig + { conn = + ESConn + { env = mainESEnv, + indexName = e ^. App.indexEnv . to idxName + }, + additionalConn = + (e ^. App.indexEnv . to idxAdditionalName) <&> \additionalIndexName -> + ESConn + { env = e ^. App.indexEnv . to idxAdditionalElastic . to (fromMaybe mainESEnv), + indexName = additionalIndexName + } + } ( either throwM pure <=< ( runFinal . unsafelyPerformConcurrency @@ -185,6 +208,7 @@ runBrigToIO e (AppT ma) = do . mapError @ErrorCall SomeException . mapError @ParseException SomeException . interpretClientToIO (e ^. casClient) + . runMetricsToIO . runRpcWithHttp (e ^. httpManager) (e ^. App.requestId) . emailSendingInterpreter e . interpretGalleyAPIAccessToRpc (e ^. disabledVersions) (e ^. galleyEndpoint) @@ -193,11 +217,11 @@ runBrigToIO e (AppT ma) = do . runDelay . nowToIOAction (e ^. currentTime) . userPendingActivationStoreToCassandra - . interpretBlockListStoreToCassandra @Cas.Client + . interpretBlockListStoreToCassandra (e ^. casClient) . interpretJwtTools . interpretPublicKeyBundle . interpretJwk - . interpretFederationDomainConfig (e ^. settings . federationStrategy) (foldMap (remotesMapFromCfgFile . fmap (.federationDomainConfig)) (e ^. settings . federationDomainConfigs)) + . interpretFederationDomainConfig (e ^. casClient) (e ^. settings . federationStrategy) (foldMap (remotesMapFromCfgFile . fmap (.federationDomainConfig)) (e ^. settings . federationDomainConfigs)) . runGundeckAPIAccess (e ^. gundeckEndpoint) . runNotificationSubsystemGundeck (defaultNotificationSubsystemConfig (e ^. App.requestId)) . runInputConst (teamTemplatesNoLocale e) @@ -213,6 +237,7 @@ runBrigToIO e (AppT ma) = do . interpretVerificationCodeStoreCassandra (e ^. casClient) . interpretPasswordStore (e ^. casClient) . interpretSessionStoreCassandra (e ^. casClient) + . interpretIndexedUserStoreES indexedUserStoreConfig . interpretUserStoreCassandra (e ^. casClient) . interpretUserKeyStoreCassandra (e ^. casClient) . runHashPassword diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index f92e18bef38..8309190c88f 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -20,7 +20,7 @@ -- FUTUREWORK: Move to Brig.User.RPC or similar. module Brig.IO.Intra ( -- * Pushing & Journaling Events - onUserEvent, + sendUserEvent, onConnectionEvent, onPropertyEvent, onClientEvent, @@ -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 @@ -109,7 +108,7 @@ import Wire.Sem.Paging.Cassandra (InternalPaging) ----------------------------------------------------------------------------- -- Event Handlers -onUserEvent :: +sendUserEvent :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member TinyLog r, @@ -121,9 +120,8 @@ 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) runEvents :: @@ -137,7 +135,7 @@ runEvents :: 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 :: @@ -192,36 +190,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, - isJust eupTeam - ] - when interesting $ Search.reindex orig - journalEvent :: (MonadReader Env m, MonadIO m) => UserId -> UserEvent -> m () journalEvent orig e = case e of UserActivated acc -> 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/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index 8fa8e91ac7e..031de77e11f 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -19,7 +19,6 @@ 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 @@ -29,19 +28,18 @@ 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 @@ -50,18 +48,17 @@ import Wire.UserSubsystem -- -- 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 PropertySubsystem r, Member UserSubsystem r, - Member (ConnectionStore InternalPaging) r, - Member PropertySubsystem r + Member Events r ) => InternalNotification -> Sem r () diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index b7ed00018e1..fcc674600f8 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -42,7 +42,6 @@ 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.ZAuth qualified as ZAuth import Cassandra (MonadClient) import Control.Error (throwE) @@ -81,6 +80,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) @@ -123,6 +123,8 @@ import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess 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 @@ -147,7 +149,10 @@ botAPI = :<|> Named @"bot-get-user-clients" botGetUserClients servicesAPI :: - (Member GalleyAPIAccess r, Member DeleteQueue r) => + ( Member GalleyAPIAccess r, + Member DeleteQueue r, + Member (Error UserSubsystemError) r + ) => ServerT ServicesAPI (Handler r) servicesAPI = Named @"post-provider-services" addService @@ -163,7 +168,12 @@ 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 EmailSending r, + Member VerificationCodeSubsystem r + ) => + ServerT ProviderAPI (Handler r) providerAPI = Named @"provider-register" newAccount :<|> Named @"provider-activate" activateAccountKey @@ -177,13 +187,23 @@ providerAPI = :<|> Named @"provider-get-account" getAccount :<|> Named @"provider-get-profile" getProviderProfile -internalProviderAPI :: (Member GalleyAPIAccess r, Member VerificationCodeSubsystem r) => ServerT BrigIRoutes.ProviderAPI (Handler r) +internalProviderAPI :: + ( Member GalleyAPIAccess r, + Member VerificationCodeSubsystem r + ) => + ServerT BrigIRoutes.ProviderAPI (Handler r) internalProviderAPI = Named @"get-provider-activation-code" getActivationCodeH -------------------------------------------------------------------------------- -- 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 VerificationCodeSubsystem r + ) => + Public.NewProvider -> + (Handler r) Public.NewProviderResponse newAccount new = do guardSecondFactorDisabled Nothing let email = (Public.newProviderEmail new) @@ -579,14 +599,22 @@ 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 guardSecondFactorDisabled (Just uid) 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 diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 255cc56a6dc..bdb0e911b45 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -33,13 +33,10 @@ import Brig.API.User qualified as API import Brig.API.Util (logEmail, logInvitationCode) import Brig.App as App import Brig.Data.User as User -import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) -import Brig.IO.Intra qualified as Intra import Brig.Options import Brig.Team.Email import Brig.Team.Template -import Brig.Team.Util (ensurePermissionToAddUser, ensurePermissions) import Brig.Types.Team (TeamSize) import Brig.User.Search.TeamSize qualified as TeamSize import Control.Lens (view, (^.)) @@ -53,11 +50,11 @@ import Data.Text.Ascii import Data.Text.Encoding (encodeUtf8) import Data.Text.Lazy qualified as LT import Data.Text.Lazy qualified as Text -import Data.Time.Clock (UTCTime) import Data.Tuple.Extra import Imports hiding (head) -import Network.Wai.Utilities hiding (code, message) +import Network.Wai.Utilities hiding (Error, code, message) import Polysemy +import Polysemy.Error import Polysemy.Input (Input, input) import Polysemy.TinyLog (TinyLog) import Polysemy.TinyLog qualified as Log @@ -87,17 +84,19 @@ import Wire.BlockListStore import Wire.EmailSending (EmailSending) import Wire.EmailSubsystem.Template import Wire.Error +import Wire.Events (Events) +import Wire.Events qualified as Events import Wire.GalleyAPIAccess (GalleyAPIAccess, ShowOrHideInvitationUrl (..)) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.InvitationCodeStore (InvitationCodeStore (..), PaginatedResult (..), StoredInvitation (..)) import Wire.InvitationCodeStore qualified as Store import Wire.InvitationCodeStore.Cassandra qualified as Store (mkInvitationCode) -import Wire.NotificationSubsystem import Wire.PasswordStore import Wire.Sem.Concurrency -import Wire.Sem.Paging.Cassandra (InternalPaging) import Wire.UserKeyStore import Wire.UserSubsystem +import Wire.UserSubsystem qualified as User +import Wire.UserSubsystem.Error servantAPI :: ( Member GalleyAPIAccess r, @@ -106,28 +105,36 @@ servantAPI :: Member Store.InvitationCodeStore r, Member EmailSending r, Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member PasswordStore r, - Member (Input TeamTemplates) r + Member (Input TeamTemplates) r, + Member Events r, + Member (Error UserSubsystemError) r ) => ServerT TeamsAPI (Handler r) servantAPI = Named @"send-team-invitation" createInvitation - :<|> Named @"get-team-invitations" listInvitations - :<|> Named @"get-team-invitation" getInvitation - :<|> Named @"delete-team-invitation" deleteInvitation + :<|> 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" getInvitationByCode - :<|> Named @"head-team-invitations" headInvitationByEmail + :<|> Named @"head-team-invitations" (lift . liftSem . headInvitationByEmail) :<|> Named @"get-team-size" teamSizePublic :<|> Named @"accept-team-invitation" acceptTeamInvitationByPersonalUser -teamSizePublic :: (Member GalleyAPIAccess r) => UserId -> TeamId -> (Handler r) TeamSize +teamSizePublic :: + ( Member GalleyAPIAccess r, + Member (Error UserSubsystemError) r + ) => + UserId -> + TeamId -> + (Handler r) TeamSize teamSizePublic uid tid = do - ensurePermissions uid tid [AddTeamMember] -- limit this to team admins to reduce risk of involuntary DOS attacks + -- limit this to team admins to reduce risk of involuntary DOS attacks + lift . liftSem $ ensurePermissions uid tid [AddTeamMember] teamSize tid teamSize :: TeamId -> (Handler r) TeamSize @@ -156,7 +163,8 @@ createInvitation :: Member EmailSending r, Member TinyLog r, Member (Input (Local ())) r, - Member (Input TeamTemplates) r + Member (Input TeamTemplates) r, + Member (Error UserSubsystemError) r ) => UserId -> TeamId -> @@ -168,7 +176,7 @@ createInvitation uid tid body = 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 + lift . liftSem $ ensurePermissionToAddUser uid tid inviteePerms pure $ CreateInvitationInviter uid from let context = @@ -320,14 +328,17 @@ isPersonalUser uke = do && isNothing account.accountUser.userTeam deleteInvitation :: - (Member GalleyAPIAccess r, Member InvitationCodeStore r) => + ( Member GalleyAPIAccess r, + Member InvitationCodeStore r, + Member (Error UserSubsystemError) r + ) => UserId -> TeamId -> InvitationId -> - (Handler r) () + Sem r () deleteInvitation uid tid iid = do ensurePermissions uid tid [AddTeamMember] - lift . liftSem $ Store.deleteInvitation tid iid + Store.deleteInvitation tid iid listInvitations :: forall r. @@ -336,23 +347,24 @@ listInvitations :: Member InvitationCodeStore r, Member (Input TeamTemplates) r, Member (Input (Local ())) r, - Member UserSubsystem r + Member UserSubsystem r, + Member (Error UserSubsystemError) r ) => UserId -> TeamId -> Maybe InvitationId -> Maybe (Range 1 500 Int32) -> - (Handler r) Public.InvitationList + Sem r Public.InvitationList listInvitations uid tid startingId mSize = do ensurePermissions uid tid [AddTeamMember] - showInvitationUrl <- lift $ liftSem $ GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid + showInvitationUrl <- GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid let toInvitations is = mapM (toInvitationHack showInvitationUrl) is - lift (liftSem $ Store.lookupInvitationsPaginated mSize tid startingId) >>= \case + Store.lookupInvitationsPaginated mSize tid startingId >>= \case PaginatedResultHasMore storedInvs -> do - invs <- lift . liftSem $ toInvitations storedInvs + invs <- toInvitations storedInvs pure $ InvitationList invs True PaginatedResult storedInvs -> do - invs <- lift . liftSem $ toInvitations storedInvs + 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. @@ -447,21 +459,22 @@ getInvitation :: ( Member GalleyAPIAccess r, Member InvitationCodeStore r, Member TinyLog r, - Member (Input TeamTemplates) r + Member (Input TeamTemplates) r, + Member (Error UserSubsystemError) r ) => UserId -> TeamId -> InvitationId -> - (Handler r) (Maybe Public.Invitation) + Sem r (Maybe Public.Invitation) getInvitation uid tid iid = do ensurePermissions uid tid [AddTeamMember] - invitationM <- lift . liftSem $ Store.lookupInvitation tid iid + invitationM <- Store.lookupInvitation tid iid case invitationM of Nothing -> pure Nothing Just invitation -> do - showInvitationUrl <- lift . liftSem $ GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid - maybeUrl <- lift . liftSem $ mkInviteUrl showInvitationUrl tid invitation.code + showInvitationUrl <- GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid + maybeUrl <- mkInviteUrl showInvitationUrl tid invitation.code pure $ Just (Store.invitationFromStored maybeUrl invitation) getInvitationByCode :: @@ -472,18 +485,19 @@ getInvitationByCode c = do inv <- lift . liftSem $ Store.lookupInvitationByCode c maybe (throwStd $ errorToWai @'E.InvalidInvitationCode) (pure . Store.invitationFromStored Nothing) inv -headInvitationByEmail :: (Member InvitationCodeStore r, Member TinyLog r) => EmailAddress -> (Handler r) Public.HeadInvitationByEmailResult +headInvitationByEmail :: + (Member InvitationCodeStore r, Member TinyLog r) => + EmailAddress -> + Sem r Public.HeadInvitationByEmailResult headInvitationByEmail email = - lift $ - liftSem $ - Store.lookupInvitationCodesByEmail email >>= \case - [] -> pure Public.InvitationByEmailNotFound - [_code] -> pure Public.InvitationByEmail - (_ : _ : _) -> do - Log.info $ - Log.msg (Log.val "team_invidation_email: multiple pending invites from different teams for the same email") - . Log.field "email" (show email) - pure Public.InvitationByEmailMoreThanOne + Store.lookupInvitationCodesByEmail email >>= \case + [] -> pure Public.InvitationByEmailNotFound + [_code] -> pure Public.InvitationByEmail + (_ : _ : _) -> do + Log.info $ + Log.msg (Log.val "team_invidation_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 @@ -498,13 +512,11 @@ getInvitationByEmail email = do 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 InvitationCodeStore r ) => TeamId -> @@ -521,13 +533,10 @@ suspendTeam tid = do 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 @@ -541,13 +550,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 -> @@ -566,14 +572,9 @@ acceptTeamInvitationByPersonalUser :: forall r. ( Member UserSubsystem r, Member GalleyAPIAccess r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, - Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member InvitationCodeStore r, - Member PasswordStore r + Member PasswordStore r, + Member Events r ) => Local UserId -> AcceptTeamInvitation -> @@ -596,7 +597,8 @@ acceptTeamInvitationByPersonalUser luid req = do lift $ do wrapClient $ User.updateUserTeam uid tid liftSem $ Store.deleteInvitation inv.teamId inv.invitationId - liftSem $ Intra.onUserEvent uid Nothing (teamUpdated uid tid) + liftSem $ User.internalUpdateSearchIndex uid + liftSem $ Events.generateUserEvent uid Nothing (teamUpdated uid tid) where checkPassword = do p <- diff --git a/services/brig/src/Brig/Team/Util.hs b/services/brig/src/Brig/Team/Util.hs deleted file mode 100644 index a838a3c5fe8..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 Control.Error -import Control.Lens -import Data.HavePendingInvitations -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/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 03b4fd7895a..5511a7750b2 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -41,7 +41,6 @@ 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 @@ -63,7 +62,7 @@ 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, (~~)) @@ -74,11 +73,10 @@ import Wire.API.User import Wire.API.User.Auth import Wire.API.User.Auth.LegalHold import Wire.API.User.Auth.Sso +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) @@ -92,16 +90,13 @@ 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 UserKeyStore r, Member UserStore r, + Member VerificationCodeSubsystem r, + Member (Input (Local ())) r, Member UserSubsystem r, - Member VerificationCodeSubsystem r + Member Events r ) => Login -> CookieType -> @@ -202,11 +197,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) -> @@ -243,12 +235,9 @@ revokeAccess luid@(tUnqualified -> u) pw cc ll = do -- 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 -> @@ -273,11 +262,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 -> @@ -390,11 +376,8 @@ 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 ) => SsoLogin -> CookieType -> @@ -416,12 +399,9 @@ ssoLogin (SsoLogin uid label) typ = do -- | 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 Events r ) => LegalHoldLogin -> CookieType -> diff --git a/services/brig/src/Brig/User/Search/Index.hs b/services/brig/src/Brig/User/Search/Index.hs index afaca2554fd..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,283 +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, \ - \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) \ - \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, \ - \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) \ - \FROM user" - -type Activated = Bool - -type ReindexRow = - ( 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) - ) - --- 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, _f23) = f2 - -reindexRowToIndexUser :: forall m. (MonadThrow m) => ReindexRow -> SearchVisibilityInbound -> m IndexUser -reindexRowToIndexUser - ( u, - mteam, - tTeam, - 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, - v <$> tTeam - ] - 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 @@ -971,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 2630842be4d..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 EmailAddress, - _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 EmailAddress - } - -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 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) - --- 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 index dce653ab03b..6121ec38178 100644 --- a/services/brig/src/Brig/User/Search/TeamSize.hs +++ b/services/brig/src/Brig/User/Search/TeamSize.hs @@ -28,6 +28,7 @@ import Control.Monad.Catch (throwM) import Data.Id import Database.Bloodhound qualified as ES import Imports hiding (log, searchable) +import Wire.IndexedUserStore (IndexedUserStoreError (..)) teamSize :: (MonadIndexIO m) => TeamId -> m TeamSize teamSize t = liftIndexIO $ do 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/test/unit/Run.hs b/services/brig/test/unit/Run.hs index 6d658acb536..a371d3130cc 100644 --- a/services/brig/test/unit/Run.hs +++ b/services/brig/test/unit/Run.hs @@ -25,7 +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.User.Search.Index.Types qualified import Test.Tasty main :: IO () @@ -33,8 +32,7 @@ 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.MLS.tests, Test.Brig.InternalNotification.tests 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 5e6af3a3d0d..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 $ 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\"}" - -indexUser1 :: IndexUser -indexUser1 = docToIndex userDoc1 diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 0fb76ff1384..abec40e3606 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -580,7 +580,6 @@ executable galley-migrate-data , exceptions , extended , imports - , lens , optparse-applicative , text , time 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/Teams.hs b/services/galley/src/Galley/API/Teams.hs index 02994a77a90..df3015be471 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -1256,8 +1256,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 diff --git a/services/galley/src/Galley/Cassandra/Team.hs b/services/galley/src/Galley/Cassandra/Team.hs index ef5a1b96b5f..484c769ab0c 100644 --- a/services/galley/src/Galley/Cassandra/Team.hs +++ b/services/galley/src/Galley/Cassandra/Team.hs @@ -326,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 From e28d6fb9ee2c60ae9702408f8b7c48c29dd62747 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 19 Sep 2024 11:57:37 +0200 Subject: [PATCH 10/15] WPB-11000 Test password reset with wrong key/code should fail (#4249) --- changelog.d/5-internal/WPB-11000 | 1 + integration/default.nix | 2 + integration/integration.cabal | 2 + integration/test/API/Brig.hs | 20 +++ integration/test/API/BrigInternal.hs | 5 + integration/test/Test/PasswordReset.hs | 105 +++++++++++++++ integration/test/Testlib/HTTP.hs | 6 + services/brig/brig.cabal | 1 - services/brig/test/integration/API/User.hs | 2 - .../integration/API/User/PasswordReset.hs | 127 ------------------ 10 files changed, 141 insertions(+), 130 deletions(-) create mode 100644 changelog.d/5-internal/WPB-11000 create mode 100644 integration/test/Test/PasswordReset.hs delete mode 100644 services/brig/test/integration/API/User/PasswordReset.hs diff --git a/changelog.d/5-internal/WPB-11000 b/changelog.d/5-internal/WPB-11000 new file mode 100644 index 00000000000..d489cc80d7e --- /dev/null +++ b/changelog.d/5-internal/WPB-11000 @@ -0,0 +1 @@ +Additional test for password reset, port tests to new integration test suite diff --git a/integration/default.nix b/integration/default.nix index abff642f97d..37d66c8daf5 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -19,6 +19,7 @@ , Cabal , case-insensitive , containers +, cookie , cql , cql-io , crypton @@ -115,6 +116,7 @@ mkDerivation { bytestring-conversion case-insensitive containers + cookie cql cql-io crypton diff --git a/integration/integration.cabal b/integration/integration.cabal index faf1a6a4867..a4351796175 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -140,6 +140,7 @@ library Test.MLS.Unreachable Test.Notifications Test.OAuth + Test.PasswordReset Test.Presence Test.Property Test.Provider @@ -193,6 +194,7 @@ library , bytestring-conversion , case-insensitive , containers + , cookie , cql , cql-io , crypton diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index fae907cd2e3..d3e268197ab 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -795,3 +795,23 @@ listInvitations :: (HasCallStack, MakesValue user) => user -> String -> App Resp 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) diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index 38fe56ac943..7d1ca70230d 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -298,3 +298,8 @@ getActivationCode :: (HasCallStack, MakesValue domain) => domain -> String -> Ap 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)] 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/Testlib/HTTP.hs b/integration/test/Testlib/HTTP.hs index ae15b01adb1..ab7e7d237bf 100644 --- a/integration/test/Testlib/HTTP.hs +++ b/integration/test/Testlib/HTTP.hs @@ -30,6 +30,7 @@ import Testlib.Assertions import Testlib.Env import Testlib.JSON import Testlib.Types +import Web.Cookie import Prelude splitHttpPath :: String -> [String] @@ -89,6 +90,11 @@ 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 diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 14e026face8..52642552923 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -376,7 +376,6 @@ executable brig-integration API.User.Client API.User.Connection API.User.Handles - API.User.PasswordReset API.User.RichInfo API.User.Util API.UserPendingActivation diff --git a/services/brig/test/integration/API/User.hs b/services/brig/test/integration/API/User.hs index d791df93082..35cf4aef598 100644 --- a/services/brig/test/integration/API/User.hs +++ b/services/brig/test/integration/API/User.hs @@ -26,7 +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.RichInfo qualified import API.User.Util import Bilge hiding (accept, timeout) @@ -67,7 +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.RichInfo.tests cl at conf p b c g ] 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 034c6c40ece..00000000000 --- a/services/brig/test/integration/API/User/PasswordReset.hs +++ /dev/null @@ -1,127 +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 Util.Timeout -import Wire.API.User -import Wire.API.User.Auth - -tests :: - DB.ClientState -> - ConnectionLimit -> - 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 - (MkLogin (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)) - ) From 12f8e16f0e4257565e76e7f9b5e9d3cb107f5e18 Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Thu, 19 Sep 2024 12:23:53 +0200 Subject: [PATCH 11/15] [chore] Removed implicit failures in favour of explicit error handling. (#4254) --- services/brig/src/Brig/API/Federation.hs | 10 ++-- services/brig/src/Brig/API/MLS/KeyPackages.hs | 44 +++++++++------- services/brig/src/Brig/API/Public.hs | 10 ++-- .../brig/src/Brig/CanonicalInterpreter.hs | 3 -- services/galley/src/Galley/API/Federation.hs | 31 +++++++----- services/galley/src/Galley/API/MLS/Message.hs | 50 +++++++++---------- 6 files changed, 78 insertions(+), 70 deletions(-) diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index fd891967200..1420eacc5f8 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -50,7 +50,6 @@ import Gundeck.Types.Push qualified as Push import Imports hiding ((\\)) import Network.Wai.Utilities.Error ((!>>)) import Polysemy -import Polysemy.Fail (Fail) import Servant (ServerT) import Servant.API import Wire.API.Connection @@ -89,7 +88,6 @@ federationSitemap :: Member NotificationSubsystem r, Member UserSubsystem r, Member UserStore r, - Member Fail r, Member DeleteQueue r ) => ServerT FederationAPI (Handler r) @@ -196,7 +194,13 @@ claimMultiPrekeyBundle :: Handler r UserClientPrekeyMap claimMultiPrekeyBundle _ uc = API.claimLocalMultiPrekeyBundles LegalholdPlusFederationNotImplemented uc !>> clientError -fedClaimKeyPackages :: (Member Fail r, Member GalleyAPIAccess r, Member UserStore r) => 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 diff --git a/services/brig/src/Brig/API/MLS/KeyPackages.hs b/services/brig/src/Brig/API/MLS/KeyPackages.hs index 33dcbcc90a1..f0e96bc1576 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages.hs @@ -45,7 +45,6 @@ import Data.Qualified import Data.Set qualified as Set import Imports import Polysemy (Member) -import Polysemy.Fail (Fail) import Wire.API.Federation.API import Wire.API.Federation.API.Brig import Wire.API.MLS.CipherSuite @@ -56,7 +55,7 @@ import Wire.API.Team.LegalHold import Wire.API.User.Client import Wire.GalleyAPIAccess (GalleyAPIAccess, getUserLegalholdStatus) import Wire.StoredUser -import Wire.UserStore (UserStore, getUsers) +import Wire.UserStore (UserStore, getUser) uploadKeyPackages :: Local UserId -> ClientId -> KeyPackageUpload -> Handler r () uploadKeyPackages lusr cid kps = do @@ -66,7 +65,9 @@ uploadKeyPackages lusr cid kps = do lift . wrapClient $ Data.insertKeyPackages (tUnqualified lusr) cid kps' claimKeyPackages :: - (Member GalleyAPIAccess r, Member UserStore r, Member Fail r) => + ( Member GalleyAPIAccess r, + Member UserStore r + ) => Local UserId -> Maybe ClientId -> Qualified UserId -> @@ -84,7 +85,9 @@ claimKeyPackages lusr mClient target mSuite = do claimLocalKeyPackages :: forall r. - (Member GalleyAPIAccess r, Member UserStore r, Member Fail r) => + ( Member GalleyAPIAccess r, + Member UserStore r + ) => Qualified UserId -> Maybe ClientId -> CipherSuiteTag -> @@ -95,9 +98,9 @@ claimLocalKeyPackages qusr skipOwn suite target = do -- 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 legalholded user to an MLS + -- This way we prevent both locally and on the remote to add a user under legalhold to an MLS -- conversation - assertUserNotLegalholded + assertUserNotUnderLegalHold -- skip own client when the target is the requesting user itself let own = guard (qusr == tUntagged target) *> skipOwn @@ -121,20 +124,23 @@ claimLocalKeyPackages qusr skipOwn suite target = do uncurry (KeyPackageBundleEntry (tUntagged target) c) <$> wrapClientM (Data.claimKeyPackage target c suite) - assertUserNotLegalholded :: ExceptT ClientError (AppT r) () - assertUserNotLegalholded = do + assertUserNotUnderLegalHold :: ExceptT ClientError (AppT r) () + assertUserNotUnderLegalHold = do -- this is okay because there can only be one StoredUser per UserId - [su] <- lift $ liftSem $ getUsers [tUnqualified target] - 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 () + 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 -> diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 1f313fb01c4..e78af13fda1 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -90,8 +90,7 @@ import Network.Wai.Utilities (CacheControl (..), (!>>)) import Network.Wai.Utilities qualified as Utilities import Polysemy import Polysemy.Error -import Polysemy.Fail (Fail) -import Polysemy.Input +import Polysemy.Input (Input) import Polysemy.TinyLog (TinyLog) import Servant hiding (Handler, JSON, addHeader, respond) import Servant qualified @@ -264,15 +263,14 @@ internalEndpointsSwaggerDocsAPI service examplePort swagger Nothing = servantSitemap :: forall r p. - ( Member (Concurrency 'Unsafe) r, - Member (Embed HttpClientIO) r, + ( Member (Embed HttpClientIO) r, Member (Embed IO) r, Member (Error UserSubsystemError) r, - Member Fail r, Member (Input (Local ())) r, Member (Input TeamTemplates) r, Member (UserPendingActivationStore p) r, Member AuthenticationSubsystem r, + Member BlockListStore r, Member DeleteQueue r, Member EmailSending r, Member EmailSubsystem r, @@ -294,7 +292,7 @@ servantSitemap :: Member UserStore r, Member UserSubsystem r, Member VerificationCodeSubsystem r, - Member BlockListStore r + Member (Concurrency 'Unsafe) r ) => ServerT BrigAPI (Handler r) servantSitemap = diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 45722ef86df..936c1c7dd95 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -27,7 +27,6 @@ import Polysemy.Async import Polysemy.Conc import Polysemy.Embed (runEmbedded) import Polysemy.Error (Error, errorToIOFinal, mapError, runError) -import Polysemy.Fail import Polysemy.Input (Input, runInputConst, runInputSem) import Polysemy.TinyLog (TinyLog) import Wire.API.Allowlists (AllowlistEmailDomains) @@ -151,7 +150,6 @@ type BrigCanonicalEffects = Error SomeException, TinyLog, Embed HttpClientIO, - Fail, Embed IO, Race, Async, @@ -201,7 +199,6 @@ runBrigToIO e (AppT ma) = do . asyncToIOFinal . interpretRace . embedToFinal - . failToEmbed @IO -- if a fallible pattern fails, we throw a hard IO error . runEmbedded (runHttpClientIO e) . loggerToTinyLogReqId (e ^. App.requestId) (e ^. applog) . runError @SomeException diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 5d23e68b499..a78652408fe 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -67,7 +67,6 @@ import Gundeck.Types.Push.V2 (RecipientClients (..)) import Imports import Polysemy import Polysemy.Error -import Polysemy.Fail (Fail) import Polysemy.Input import Polysemy.Internal.Kind (Append) import Polysemy.Resource @@ -606,7 +605,6 @@ sendMLSCommitBundle :: Member (Input UTCTime) r, Member LegalHoldStore r, Member MemberStore r, - Member Fail r, Member Resource r, Member TeamStore r, Member P.TinyLog r, @@ -629,17 +627,24 @@ sendMLSCommitBundle remoteDomain msr = handleMLSMessageErrors $ do (ctype, qConvOrSub) <- getConvFromGroupId ibundle.groupId when (qUnqualified qConvOrSub /= msr.convOrSubId) $ throwS @'MLSGroupConversationMismatch -- this cannot throw the error since we always pass the sender which is qualified to be remote - Just resp <- - runErrorS @MLSLegalholdIncompatible $ - postMLSCommitBundle - loc - (tUntagged @QRemote sender) - msr.senderClient - ctype - qConvOrSub - Nothing - ibundle - pure $ MLSMessageResponseUpdates $ map lcuUpdate resp + 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, diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 75de5388c22..0478b06ad83 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -35,7 +35,7 @@ import Data.Json.Util import Data.LegalHold import Data.Qualified import Data.Set qualified as Set -import Data.Tagged (Tagged) +import Data.Tagged import Data.Text.Lazy qualified as LT import Data.Tuple.Extra import Galley.API.Action @@ -60,11 +60,10 @@ import Galley.Effects.ConversationStore import Galley.Effects.FederatorAccess import Galley.Effects.MemberStore import Galley.Effects.SubConversationStore -import Galley.Effects.TeamStore (getUserTeams) +import Galley.Effects.TeamStore qualified as TeamStore import Imports import Polysemy import Polysemy.Error -import Polysemy.Fail import Polysemy.Input import Polysemy.Internal import Polysemy.Output @@ -151,13 +150,12 @@ postMLSMessageFromLocalUser lusr c conn smsg = do pure $ MLSMessageSendingStatus events t postMLSCommitBundle :: - ( HasProposalEffects r, - Members MLSBundleStaticErrors r, - Member Fail r, - Member (ErrorS MLSLegalholdIncompatible) 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 -> @@ -175,13 +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 Fail r, - Member (ErrorS MLSLegalholdIncompatible) r, Member Resource r, - Member SubConversationStore r + Member SubConversationStore r, + Members MLSBundleStaticErrors r, + HasProposalEffects r ) => Local UserId -> ClientId -> @@ -199,13 +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 (ErrorS MLSLegalholdIncompatible) r, Member SubConversationStore r, - Member Random r, - Member Fail r + Members MLSBundleStaticErrors r, + HasProposalEffects r ) => Qualified UserId -> ClientId -> @@ -222,22 +218,24 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do note (mlsProtocolError "Unsupported ciphersuite") $ cipherSuiteTag bundle.groupInfo.value.groupContext.cipherSuite - -- when a user tries to join any mls conversation while being legalholded + -- 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 <- getUserTeams (tUnqualified luid) + 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 - Right resp <- runError @(Tagged TeamMemberNotFound ()) $ getUserStatus luid tid (tUnqualified luid) - case resp.ulhsrStatus of - UserLegalHoldPending -> throwS @MLSLegalholdIncompatible - UserLegalHoldEnabled -> throwS @MLSLegalholdIncompatible - UserLegalHoldDisabled -> pure () - UserLegalHoldNoConsent -> pure () + 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 () From 023769624fd5b348de0319e9183b5a3149a8fbdf Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Thu, 19 Sep 2024 15:48:53 +0200 Subject: [PATCH 12/15] [WPB-10708] personal account to own team (#4251) * Add endpoint to upgrade a personal user to a team * Test upgrading personal user to team * Add CHANGELOG entry --------- Co-authored-by: Paolo Capriotti --- changelog.d/1-api-changes/wpb-10708 | 1 + integration/test/API/Brig.hs | 5 +++ integration/test/API/Galley.hs | 6 +++ integration/test/Test/Teams.hs | 32 +++++++++++++- integration/test/Testlib/Assertions.hs | 3 ++ libs/wire-api/src/Wire/API/Error/Brig.hs | 3 ++ .../src/Wire/API/Routes/Public/Brig.hs | 43 ++++++++++++------- libs/wire-api/src/Wire/API/User.hs | 40 +++++++++++++++++ services/brig/src/Brig/API/Public.hs | 33 +++++++++++--- services/brig/src/Brig/API/Types.hs | 6 --- services/brig/src/Brig/API/User.hs | 42 +++++++++++++++++- 11 files changed, 186 insertions(+), 28 deletions(-) create mode 100644 changelog.d/1-api-changes/wpb-10708 diff --git a/changelog.d/1-api-changes/wpb-10708 b/changelog.d/1-api-changes/wpb-10708 new file mode 100644 index 00000000000..cfbe92afa70 --- /dev/null +++ b/changelog.d/1-api-changes/wpb-10708 @@ -0,0 +1 @@ +Add endpoint to upgrade a personal user to a team owner diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index d3e268197ab..a40dddd3bd9 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -815,3 +815,8 @@ updateEmail :: (HasCallStack, MakesValue user) => user -> String -> String -> St 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"] diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index 6bb55137d82..01df8dc89ea 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -519,6 +519,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 diff --git a/integration/test/Test/Teams.hs b/integration/test/Test/Teams.hs index c54a7b18b46..dbf08e3e2a7 100644 --- a/integration/test/Test/Teams.hs +++ b/integration/test/Test/Teams.hs @@ -20,7 +20,7 @@ module Test.Teams where import API.Brig import API.BrigInternal (createUser, getInvitationCode, refreshIndex) import API.Common -import API.Galley (getTeamMembers) +import API.Galley (getTeam, getTeamMembers) import API.GalleyInternal (setTeamFeatureStatus) import Control.Monad.Codensity (Codensity (runCodensity)) import Control.Monad.Extra (findM) @@ -172,3 +172,33 @@ testTeamUserCannotBeInvited = do (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" diff --git a/integration/test/Testlib/Assertions.hs b/integration/test/Testlib/Assertions.hs index af7d18900e2..c2f9efef3d1 100644 --- a/integration/test/Testlib/Assertions.hs +++ b/integration/test/Testlib/Assertions.hs @@ -206,6 +206,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 -> diff --git a/libs/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index 416e5fecaa2..7846f5c51f5 100644 --- a/libs/wire-api/src/Wire/API/Error/Brig.hs +++ b/libs/wire-api/src/Wire/API/Error/Brig.hs @@ -99,6 +99,7 @@ data BrigError | TooManyProperties | PropertyKeyTooLarge | PropertyValueTooLarge + | UserAlreadyInATeam instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: BrigError) where addToOpenApi = addStaticErrorToSwagger @(MapError e) @@ -295,3 +296,5 @@ type instance MapError 'TooManyProperties = 'StaticError 403 "too-many-propertie 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" 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 7394673620e..6e457fefa6d 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -471,23 +471,36 @@ type UserHandleAPI = ) 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." + :> MakesFederatedCall 'Brig "send-connection-action" + :> "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) diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 2ff0c3eb3e0..4a2f92dd38f 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -46,6 +46,11 @@ module Wire.API.User mkUserProfileWithEmail, userObjectSchema, + -- * UpgradePersonalToTeam + CreateUserTeam (..), + UpgradePersonalToTeamResponses, + UpgradePersonalToTeamError (..), + -- * NewUser NewUserPublic (..), RegisterError (..), @@ -772,6 +777,41 @@ 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 + deriving (Show) + +type UpgradePersonalToTeamResponses = + '[ ErrorResponse UserAlreadyInATeam, + Respond 200 "Team created" CreateUserTeam + ] + +instance + AsUnion + UpgradePersonalToTeamResponses + (Either UpgradePersonalToTeamError CreateUserTeam) + where + toUnion (Left UpgradePersonalToTeamErrorAlreadyInATeam) = + Z (I (dynError @(MapError UserAlreadyInATeam))) + toUnion (Right x) = S (Z (I x)) + + fromUnion (Z (I _)) = Left UpgradePersonalToTeamErrorAlreadyInATeam + fromUnion (S (Z (I x))) = Right x + fromUnion (S (S x)) = case x of {} + data RegisterError = RegisterErrorAllowlistError | RegisterErrorInvalidInvitationCode diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index e78af13fda1..b557bbc2c1e 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -41,6 +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.ConnectionStore import Brig.Effects.JwtTools (JwtTools) import Brig.Effects.PublicKeyBundle (PublicKeyBundle) import Brig.Effects.SFT @@ -82,6 +83,7 @@ import Data.Qualified import Data.Range import Data.Schema () import Data.Text.Encoding qualified as Text +import Data.Time.Clock import Data.ZAuth.Token qualified as ZAuth import FileEmbedLzma import Imports hiding (head) @@ -161,6 +163,7 @@ import Wire.PropertySubsystem import Wire.Sem.Concurrency import Wire.Sem.Jwk (Jwk) import Wire.Sem.Now (Now) +import Wire.Sem.Paging.Cassandra import Wire.UserKeyStore import Wire.UserSearch.Types import Wire.UserStore (UserStore) @@ -267,10 +270,10 @@ servantSitemap :: Member (Embed IO) r, Member (Error UserSubsystemError) r, Member (Input (Local ())) r, + Member (Input UTCTime) r, Member (Input TeamTemplates) r, Member (UserPendingActivationStore p) r, Member AuthenticationSubsystem r, - Member BlockListStore r, Member DeleteQueue r, Member EmailSending r, Member EmailSubsystem r, @@ -292,7 +295,9 @@ servantSitemap :: Member UserStore r, Member UserSubsystem r, Member VerificationCodeSubsystem r, - Member (Concurrency 'Unsafe) r + Member (Concurrency 'Unsafe) r, + Member BlockListStore r, + Member (ConnectionStore InternalPaging) r ) => ServerT BrigAPI (Handler r) servantSitemap = @@ -346,7 +351,8 @@ servantSitemap = accountAPI :: ServerT AccountAPI (Handler r) accountAPI = - Named @"register" (callsFed (exposeAnnotations createUser)) + Named @"upgrade-personal-to-team" upgradePersonalToTeam + :<|> Named @"register" (callsFed (exposeAnnotations createUser)) :<|> Named @"verify-delete" (callsFed (exposeAnnotations verifyDeleteUser)) :<|> Named @"get-activate" (callsFed (exposeAnnotations activate)) :<|> Named @"post-activate" (callsFed (exposeAnnotations activateKey)) @@ -696,6 +702,23 @@ 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 + ) => + 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 BlockListStore r, @@ -768,9 +791,9 @@ createUser (Public.NewUserPublic new) = lift . runExceptT $ do | otherwise = liftSem $ sendActivationMail email name key code locale - sendWelcomeEmail :: (Member EmailSending r) => Public.EmailAddress -> 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 _ -> diff --git a/services/brig/src/Brig/API/Types.hs b/services/brig/src/Brig/API/Types.hs index 8eac26e9861..97d15e8c06b 100644 --- a/services/brig/src/Brig/API/Types.hs +++ b/services/brig/src/Brig/API/Types.hs @@ -58,12 +58,6 @@ 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 diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 17331d18ce1..fecd3001fac 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, @@ -81,6 +82,7 @@ import Brig.Data.Connection (countConnections) import Brig.Data.Connection qualified as Data import Brig.Data.User import Brig.Data.User qualified as Data +import Brig.Effects.ConnectionStore import Brig.Effects.UserPendingActivationStore (UserPendingActivation (..), UserPendingActivationStore) import Brig.Effects.UserPendingActivationStore qualified as UserPendingActivationStore import Brig.IO.Intra qualified as Intra @@ -106,7 +108,7 @@ import Data.List1 as List1 (List1, singleton) import Data.Misc import Data.Qualified import Data.Range -import Data.Time.Clock (addUTCTime) +import Data.Time.Clock import Data.UUID.V4 (nextRandom) import Imports import Network.Wai.Utilities @@ -146,6 +148,7 @@ import Wire.PasswordResetCodeStore (PasswordResetCodeStore) import Wire.PasswordStore (PasswordStore, lookupHashedPassword, upsertHashedPassword) import Wire.PropertySubsystem as PropertySubsystem import Wire.Sem.Concurrency +import Wire.Sem.Paging.Cassandra import Wire.UserKeyStore import Wire.UserStore import Wire.UserSubsystem as User @@ -253,6 +256,43 @@ createUserSpar new = do Team.TeamName nm <- lift $ liftSem $ GalleyAPIAccess.getTeamName tid pure $ CreateUserTeam tid nm +upgradePersonalToTeam :: + forall r. + ( Member GalleyAPIAccess 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 + let mTid = mSelfProfile >>= userTeam . selfUser + when (isJust mTid) $ + throwE UpgradePersonalToTeamErrorAlreadyInATeam + + lift $ do + -- generate team ID + tid <- randomId + + let uid = tUnqualified luid + createUserTeam <- do + liftSem $ GalleyAPIAccess.createTeam uid (bnuTeam bNewTeam) tid + let BindingNewTeam newTeam = bNewTeam.bnuTeam + pure $ CreateUserTeam tid (fromRange (newTeam ^. newTeamName)) + + wrapClient $ updateUserTeam uid tid + liftSem $ Intra.sendUserEvent uid Nothing (teamUpdated uid tid) + initAccountFeatureConfig uid + + pure $! createUserTeam + -- docs/reference/user/registration.md {#RefRegistration} createUser :: forall r p. From 674b067254627dc4e892f18ec4e0f7b40fa5a364 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 19 Sep 2024 16:37:11 +0200 Subject: [PATCH 13/15] Send confirmation email after upgrade to team owner (#4253) * Send confirmation email * Add placeholder templates * Add CHANGELOG entry --- .../2-features/personal-account-to-team-email | 1 + libs/wire-api/src/Wire/API/Team.hs | 2 +- libs/wire-api/src/Wire/API/User.hs | 14 +++++-- .../src/Wire/EmailSubsystem.hs | 1 + .../src/Wire/EmailSubsystem/Interpreter.hs | 37 +++++++++++++++++++ .../src/Wire/EmailSubsystem/Template.hs | 10 +++++ .../en/user/email/upgrade-subject.txt | 1 + .../brig/templates/en/user/email/upgrade.html | 1 + .../brig/templates/en/user/email/upgrade.txt | 22 +++++++++++ services/brig/src/Brig/API/Public.hs | 1 + services/brig/src/Brig/API/User.hs | 18 ++++++++- services/brig/src/Brig/User/Template.hs | 8 ++++ 12 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 changelog.d/2-features/personal-account-to-team-email create mode 100644 services/brig/deb/opt/brig/templates/en/user/email/upgrade-subject.txt create mode 100644 services/brig/deb/opt/brig/templates/en/user/email/upgrade.html create mode 100644 services/brig/deb/opt/brig/templates/en/user/email/upgrade.txt diff --git a/changelog.d/2-features/personal-account-to-team-email b/changelog.d/2-features/personal-account-to-team-email new file mode 100644 index 00000000000..c8bbe2bf91b --- /dev/null +++ b/changelog.d/2-features/personal-account-to-team-email @@ -0,0 +1 @@ +Send confirmation email after adding a personal user to a new team diff --git a/libs/wire-api/src/Wire/API/Team.hs b/libs/wire-api/src/Wire/API/Team.hs index 13c09ab567b..cffcd2bac95 100644 --- a/libs/wire-api/src/Wire/API/Team.hs +++ b/libs/wire-api/src/Wire/API/Team.hs @@ -177,7 +177,7 @@ instance ToSchema TeamList where -------------------------------------------------------------------------------- -- NewTeam -newtype BindingNewTeam = BindingNewTeam (NewTeam ()) +newtype BindingNewTeam = BindingNewTeam {bntTeam :: NewTeam ()} deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema BindingNewTeam) diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 4a2f92dd38f..08a8f758db9 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -791,11 +791,14 @@ instance ToSchema CreateUserTeam where <$> createdTeamId .= field "team_id" schema <*> createdTeamName .= field "team_name" schema -data UpgradePersonalToTeamError = UpgradePersonalToTeamErrorAlreadyInATeam +data UpgradePersonalToTeamError + = UpgradePersonalToTeamErrorAlreadyInATeam + | UpgradePersonalToTeamErrorUserNotFound deriving (Show) type UpgradePersonalToTeamResponses = '[ ErrorResponse UserAlreadyInATeam, + ErrorResponse UserNotFound, Respond 200 "Team created" CreateUserTeam ] @@ -806,11 +809,14 @@ instance where toUnion (Left UpgradePersonalToTeamErrorAlreadyInATeam) = Z (I (dynError @(MapError UserAlreadyInATeam))) - toUnion (Right x) = S (Z (I x)) + 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 x))) = Right x - fromUnion (S (S x)) = case x of {} + 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 diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem.hs index c604fb36ed8..e4090103799 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem.hs @@ -21,5 +21,6 @@ data EmailSubsystem m a where 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 () + SendUpgradePersonalToTeamConfirmationEmail :: EmailAddress -> Name -> Text -> Locale -> EmailSubsystem m () makeSem ''EmailSubsystem diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs index 2fda920c11f..a152b166af1 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs @@ -36,6 +36,7 @@ emailSubsystemInterpreter tpls branding = interpret \case 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 + SendUpgradePersonalToTeamConfirmationEmail email name teamName locale -> sendUpgradePersonalToTeamConfirmationEmailImpl tpls branding email name teamName locale ------------------------------------------------------------------------------- -- Verification Email for @@ -395,6 +396,42 @@ renderDeletionEmail email name cKey cValue DeletionEmailTemplate {..} branding = replace2 "code" = code replace2 x = x +-------------------------------------------------------------------------------- +-- Upgrade personal user to team owner confirmation email + +sendUpgradePersonalToTeamConfirmationEmailImpl :: + (Member EmailSending r) => + Localised UserTemplates -> + TemplateBranding -> + EmailAddress -> + Name -> + Text -> + Locale -> + Sem r () +sendUpgradePersonalToTeamConfirmationEmailImpl userTemplates branding email name teamName locale = do + let tpl = upgradePersonalToTeamEmail . snd $ forLocale (Just locale) userTemplates + sendMail $ renderUpgradePersonalToTeamConfirmationEmail email name teamName tpl branding + +renderUpgradePersonalToTeamConfirmationEmail :: EmailAddress -> Name -> Text -> UpgradePersonalToTeamEmailTemplate -> TemplateBranding -> Mail +renderUpgradePersonalToTeamConfirmationEmail email name _teamName UpgradePersonalToTeamEmailTemplate {..} branding = + (emptyMail from) + { mailTo = [to], + mailHeaders = + [ ("Subject", toStrict subj), + ("X-Zeta-Purpose", "Upgrade") + ], + mailParts = [[plainPart txt, htmlPart html]] + } + where + from = Address (Just upgradePersonalToTeamEmailSenderName) (fromEmail upgradePersonalToTeamEmailSender) + to = mkMimeAddress name email + txt = renderTextWithBranding upgradePersonalToTeamEmailBodyText replace1 branding + html = renderHtmlWithBranding upgradePersonalToTeamEmailBodyHtml replace1 branding + subj = renderTextWithBranding upgradePersonalToTeamEmailSubject replace1 branding + replace1 "email" = fromEmail email + replace1 "name" = fromName name + replace1 x = x + ------------------------------------------------------------------------------- -- MIME Conversions diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs index 818cdca2e9e..f1c7a996f56 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs @@ -35,6 +35,7 @@ module Wire.EmailSubsystem.Template LoginCallTemplate (..), DeletionSmsTemplate (..), DeletionEmailTemplate (..), + UpgradePersonalToTeamEmailTemplate (..), NewClientEmailTemplate (..), SecondFactorVerificationEmailTemplate (..), @@ -105,6 +106,7 @@ data UserTemplates = UserTemplates loginCall :: LoginCallTemplate, deletionSms :: DeletionSmsTemplate, deletionEmail :: DeletionEmailTemplate, + upgradePersonalToTeamEmail :: UpgradePersonalToTeamEmailTemplate, newClientEmail :: NewClientEmailTemplate, verificationLoginEmail :: SecondFactorVerificationEmailTemplate, verificationScimTokenEmail :: SecondFactorVerificationEmailTemplate, @@ -157,6 +159,14 @@ data DeletionEmailTemplate = DeletionEmailTemplate deletionEmailSenderName :: Text } +data UpgradePersonalToTeamEmailTemplate = UpgradePersonalToTeamEmailTemplate + { upgradePersonalToTeamEmailSubject :: Template, + upgradePersonalToTeamEmailBodyText :: Template, + upgradePersonalToTeamEmailBodyHtml :: Template, + upgradePersonalToTeamEmailSender :: EmailAddress, + upgradePersonalToTeamEmailSenderName :: Text + } + data PasswordResetEmailTemplate = PasswordResetEmailTemplate { passwordResetEmailUrl :: Template, passwordResetEmailSubject :: Template, diff --git a/services/brig/deb/opt/brig/templates/en/user/email/upgrade-subject.txt b/services/brig/deb/opt/brig/templates/en/user/email/upgrade-subject.txt new file mode 100644 index 00000000000..8a92b9f9a36 --- /dev/null +++ b/services/brig/deb/opt/brig/templates/en/user/email/upgrade-subject.txt @@ -0,0 +1 @@ +Delete account? \ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/user/email/upgrade.html b/services/brig/deb/opt/brig/templates/en/user/email/upgrade.html new file mode 100644 index 00000000000..690b0104fdd --- /dev/null +++ b/services/brig/deb/opt/brig/templates/en/user/email/upgrade.html @@ -0,0 +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 diff --git a/services/brig/deb/opt/brig/templates/en/user/email/upgrade.txt b/services/brig/deb/opt/brig/templates/en/user/email/upgrade.txt new file mode 100644 index 00000000000..744da7dc05c --- /dev/null +++ b/services/brig/deb/opt/brig/templates/en/user/email/upgrade.txt @@ -0,0 +1,22 @@ +[${brand_logo}] + +${brand_label_url} [${brand_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 [${url}]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 [${forgot}]. + +If you have any questions, please contact us [${support}]. + + +-------------------------------------------------------------------------------- + +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/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index b557bbc2c1e..8d4d35d653f 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -705,6 +705,7 @@ createAccessToken method luid cid proof = do upgradePersonalToTeam :: ( Member (ConnectionStore InternalPaging) r, Member (Embed HttpClientIO) r, + Member EmailSubsystem r, Member GalleyAPIAccess r, Member (Input (Local ())) r, Member (Input UTCTime) r, diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index fecd3001fac..6e516ccca60 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -259,6 +259,7 @@ createUserSpar new = do upgradePersonalToTeam :: forall r. ( Member GalleyAPIAccess r, + Member EmailSubsystem r, Member UserSubsystem r, Member TinyLog r, Member (Embed HttpClientIO) r, @@ -273,8 +274,12 @@ upgradePersonalToTeam :: upgradePersonalToTeam luid bNewTeam = do -- check that the user is not part of a team mSelfProfile <- lift $ liftSem $ getSelfProfile luid - let mTid = mSelfProfile >>= userTeam . selfUser - when (isJust mTid) $ + user <- + maybe + (throwE UpgradePersonalToTeamErrorUserNotFound) + (pure . selfUser) + mSelfProfile + when (isJust user.userTeam) $ throwE UpgradePersonalToTeamErrorAlreadyInATeam lift $ do @@ -291,6 +296,15 @@ upgradePersonalToTeam luid bNewTeam = do liftSem $ Intra.sendUserEvent uid Nothing (teamUpdated uid tid) initAccountFeatureConfig uid + -- send confirmation email + for_ (userEmail user) $ \email -> do + liftSem $ + sendUpgradePersonalToTeamConfirmationEmail + email + user.userDisplayName + bNewTeam.bnuTeam.bntTeam._newTeamName.fromRange + user.userLocale + pure $! createUserTeam -- docs/reference/user/registration.md {#RefRegistration} diff --git a/services/brig/src/Brig/User/Template.hs b/services/brig/src/Brig/User/Template.hs index 0667a4b2cd2..00aa5a5b6d2 100644 --- a/services/brig/src/Brig/User/Template.hs +++ b/services/brig/src/Brig/User/Template.hs @@ -28,6 +28,7 @@ module Brig.User.Template LoginCallTemplate (..), DeletionSmsTemplate (..), DeletionEmailTemplate (..), + UpgradePersonalToTeamEmailTemplate (..), NewClientEmailTemplate (..), SecondFactorVerificationEmailTemplate (..), loadUserTemplates, @@ -109,6 +110,13 @@ loadUserTemplates o = readLocalesDir defLocale templateDir "user" $ \fp -> <*> pure emailSender <*> readText fp "email/sender.txt" ) + <*> ( UpgradePersonalToTeamEmailTemplate + <$> readTemplate fp "email/upgrade-subject.txt" + <*> readTemplate fp "email/upgrade.txt" + <*> readTemplate fp "email/upgrade.html" + <*> pure emailSender + <*> readText fp "email/sender.txt" + ) <*> ( NewClientEmailTemplate <$> readTemplate fp "email/new-client-subject.txt" <*> readTemplate fp "email/new-client.txt" From ef612baa6ac5cb10e7c7902d86f854d9a176c900 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Thu, 19 Sep 2024 22:14:00 +0200 Subject: [PATCH 14/15] [WPB-11186] Translate flaky integration test to /integration. (#4258) Co-authored-by: Leif Battermann --- integration/test/Test/Conversation.hs | 34 +++++++ integration/test/Testlib/Assertions.hs | 9 ++ services/galley/test/integration/API.hs | 88 ------------------- .../test/integration/API/Federation/Util.hs | 28 ------ services/galley/test/integration/API/Util.hs | 7 -- 5 files changed, 43 insertions(+), 123 deletions(-) diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index 844fd6e295e..714a75d7254 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -874,3 +874,37 @@ 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 diff --git a/integration/test/Testlib/Assertions.hs b/integration/test/Testlib/Assertions.hs index c2f9efef3d1..bb4e4a5d573 100644 --- a/integration/test/Testlib/Assertions.hs +++ b/integration/test/Testlib/Assertions.hs @@ -180,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 diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 9d5ef6b1a4d..18ce92d9e20 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -27,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 @@ -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, @@ -249,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 @@ -356,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 $ do - let notParticipatingRemotes = allRemotes \\ participatingRemotes - assertBool "No reachable backend in the test" (not (null participatingRemotes)) - assertBool "No unreachable backend in the test" (not (null notParticipatingRemotes)) - - 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] -- TODO: sometimes, (at least?) one of these users gets a "connection accepted" event. - postCryptoMessageVerifyMsgSentAndRejectIfMissingClient :: TestM () postCryptoMessageVerifyMsgSentAndRejectIfMissingClient = do localDomain <- viewFederationDomain diff --git a/services/galley/test/integration/API/Federation/Util.hs b/services/galley/test/integration/API/Federation/Util.hs index c4e6a41ea49..84fc3acea22 100644 --- a/services/galley/test/integration/API/Federation/Util.hs +++ b/services/galley/test/integration/API/Federation/Util.hs @@ -17,17 +17,10 @@ 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 @@ -111,24 +104,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/Util.hs b/services/galley/test/integration/API/Util.hs index 16938edb549..950638f5bab 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -20,7 +20,6 @@ module API.Util where -import API.Federation.Util import API.SQS qualified as SQS import Bilge hiding (timeout) import Bilge.Assert @@ -2774,9 +2773,3 @@ createAndConnectUsers domains = do (False, True) -> connectWithRemoteUser (qUnqualified b) a (False, False) -> pure () pure users - -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 From 7ef9d8fae8be3da41de6c7be88ec555524864d00 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 20 Sep 2024 08:33:32 +0200 Subject: [PATCH 15/15] NewTeam types refactoring (#4257) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Separate BindingNewTeam for NonBindingNewTeam * Rename BindingNewTeam → NewTeam * Remove NewTeam lenses --- .../5-internal/new-team-types-refactoring | 1 + .../src/Wire/API/Routes/Internal/Galley.hs | 2 +- .../src/Wire/API/Routes/Public/Galley/Team.hs | 37 +- libs/wire-api/src/Wire/API/Team.hs | 79 +--- libs/wire-api/src/Wire/API/User.hs | 6 +- .../golden/Test/Wire/API/Golden/Generated.hs | 4 +- .../Generated/BindingNewTeamUser_user.hs | 60 ++- .../Golden/Generated/BindingNewTeam_team.hs | 353 ------------------ .../Wire/API/Golden/Generated/NewTeam_team.hs | 283 ++++++++++++++ .../Wire/API/Golden/Generated/NewUser_user.hs | 27 +- ..._1.json => testObject_NewTeam_team_1.json} | 0 ...0.json => testObject_NewTeam_team_10.json} | 0 ...1.json => testObject_NewTeam_team_11.json} | 0 ...2.json => testObject_NewTeam_team_12.json} | 0 ...3.json => testObject_NewTeam_team_13.json} | 0 ...4.json => testObject_NewTeam_team_14.json} | 0 ...5.json => testObject_NewTeam_team_15.json} | 0 ...6.json => testObject_NewTeam_team_16.json} | 0 ...7.json => testObject_NewTeam_team_17.json} | 0 ...8.json => testObject_NewTeam_team_18.json} | 0 ...9.json => testObject_NewTeam_team_19.json} | 0 ..._2.json => testObject_NewTeam_team_2.json} | 0 ...0.json => testObject_NewTeam_team_20.json} | 0 ..._3.json => testObject_NewTeam_team_3.json} | 0 ..._4.json => testObject_NewTeam_team_4.json} | 0 ..._5.json => testObject_NewTeam_team_5.json} | 0 ..._6.json => testObject_NewTeam_team_6.json} | 0 ..._7.json => testObject_NewTeam_team_7.json} | 0 ..._8.json => testObject_NewTeam_team_8.json} | 0 ..._9.json => testObject_NewTeam_team_9.json} | 0 .../unit/Test/Wire/API/Roundtrip/Aeson.hs | 2 +- libs/wire-api/wire-api.cabal | 2 +- .../src/Wire/GalleyAPIAccess.hs | 2 +- .../src/Wire/GalleyAPIAccess/Rpc.hs | 2 +- services/brig/src/Brig/API/Public.hs | 6 +- services/brig/src/Brig/API/User.hs | 10 +- services/brig/test/integration/API/Team.hs | 2 +- .../brig/test/integration/API/Team/Util.hs | 8 +- .../integration/API/UserPendingActivation.hs | 4 +- services/galley/src/Galley/API/Teams.hs | 43 +-- services/galley/test/integration/API/Util.hs | 4 +- services/spar/test-integration/Util/Core.hs | 4 +- tools/stern/test/integration/Util.hs | 2 +- 43 files changed, 409 insertions(+), 534 deletions(-) create mode 100644 changelog.d/5-internal/new-team-types-refactoring delete mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/BindingNewTeam_team.hs create mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewTeam_team.hs rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_1.json => testObject_NewTeam_team_1.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_10.json => testObject_NewTeam_team_10.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_11.json => testObject_NewTeam_team_11.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_12.json => testObject_NewTeam_team_12.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_13.json => testObject_NewTeam_team_13.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_14.json => testObject_NewTeam_team_14.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_15.json => testObject_NewTeam_team_15.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_16.json => testObject_NewTeam_team_16.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_17.json => testObject_NewTeam_team_17.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_18.json => testObject_NewTeam_team_18.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_19.json => testObject_NewTeam_team_19.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_2.json => testObject_NewTeam_team_2.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_20.json => testObject_NewTeam_team_20.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_3.json => testObject_NewTeam_team_3.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_4.json => testObject_NewTeam_team_4.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_5.json => testObject_NewTeam_team_5.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_6.json => testObject_NewTeam_team_6.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_7.json => testObject_NewTeam_team_7.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_8.json => testObject_NewTeam_team_8.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_9.json => testObject_NewTeam_team_9.json} (100%) diff --git a/changelog.d/5-internal/new-team-types-refactoring b/changelog.d/5-internal/new-team-types-refactoring new file mode 100644 index 00000000000..70b4ade0568 --- /dev/null +++ b/changelog.d/5-internal/new-team-types-refactoring @@ -0,0 +1 @@ +Simplify NewTeam and related types and remove lenses 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 22f23d50a31..ea493672d82 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -216,7 +216,7 @@ type ITeamsAPIBase = :<|> Named "create-binding-team" ( ZUser - :> ReqBody '[JSON] BindingNewTeam + :> ReqBody '[JSON] NewTeam :> MultiVerb1 'PUT '[JSON] 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 4c0c61751d4..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,7 +17,12 @@ 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 import Servant.OpenApi.Internal.Orphans () @@ -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/Team.hs b/libs/wire-api/src/Wire/API/Team.hs index cffcd2bac95..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 {bntTeam :: 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/User.hs b/libs/wire-api/src/Wire/API/User.hs index 08a8f758db9..5d5a42af3b1 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -206,7 +206,7 @@ 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 @@ -1305,7 +1305,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 @@ -1319,7 +1319,7 @@ instance ToSchema BindingNewTeamUser where object "BindingNewTeamUser" $ BindingNewTeamUser <$> bnuTeam - .= bindingNewTeamObjectSchema + .= newTeamObjectSchema <*> bnuCurrency .= maybe_ (optField "currency" genericToSchema) 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 ec9eb270c2f..c0827feab02 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 @@ -37,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 @@ -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 @@ -1156,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" $ 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/NewTeam_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewTeam_team.hs new file mode 100644 index 00000000000..a5cb02772ba --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewTeam_team.hs @@ -0,0 +1,283 @@ +-- 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.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/NewUser_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs index 117280d3753..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)) @@ -137,20 +137,17 @@ testObject_NewUser_user_7 = 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 } 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/unit/Test/Wire/API/Roundtrip/Aeson.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs index c9c780681c1..83d59a00b29 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 @@ -206,7 +206,7 @@ tests = testRoundTrip @SystemSettings.SystemSettings, testRoundTrip @SystemSettings.SystemSettingsPublic, testRoundTrip @SystemSettings.SystemSettingsInternal, - testRoundTrip @Team.BindingNewTeam, + testRoundTrip @Team.NewTeam, testRoundTrip @Team.TeamBinding, testRoundTrip @Team.Team, testRoundTrip @Team.TeamList, diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 9d2026da817..1b768dd1fc8 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -373,7 +373,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 @@ -461,6 +460,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 diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs index 07003fed93c..859fe913628 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs @@ -73,7 +73,7 @@ data GalleyAPIAccess m a where GalleyAPIAccess m Bool CreateTeam :: UserId -> - BindingNewTeam -> + NewTeam -> TeamId -> GalleyAPIAccess m () GetTeamMember :: diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs index 340018628f7..f7ee1c47f65 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs @@ -284,7 +284,7 @@ createTeam :: Member TinyLog r ) => UserId -> - BindingNewTeam -> + NewTeam -> TeamId -> Sem r () createTeam u t teamid = do diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 8d4d35d653f..fe994a98952 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -58,7 +58,7 @@ 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 (view, (.~), (?~)) import Control.Monad.Catch (throwM) import Control.Monad.Except import Data.Aeson hiding (json) @@ -787,8 +787,8 @@ createUser (Public.NewUserPublic new) = lift . runExceptT $ do 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 diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 6e516ccca60..982c907e338 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -289,8 +289,8 @@ upgradePersonalToTeam luid bNewTeam = do let uid = tUnqualified luid createUserTeam <- do liftSem $ GalleyAPIAccess.createTeam uid (bnuTeam bNewTeam) tid - let BindingNewTeam newTeam = bNewTeam.bnuTeam - pure $ CreateUserTeam tid (fromRange (newTeam ^. newTeamName)) + let newTeam = bNewTeam.bnuTeam + pure $ CreateUserTeam tid (fromRange newTeam.newTeamName) wrapClient $ updateUserTeam uid tid liftSem $ Intra.sendUserEvent uid Nothing (teamUpdated uid tid) @@ -302,7 +302,7 @@ upgradePersonalToTeam luid bNewTeam = do sendUpgradePersonalToTeamConfirmationEmail email user.userDisplayName - bNewTeam.bnuTeam.bntTeam._newTeamName.fromRange + bNewTeam.bnuTeam.newTeamName.fromRange user.userLocale pure $! createUserTeam @@ -394,10 +394,10 @@ 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 diff --git a/services/brig/test/integration/API/Team.hs b/services/brig/test/integration/API/Team.hs index daac2f2e6eb..5cc29c6f86e 100644 --- a/services/brig/test/integration/API/Team.hs +++ b/services/brig/test/integration/API/Team.hs @@ -668,7 +668,7 @@ testInvitationMutuallyExclusive brig = do req :: EmailAddress -> Maybe InvitationCode -> - Maybe BindingNewTeam -> + Maybe NewTeam -> Maybe InvitationCode -> HttpT IO (Response (Maybe LByteString)) req e c t i = diff --git a/services/brig/test/integration/API/Team/Util.hs b/services/brig/test/integration/API/Team/Util.hs index defa0f8e5b3..12258c5dbd5 100644 --- a/services/brig/test/integration/API/Team/Util.hs +++ b/services/brig/test/integration/API/Team/Util.hs @@ -257,8 +257,8 @@ deleteTeam g tid u = do !!! const 202 === statusCode -newTeam :: BindingNewTeam -newTeam = BindingNewTeam $ newNewTeam (unsafeRange "teamName") DefaultIcon +newTeam :: NewTeam +newTeam = newNewTeam (unsafeRange "teamName") DefaultIcon putLegalHoldEnabled :: (HasCallStack) => TeamId -> FeatureStatus -> Galley -> Http () putLegalHoldEnabled tid enabled g = do @@ -302,7 +302,7 @@ extAccept email name phone phoneCode code = "team_code" .= code ] -register :: EmailAddress -> BindingNewTeam -> Brig -> Http (Response (Maybe LByteString)) +register :: EmailAddress -> NewTeam -> Brig -> Http (Response (Maybe LByteString)) register e t brig = post ( brig @@ -319,7 +319,7 @@ register e t brig = ) ) -register' :: EmailAddress -> BindingNewTeam -> ActivationCode -> Brig -> Http (Response (Maybe LByteString)) +register' :: EmailAddress -> NewTeam -> ActivationCode -> Brig -> Http (Response (Maybe LByteString)) register' e t c brig = post ( brig diff --git a/services/brig/test/integration/API/UserPendingActivation.hs b/services/brig/test/integration/API/UserPendingActivation.hs index 00e2e3e8de8..da5c80d12cb 100644 --- a/services/brig/test/integration/API/UserPendingActivation.hs +++ b/services/brig/test/integration/API/UserPendingActivation.hs @@ -147,8 +147,8 @@ getInvitationByEmail brig email = Brig -> Galley -> m (UserId, TeamId) createUserWithTeamDisableSSO brg gly = do diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index df3015be471..f6105cc46f1 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -228,41 +228,14 @@ 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, @@ -271,12 +244,12 @@ createBindingTeam :: ) => 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 + E.createTeam (Just tid) zusr body.newTeamName body.newTeamIcon body.newTeamIconKey Binding finishCreateTeam team owner [] Nothing pure tid diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 950638f5bab..026a3475fc3 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -275,7 +275,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) ["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 diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index e041c3d0b1c..474cc09bb8d 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -606,8 +606,8 @@ 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 EmailAddress randomEmail = do diff --git a/tools/stern/test/integration/Util.hs b/tools/stern/test/integration/Util.hs index 0e533484b96..d151434f164 100644 --- a/tools/stern/test/integration/Util.hs +++ b/tools/stern/test/integration/Util.hs @@ -96,7 +96,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] (,e) . responseJsonUnsafe <$> (post (b . path "/i/users" . Bilge.json pl) TestM (UserId, EmailAddress)