diff --git a/changelog.d/1-api-changes/WPB-3611 b/changelog.d/1-api-changes/WPB-3611 deleted file mode 100644 index 95fa03edec4..00000000000 --- a/changelog.d/1-api-changes/WPB-3611 +++ /dev/null @@ -1,5 +0,0 @@ -Added a new notification event type, "federation.connectionRemoved" -This event contains a pair of domains that are no longer federating, and is used to inform other federation members of the change. -This notification is sent twice to local clients of federation members who receive this notification. Once before and once after cleaning up local conversaions where users from both domains are present. - -Added a new Galley federation endpoint "/federation/on-connection-removed" to receive the connection removed notification. \ No newline at end of file diff --git a/changelog.d/1-api-changes/WPB-4668-disable-defederation b/changelog.d/1-api-changes/WPB-4668-disable-defederation new file mode 100644 index 00000000000..baef31417a9 --- /dev/null +++ b/changelog.d/1-api-changes/WPB-4668-disable-defederation @@ -0,0 +1 @@ +Remove de-federation (to avoid a scalability issue). \ No newline at end of file diff --git a/changelog.d/3-bug-fixes/WPB-4629 b/changelog.d/3-bug-fixes/WPB-4629 new file mode 100644 index 00000000000..5d1724fe66e --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-4629 @@ -0,0 +1 @@ +Fixed add user to conversation when one of the other participating backends is offline diff --git a/changelog.d/3-bug-fixes/remote-member-removal-notification b/changelog.d/3-bug-fixes/remote-member-removal-notification new file mode 100644 index 00000000000..a94c916a689 --- /dev/null +++ b/changelog.d/3-bug-fixes/remote-member-removal-notification @@ -0,0 +1 @@ +This fixes a bug where a remote member is removed from a conversation while their backend is unreachable, and the backend does not receive the removal notification once it is reachable again. diff --git a/changelog.d/4-docs/WPB-4240 b/changelog.d/4-docs/WPB-4240 new file mode 100644 index 00000000000..d7dd76196ec --- /dev/null +++ b/changelog.d/4-docs/WPB-4240 @@ -0,0 +1 @@ +Updating the route documentation from Swagger 2 to OpenAPI 3. \ No newline at end of file diff --git a/changelog.d/5-internal/WPB-4240 b/changelog.d/5-internal/WPB-4240 new file mode 100644 index 00000000000..bca7dcb1fc6 --- /dev/null +++ b/changelog.d/5-internal/WPB-4240 @@ -0,0 +1,4 @@ +Updating the route documentation library from swagger2 to openapi3. + +This also introduced a breaking change in how we track what federation calls each route makes. +The openapi3 library doesn't support extension fields, and as such tags are being used instead in a similar way. \ No newline at end of file diff --git a/changelog.d/6-federation/WPB-3611 b/changelog.d/6-federation/WPB-3611 deleted file mode 100644 index 4d485843b0f..00000000000 --- a/changelog.d/6-federation/WPB-3611 +++ /dev/null @@ -1 +0,0 @@ -Defederating from a remote server will now inform your remaining federation members, allowing them to clean up their local conversations and inform their clients. \ No newline at end of file diff --git a/charts/fake-aws-s3/requirements.yaml b/charts/fake-aws-s3/requirements.yaml index da4723909d2..f62c11a7b74 100644 --- a/charts/fake-aws-s3/requirements.yaml +++ b/charts/fake-aws-s3/requirements.yaml @@ -1,4 +1,4 @@ dependencies: - name: minio - version: 3.2.0 + version: 5.0.13 repository: https://charts.min.io/ diff --git a/charts/fake-aws-s3/values.yaml b/charts/fake-aws-s3/values.yaml index a736eb82cb0..bf36bdf4a93 100644 --- a/charts/fake-aws-s3/values.yaml +++ b/charts/fake-aws-s3/values.yaml @@ -1,9 +1,5 @@ -# See defaults in https://github.com/helm/charts/tree/master/stable/minio +# See defaults in https://github.com/minio/minio/blob/RELEASE.2023-07-07T07-13-57Z/helm/minio/values.yaml minio: - mcImage: - repository: quay.io/minio/mc - tag: RELEASE.2021-10-07T04-19-58Z - pullPolicy: IfNotPresent fullnameOverride: fake-aws-s3 service: port: "9000" diff --git a/deploy/dockerephemeral/docker-compose.yaml b/deploy/dockerephemeral/docker-compose.yaml index e02620b7677..17321961014 100644 --- a/deploy/dockerephemeral/docker-compose.yaml +++ b/deploy/dockerephemeral/docker-compose.yaml @@ -57,8 +57,7 @@ services: fake_s3: container_name: demo_wire_s3 -# image: minio/minio:RELEASE.2018-05-25T19-49-13Z - image: julialongtin/minio:0.0.9 + image: minio/minio:RELEASE.2023-07-07T07-13-57Z ports: - "127.0.0.1:4570:9000" environment: diff --git a/docs/src/how-to/install/sft.md b/docs/src/how-to/install/sft.md index e4560c72168..9074bd93a21 100644 --- a/docs/src/how-to/install/sft.md +++ b/docs/src/how-to/install/sft.md @@ -18,7 +18,7 @@ tags: sftd: host: sftd.example.com # Replace example.com with your domain - allowOrigin: webapp.example.com # Should be the address you used for the webapp deployment + allowOrigin: https://webapp.example.com # Should be the address you used for the webapp deployment (Note: you must include the uri scheme "https://") ``` In your `secrets.yaml` you should set the TLS keys for sftd domain: diff --git a/docs/src/understand/configure-federation.md b/docs/src/understand/configure-federation.md index 3477a10242a..455aafd6437 100644 --- a/docs/src/understand/configure-federation.md +++ b/docs/src/understand/configure-federation.md @@ -457,13 +457,8 @@ the sysadmin: * [`PUT`](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/put_i_federation_remotes__domain_) -* [`DELETE`](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/delete_i_federation_remotes__domain_) - - **WARNING:** If you delete a connection, all users from that - remote will be removed from local conversations, and all - conversations hosted by that remote will be removed from the local - backend. Connections between local and remote users that are - removed will be archived, and can be re-established should you - decide to add the same backend later. +* **NOTE:** De-federating (`DELETE`) has been removed from the API to + avoid a scalability issue. Watch out for a fix in the changelog! The `remotes` list looks like this: diff --git a/integration/default.nix b/integration/default.nix index 1aa88b39fda..b42304134c1 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -15,8 +15,11 @@ , Cabal , case-insensitive , containers +, cql +, cql-io , cryptonite , data-default +, data-timeout , directory , errors , exceptions @@ -80,8 +83,11 @@ mkDerivation { bytestring-conversion case-insensitive containers + cql + cql-io cryptonite data-default + data-timeout directory errors exceptions diff --git a/integration/integration.cabal b/integration/integration.cabal index 622038313db..3c944a0fd01 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -100,21 +100,23 @@ library Notifications RunAllTests SetupHelpers + Test.AccessUpdate Test.AssetDownload Test.B2B Test.Brig Test.Client Test.Conversation - Test.Defederation Test.Demo Test.Federation Test.Federator + Test.MessageTimer Test.MLS Test.MLS.KeyPackage Test.MLS.One2One Test.MLS.SubConversation Test.Notifications Test.Presence + Test.Roles Test.User Testlib.App Testlib.Assertions @@ -147,8 +149,11 @@ library , bytestring-conversion , case-insensitive , containers + , cql + , cql-io , cryptonite , data-default + , data-timeout , directory , errors , exceptions diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index a4578b17bbd..29adee75a0c 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -99,25 +99,6 @@ updateFedConn' owndom dom fedConn = do conn <- make fedConn submit "PUT" $ addJSON conn req -deleteFedConn :: (HasCallStack, MakesValue owndom) => owndom -> String -> App Response -deleteFedConn owndom dom = do - bindResponse (deleteFedConn' owndom dom) $ \res -> do - res.status `shouldMatchRange` (200, 299) - pure res - -deleteFedConn' :: (HasCallStack, MakesValue owndom) => owndom -> String -> App Response -deleteFedConn' owndom dom = do - req <- rawBaseRequest owndom Brig Unversioned ("/i/federation/remotes/" <> dom) - submit "DELETE" req - -deleteAllFedConns :: (HasCallStack, MakesValue dom) => dom -> App () -deleteAllFedConns dom = do - readFedConns dom >>= \resp -> - resp.json %. "remotes" - & asList - >>= traverse (\v -> v %. "domain" & asString) - >>= mapM_ (deleteFedConn dom) - registerOAuthClient :: (HasCallStack, MakesValue user, MakesValue name, MakesValue url) => user -> name -> url -> App Response registerOAuthClient user name url = do req <- baseRequest user Brig Unversioned "i/oauth/clients" diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index efa09bc40e2..14a8845ce4a 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -5,6 +5,7 @@ module API.Galley where import Control.Lens hiding ((.=)) import Control.Monad.Reader import Data.Aeson qualified as Aeson +import Data.Aeson.Types qualified as Aeson import Data.ByteString.Base64 qualified as B64 import Data.ByteString.Base64.URL qualified as B64U import Data.ByteString.Char8 qualified as BS @@ -83,6 +84,21 @@ postConversation user cc = do ccv <- make cc submit "POST" $ req & addJSON ccv +deleteTeamConversation :: + ( HasCallStack, + MakesValue user, + MakesValue conv + ) => + String -> + conv -> + user -> + App Response +deleteTeamConversation tid qcnv user = do + cnv <- snd <$> objQid qcnv + let path = joinHttpPath ["teams", tid, "conversations", cnv] + req <- baseRequest user Galley Versioned path + submit "DELETE" req + putConversationProtocol :: ( HasCallStack, MakesValue user, @@ -296,12 +312,39 @@ getGroupClients user groupId = do (joinHttpPath ["i", "group", BS.unpack . B64U.encodeUnpadded . B64.decodeLenient $ BS.pack groupId]) submit "GET" req -addMembers :: (HasCallStack, MakesValue user, MakesValue conv) => user -> conv -> [Value] -> App Response -addMembers usr qcnv newMembers = do +data AddMembers = AddMembers + { users :: [Value], + role :: Maybe String, + version :: Maybe Int + } + +instance Default AddMembers where + def = AddMembers {users = [], role = Nothing, version = Nothing} + +addMembers :: + (HasCallStack, MakesValue user, MakesValue conv) => + user -> + conv -> + AddMembers -> + App Response +addMembers usr qcnv opts = do (convDomain, convId) <- objQid qcnv - qUsers <- mapM objQidObject newMembers - req <- baseRequest usr Galley Versioned (joinHttpPath ["conversations", convDomain, convId, "members"]) - submit "POST" (req & addJSONObject ["qualified_users" .= qUsers]) + qUsers <- mapM objQidObject opts.users + let path = case opts.version of + Just v | v <= 1 -> ["conversations", convId, "members", "v2"] + _ -> ["conversations", convDomain, convId, "members"] + req <- + baseRequest + usr + Galley + (maybe Versioned ExplicitVersion opts.version) + (joinHttpPath path) + submit "POST" $ + req + & addJSONObject + ( ["qualified_users" .= qUsers] + <> ["conversation_role" .= r | r <- toList opts.role] + ) removeMember :: (HasCallStack, MakesValue remover, MakesValue conv, MakesValue removed) => remover -> conv -> removed -> App Response removeMember remover qcnv removed = do @@ -342,3 +385,89 @@ getConversationCode user conv mbZHost = do & addQueryParams [("cnv", convId)] & maybe id zHost mbZHost ) + +changeConversationName :: + (HasCallStack, MakesValue user, MakesValue conv, MakesValue name) => + user -> + conv -> + name -> + App Response +changeConversationName user qcnv name = do + (convDomain, convId) <- objQid qcnv + let path = joinHttpPath ["conversations", convDomain, convId, "name"] + nameReq <- make name + req <- baseRequest user Galley Versioned path + submit "PUT" (req & addJSONObject ["name" .= nameReq]) + +updateRole :: + ( HasCallStack, + MakesValue callerUser, + MakesValue targetUser, + MakesValue roleUpdate, + MakesValue qcnv + ) => + callerUser -> + targetUser -> + roleUpdate -> + qcnv -> + App Response +updateRole caller target role qcnv = do + (cnvDomain, cnvId) <- objQid qcnv + (tarDomain, tarId) <- objQid target + roleReq <- make role + req <- + baseRequest + caller + Galley + Versioned + ( joinHttpPath ["conversations", cnvDomain, cnvId, "members", tarDomain, tarId] + ) + submit "PUT" (req & addJSONObject ["conversation_role" .= roleReq]) + +updateReceiptMode :: + ( HasCallStack, + MakesValue user, + MakesValue conv, + MakesValue mode + ) => + user -> + conv -> + mode -> + App Response +updateReceiptMode user qcnv mode = do + (cnvDomain, cnvId) <- objQid qcnv + modeReq <- make mode + let path = joinHttpPath ["conversations", cnvDomain, cnvId, "receipt-mode"] + req <- baseRequest user Galley Versioned path + submit "PUT" (req & addJSONObject ["receipt_mode" .= modeReq]) + +updateAccess :: + ( HasCallStack, + MakesValue user, + MakesValue conv + ) => + user -> + conv -> + [Aeson.Pair] -> + App Response +updateAccess user qcnv update = do + (cnvDomain, cnvId) <- objQid qcnv + let path = joinHttpPath ["conversations", cnvDomain, cnvId, "access"] + req <- baseRequest user Galley Versioned path + submit "PUT" (req & addJSONObject update) + +updateMessageTimer :: + ( HasCallStack, + MakesValue user, + MakesValue conv + ) => + user -> + conv -> + Word64 -> + App Response +updateMessageTimer user qcnv update = do + (cnvDomain, cnvId) <- objQid qcnv + updateReq <- make update + let path = joinHttpPath ["conversations", cnvDomain, cnvId, "message-timer"] + req <- baseRequest user Galley Versioned path + submit "PUT" (addJSONObject ["message_timer" .= updateReq] req) diff --git a/integration/test/API/GalleyInternal.hs b/integration/test/API/GalleyInternal.hs index 989e021e585..428199004e5 100644 --- a/integration/test/API/GalleyInternal.hs +++ b/integration/test/API/GalleyInternal.hs @@ -51,12 +51,3 @@ getFederationStatus user domains = submit "GET" $ req & addJSONObject ["domains" .= domainList] - -deleteFederationDomain :: - ( HasCallStack - ) => - String -> - App Response -deleteFederationDomain domain = do - req <- rawBaseRequest OwnDomain Galley Unversioned $ joinHttpPath ["i", "federation", domain] - submit "DELETE" req diff --git a/integration/test/Notifications.hs b/integration/test/Notifications.hs index 0364bee7ceb..a6ffb6505b1 100644 --- a/integration/test/Notifications.hs +++ b/integration/test/Notifications.hs @@ -65,8 +65,63 @@ isMemberJoinNotif n = fieldEquals n "payload.0.type" "conversation.member-join" isConvLeaveNotif :: MakesValue a => a -> App Bool isConvLeaveNotif n = fieldEquals n "payload.0.type" "conversation.member-leave" -isNotifConv :: (MakesValue conv, MakesValue a) => conv -> a -> App Bool +isNotifConv :: (MakesValue conv, MakesValue a, HasCallStack) => conv -> a -> App Bool isNotifConv conv n = fieldEquals n "payload.0.qualified_conversation" (objQidObject conv) -isNotifForUser :: (MakesValue user, MakesValue a) => user -> a -> App Bool +isNotifForUser :: (MakesValue user, MakesValue a, HasCallStack) => user -> a -> App Bool isNotifForUser user n = fieldEquals n "payload.0.data.qualified_user_ids.0" (objQidObject user) + +isNotifFromUser :: (MakesValue user, MakesValue a, HasCallStack) => user -> a -> App Bool +isNotifFromUser user n = fieldEquals n "payload.0.qualified_from" (objQidObject user) + +isConvNameChangeNotif :: (HasCallStack, MakesValue a) => a -> App Bool +isConvNameChangeNotif n = fieldEquals n "payload.0.type" "conversation.rename" + +isMemberUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool +isMemberUpdateNotif n = fieldEquals n "payload.0.type" "conversation.member-update" + +isReceiptModeUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool +isReceiptModeUpdateNotif n = + fieldEquals n "payload.0.type" "conversation.receipt-mode-update" + +isConvMsgTimerUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool +isConvMsgTimerUpdateNotif n = + fieldEquals n "payload.0.type" "conversation.message-timer-update" + +isConvAccessUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool +isConvAccessUpdateNotif n = + fieldEquals n "payload.0.type" "conversation.access-update" + +isConvCreateNotif :: MakesValue a => a -> App Bool +isConvCreateNotif n = fieldEquals n "payload.0.type" "conversation.create" + +isConvDeleteNotif :: MakesValue a => a -> App Bool +isConvDeleteNotif n = fieldEquals n "payload.0.type" "conversation.delete" + +assertLeaveNotification :: + ( HasCallStack, + MakesValue fromUser, + MakesValue conv, + MakesValue user, + MakesValue kickedUser + ) => + fromUser -> + conv -> + user -> + String -> + kickedUser -> + App () +assertLeaveNotification fromUser conv user client leaver = + void $ + awaitNotification + user + client + noValue + 2 + ( allPreds + [ isConvLeaveNotif, + isNotifConv conv, + isNotifForUser leaver, + isNotifFromUser fromUser + ] + ) diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 027ae4b46cd..f151475c689 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -1,9 +1,11 @@ {-# OPTIONS_GHC -Wno-ambiguous-fields #-} +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} module SetupHelpers where import API.Brig import API.BrigInternal +import API.Common import API.Galley import Control.Concurrent (threadDelay) import Control.Monad.Reader @@ -11,7 +13,6 @@ import Data.Aeson hiding ((.=)) import Data.Aeson.Types qualified as Aeson import Data.Default import Data.Function -import Data.List qualified as List import Data.UUID.V1 (nextUUID) import Data.UUID.V4 (nextRandom) import GHC.Stack @@ -33,15 +34,43 @@ deleteUser user = bindResponse (API.Brig.deleteUser user) $ \resp -> do resp.status `shouldMatchInt` 200 -- | returns (user, team id) -createTeam :: (HasCallStack, MakesValue domain) => domain -> App (Value, String) -createTeam domain = do +createTeam :: (HasCallStack, MakesValue domain) => domain -> Int -> App (Value, String, [Value]) +createTeam domain memberCount = do res <- createUser domain def {team = True} - user <- res.json - tid <- user %. "team" & asString - -- TODO - -- SQS.assertTeamActivate "create team" tid - -- refreshIndex - pure (user, tid) + owner <- res.json + tid <- owner %. "team" & asString + members <- for [2 .. memberCount] $ \_ -> createTeamMember owner tid + pure (owner, tid, members) + +createTeamMember :: + (HasCallStack, MakesValue inviter) => + inviter -> + String -> + App Value +createTeamMember inviter tid = do + newUserEmail <- randomEmail + let invitationJSON = ["role" .= "member", "email" .= newUserEmail] + invitationReq <- + baseRequest inviter Brig Versioned $ + joinHttpPath ["teams", tid, "invitations"] + invitation <- getJSON 201 =<< submit "POST" (addJSONObject invitationJSON invitationReq) + invitationId <- objId invitation + invitationCodeReq <- + rawBaseRequest inviter Brig Unversioned "/i/teams/invitation-code" + <&> addQueryParams [("team", tid), ("invitation_id", invitationId)] + invitationCode <- bindResponse (submit "GET" invitationCodeReq) $ \res -> do + res.status `shouldMatchInt` 200 + res.json %. "code" & asString + let registerJSON = + [ "name" .= newUserEmail, + "email" .= newUserEmail, + "password" .= defPassword, + "team_code" .= invitationCode + ] + registerReq <- + rawBaseRequest inviter Brig Versioned "/register" + <&> addJSONObject registerJSON + getJSON 201 =<< submit "POST" registerReq connectUsers2 :: ( HasCallStack, @@ -86,7 +115,7 @@ simpleMixedConversationSetup :: domain -> App (Value, Value, Value) simpleMixedConversationSetup secondDomain = do - (alice, tid) <- createTeam OwnDomain + (alice, tid, _) <- createTeam OwnDomain 1 bob <- randomUser secondDomain def connectUsers [alice, bob] @@ -119,14 +148,6 @@ addUserToTeam u = do code <- resp %. "code" & asString addUser u def {email = Just email, teamCode = Just code} >>= getJSON 201 -resetFedConns :: (HasCallStack, MakesValue owndom) => owndom -> App () -resetFedConns owndom = do - bindResponse (readFedConns owndom) $ \resp -> do - rdoms :: [String] <- do - rawlist <- resp.json %. "remotes" & asList - (asString . (%. "domain")) `mapM` rawlist - deleteFedConn' owndom `mapM_` rdoms - -- | Create a user on the given domain, such that the 1-1 conversation with -- 'other' resides on 'convDomain'. This connects the two users as a side-effect. createMLSOne2OnePartner :: MakesValue user => Domain -> user -> Domain -> App Value @@ -156,40 +177,14 @@ randomUserId domain = do uid <- randomId pure $ object ["id" .= uid, "domain" .= d] -addFullSearchFor :: [String] -> Value -> App Value -addFullSearchFor domains val = - modifyField - "optSettings.setFederationDomainConfigs" - ( \configs -> do - cfg <- assertJust "" configs - xs <- cfg & asList - pure (xs <> [object ["domain" .= domain, "search_policy" .= "full_search"] | domain <- domains]) - ) - val - -fullSearchWithAll :: ServiceOverrides -fullSearchWithAll = - def - { brigCfg = \val -> do - ownDomain <- asString =<< val %. "optSettings.setFederationDomain" - env <- ask - let remoteDomains = List.delete ownDomain $ [env.domain1, env.domain2] <> env.dynamicDomains - addFullSearchFor remoteDomains val - } - -withFederatingBackendsAllowDynamic :: HasCallStack => Int -> ((String, String, String) -> App a) -> App a -withFederatingBackendsAllowDynamic n k = do +withFederatingBackendsAllowDynamic :: HasCallStack => ((String, String, String) -> App a) -> App a +withFederatingBackendsAllowDynamic k = do let setFederationConfig = setField "optSettings.setFederationStrategy" "allowDynamic" - >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) startDynamicBackends [ def {brigCfg = setFederationConfig}, def {brigCfg = setFederationConfig}, def {brigCfg = setFederationConfig} ] - $ \dynDomains -> do - domains@[domainA, domainB, domainC] <- pure dynDomains - sequence_ [createFedConn x (FedConn y "full_search") | x <- domains, y <- domains, x /= y] - liftIO $ threadDelay (n * 1000 * 1000) -- wait for federation status to be updated - k (domainA, domainB, domainC) + $ \[domainA, domainB, domainC] -> k (domainA, domainB, domainC) diff --git a/integration/test/Test/AccessUpdate.hs b/integration/test/Test/AccessUpdate.hs new file mode 100644 index 00000000000..42ef9f6b19a --- /dev/null +++ b/integration/test/Test/AccessUpdate.hs @@ -0,0 +1,122 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.AccessUpdate where + +import API.Brig +import API.Galley +import Control.Monad.Codensity +import Control.Monad.Reader +import GHC.Stack +import Notifications +import SetupHelpers +import Testlib.Prelude +import Testlib.ResourcePool + +-- @SF.Federation @SF.Separation @TSFI.RESTfulAPI @S2 +-- +-- The test asserts that, among others, remote users are removed from a +-- conversation when an access update occurs that disallows guests from +-- accessing. +testAccessUpdateGuestRemoved :: HasCallStack => App () +testAccessUpdateGuestRemoved = do + (alice, tid, [bob]) <- createTeam OwnDomain 2 + charlie <- randomUser OwnDomain def + dee <- randomUser OtherDomain def + mapM_ (connectUsers2 alice) [charlie, dee] + [aliceClient, bobClient, charlieClient, deeClient] <- + mapM + (\user -> objId $ bindResponse (addClient user def) $ getJSON 201) + [alice, bob, charlie, dee] + conv <- + postConversation + alice + defProteus + { qualifiedUsers = [bob, charlie, dee], + team = Just tid + } + >>= getJSON 201 + + let update = ["access" .= ([] :: [String]), "access_role" .= ["team_member"]] + void $ updateAccess alice conv update >>= getJSON 200 + + mapM_ (assertLeaveNotification alice conv alice aliceClient) [charlie, dee] + mapM_ (assertLeaveNotification alice conv bob bobClient) [charlie, dee] + mapM_ (assertLeaveNotification alice conv charlie charlieClient) [charlie, dee] + mapM_ (assertLeaveNotification alice conv dee deeClient) [charlie, dee] + + bindResponse (getConversation alice conv) $ \res -> do + res.status `shouldMatchInt` 200 + res.json %. "members.others.0.qualified_id" `shouldMatch` objQidObject bob + +-- @END + +testAccessUpdateGuestRemovedUnreachableRemotes :: HasCallStack => App () +testAccessUpdateGuestRemovedUnreachableRemotes = do + resourcePool <- asks resourcePool + (alice, tid, [bob]) <- createTeam OwnDomain 2 + charlie <- randomUser OwnDomain def + connectUsers2 alice charlie + [aliceClient, bobClient, charlieClient] <- + mapM + (\user -> objId $ bindResponse (addClient user def) $ getJSON 201) + [alice, bob, charlie] + (conv, dee) <- runCodensity (acquireResources 1 resourcePool) $ \[dynBackend] -> + runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do + dee <- randomUser dynBackend.berDomain def + connectUsers2 alice dee + conv <- + postConversation + alice + ( defProteus + { qualifiedUsers = [bob, charlie, dee], + team = Just tid + } + ) + >>= getJSON 201 + pure (conv, dee) + + let update = ["access" .= ([] :: [String]), "access_role" .= ["team_member"]] + void $ updateAccess alice conv update >>= getJSON 200 + + mapM_ (assertLeaveNotification alice conv alice aliceClient) [charlie, dee] + mapM_ (assertLeaveNotification alice conv bob bobClient) [charlie, dee] + mapM_ (assertLeaveNotification alice conv charlie charlieClient) [charlie, dee] + + bindResponse (getConversation alice conv) $ \res -> do + res.status `shouldMatchInt` 200 + res.json %. "members.others.0.qualified_id" `shouldMatch` objQidObject bob + +testAccessUpdateWithRemotes :: HasCallStack => App () +testAccessUpdateWithRemotes = do + [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OtherDomain, OwnDomain] + conv <- + postConversation alice (defProteus {qualifiedUsers = [bob, charlie]}) + >>= getJSON 201 + let update_access_value = ["code"] + update_access_role_value = ["team_member", "non_team_member", "guest", "service"] + update = ["access" .= update_access_value, "access_role" .= update_access_role_value] + withWebSockets [alice, bob, charlie] $ \wss -> do + void $ updateAccess alice conv update >>= getJSON 200 + for_ wss $ \ws -> do + notif <- awaitMatch 10 isConvAccessUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + notif %. "payload.0.data.access" `shouldMatch` update_access_value + notif %. "payload.0.data.access_role_v2" `shouldMatch` update_access_role_value diff --git a/integration/test/Test/Brig.hs b/integration/test/Test/Brig.hs index a41302d8210..d9a64d3915a 100644 --- a/integration/test/Test/Brig.hs +++ b/integration/test/Test/Brig.hs @@ -4,6 +4,7 @@ import API.Brig qualified as Public import API.BrigInternal qualified as Internal import API.Common qualified as API import API.GalleyInternal qualified as Internal +import Control.Concurrent (threadDelay) import Data.Aeson qualified as Aeson import Data.Aeson.Types hiding ((.=)) import Data.Set qualified as Set @@ -29,14 +30,7 @@ testSearchContactForExternalUsers = do testCrudFederationRemotes :: HasCallStack => App () testCrudFederationRemotes = do otherDomain <- asString OtherDomain - let overrides = - def - { brigCfg = - setField - "optSettings.setFederationDomainConfigs" - [object ["domain" .= otherDomain, "search_policy" .= "full_search"]] - } - withModifiedBackend overrides $ \ownDomain -> do + withModifiedBackend def $ \ownDomain -> do let parseFedConns :: HasCallStack => Response -> App [Value] parseFedConns resp = -- Pick out the list of federation domain configs @@ -45,74 +39,40 @@ testCrudFederationRemotes = do -- Enforce that the values are objects and not something else >>= traverse (fmap Object . asObject) - addOnce :: (MakesValue fedConn, Ord fedConn2, ToJSON fedConn2, MakesValue fedConn2, HasCallStack) => fedConn -> [fedConn2] -> App () - addOnce fedConn want = do + addTest :: (MakesValue fedConn, Ord fedConn2, ToJSON fedConn2, MakesValue fedConn2, HasCallStack) => fedConn -> [fedConn2] -> App () + addTest fedConn want = do bindResponse (Internal.createFedConn ownDomain fedConn) $ \res -> do addFailureContext ("res = " <> show res) $ res.status `shouldMatchInt` 200 res2 <- parseFedConns =<< Internal.readFedConns ownDomain sort res2 `shouldMatch` sort want - addFail :: HasCallStack => MakesValue fedConn => fedConn -> App () - addFail fedConn = do - bindResponse (Internal.createFedConn' ownDomain fedConn) $ \res -> do - addFailureContext ("res = " <> show res) $ res.status `shouldMatchInt` 533 - - deleteOnce :: (Ord fedConn, ToJSON fedConn, MakesValue fedConn) => String -> [fedConn] -> App () - deleteOnce domain want = do - bindResponse (Internal.deleteFedConn ownDomain domain) $ \res -> do - addFailureContext ("res = " <> show res) $ res.status `shouldMatchInt` 200 - res2 <- parseFedConns =<< Internal.readFedConns ownDomain - sort res2 `shouldMatch` sort want - - deleteFail :: HasCallStack => String -> App () - deleteFail del = do - bindResponse (Internal.deleteFedConn' ownDomain del) $ \res -> do - addFailureContext ("res = " <> show res) $ res.status `shouldMatchInt` 533 - - updateOnce :: (MakesValue fedConn, Ord fedConn2, ToJSON fedConn2, MakesValue fedConn2, HasCallStack) => String -> fedConn -> [fedConn2] -> App () - updateOnce domain fedConn want = do + updateTest :: (MakesValue fedConn, Ord fedConn2, ToJSON fedConn2, MakesValue fedConn2, HasCallStack) => String -> fedConn -> [fedConn2] -> App () + updateTest domain fedConn want = do bindResponse (Internal.updateFedConn ownDomain domain fedConn) $ \res -> do addFailureContext ("res = " <> show res) $ res.status `shouldMatchInt` 200 res2 <- parseFedConns =<< Internal.readFedConns ownDomain sort res2 `shouldMatch` sort want - updateFail :: (MakesValue fedConn, HasCallStack) => String -> fedConn -> App () - updateFail domain fedConn = do - bindResponse (Internal.updateFedConn' ownDomain domain fedConn) $ \res -> do - addFailureContext ("res = " <> show res) $ res.status `shouldMatchInt` 533 - dom1 :: String <- (<> ".example.com") . UUID.toString <$> liftIO UUID.nextRandom - dom2 :: String <- (<> ".example.com") . UUID.toString <$> liftIO UUID.nextRandom - let remote1, remote1', remote1'' :: Internal.FedConn + let remote1, remote1' :: Internal.FedConn remote1 = Internal.FedConn dom1 "no_search" remote1' = remote1 {Internal.searchStrategy = "full_search"} - remote1'' = remote1 {Internal.domain = dom2} cfgRemotesExpect :: Internal.FedConn cfgRemotesExpect = Internal.FedConn (cs otherDomain) "full_search" - remote1J <- make remote1 - remote1J' <- make remote1' - - resetFedConns ownDomain + liftIO $ threadDelay 5_000_000 cfgRemotes <- parseFedConns =<< Internal.readFedConns ownDomain - cfgRemotes `shouldMatch` [cfgRemotesExpect] + cfgRemotes `shouldMatch` ([] @Value) -- entries present in the config file can be idempotently added if identical, but cannot be - -- updated, deleted or updated. - addOnce cfgRemotesExpect [cfgRemotesExpect] - addFail (cfgRemotesExpect {Internal.searchStrategy = "no_search"}) - deleteFail (Internal.domain cfgRemotesExpect) - updateFail (Internal.domain cfgRemotesExpect) (cfgRemotesExpect {Internal.searchStrategy = "no_search"}) + -- updated. + addTest cfgRemotesExpect [cfgRemotesExpect] -- create - addOnce remote1 $ (remote1J : cfgRemotes) - addOnce remote1 $ (remote1J : cfgRemotes) -- idempotency + addTest remote1 [cfgRemotesExpect, remote1] + addTest remote1 [cfgRemotesExpect, remote1] -- idempotency -- update - updateOnce (Internal.domain remote1) remote1' (remote1J' : cfgRemotes) - updateFail (Internal.domain remote1) remote1'' - -- delete - deleteOnce (Internal.domain remote1) cfgRemotes - deleteOnce (Internal.domain remote1) cfgRemotes -- idempotency + updateTest (Internal.domain remote1) remote1' [cfgRemotesExpect, remote1'] testCrudOAuthClient :: HasCallStack => App () testCrudOAuthClient = do @@ -185,7 +145,6 @@ testRemoteUserSearch :: HasCallStack => App () testRemoteUserSearch = do let overrides = setField "optSettings.setFederationStrategy" "allowDynamic" - >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) startDynamicBackends [def {brigCfg = overrides}, def {brigCfg = overrides}] $ \dynDomains -> do domains@[d1, d2] <- pure dynDomains diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index fc15a95f03d..bacd00c5c24 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -1,27 +1,46 @@ {-# OPTIONS_GHC -Wno-ambiguous-fields #-} {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + module Test.Conversation where -import API.Brig (getConnection, getConnections, postConnection) +import API.Brig import API.BrigInternal import API.Galley import API.GalleyInternal -import API.Gundeck (getNotifications) import Control.Applicative import Control.Concurrent (threadDelay) +import Control.Monad.Codensity +import Control.Monad.Reader import Data.Aeson qualified as Aeson import Data.Text qualified as T import GHC.Stack -import SetupHelpers +import Notifications +import SetupHelpers hiding (deleteUser) import Testlib.One2One (generateRemoteAndConvIdWithDomain) import Testlib.Prelude +import Testlib.ResourcePool testDynamicBackendsFullyConnectedWhenAllowAll :: HasCallStack => App () testDynamicBackendsFullyConnectedWhenAllowAll = do let overrides = def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} - <> fullSearchWithAll startDynamicBackends [overrides, overrides, overrides] $ \dynDomains -> do [domainA, domainB, domainC] <- pure dynDomains uidA <- randomUser domainA def {team = True} @@ -61,7 +80,6 @@ testDynamicBackendsFullyConnectedWhenAllowDynamic :: HasCallStack => App () testDynamicBackendsFullyConnectedWhenAllowDynamic = do let overrides = setField "optSettings.setFederationStrategy" "allowDynamic" - >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) startDynamicBackends [ def {brigCfg = overrides}, @@ -90,14 +108,10 @@ testDynamicBackendsNotFullyConnected = do def { brigCfg = setField "optSettings.setFederationStrategy" "allowDynamic" - >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) } startDynamicBackends [overrides, overrides, overrides] $ - \dynDomains -> do - domains@[domainA, domainB, domainC] <- pure dynDomains - -- clean federation config - sequence_ [deleteFedConn x y | x <- domains, y <- domains, x /= y] + \[domainA, domainB, domainC] -> do -- A is connected to B and C, but B and C are not connected to each other void $ createFedConn domainA $ FedConn domainB "full_search" void $ createFedConn domainB $ FedConn domainA "full_search" @@ -139,7 +153,6 @@ testCreateConversationFullyConnected :: HasCallStack => App () testCreateConversationFullyConnected = do let setFederationConfig = setField "optSettings.setFederationStrategy" "allowDynamic" - >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) startDynamicBackends [ def {brigCfg = setFederationConfig}, @@ -157,7 +170,6 @@ testCreateConversationNonFullyConnected :: HasCallStack => App () testCreateConversationNonFullyConnected = do let setFederationConfig = setField "optSettings.setFederationStrategy" "allowDynamic" - >=> removeField "optSettings.setFederationDomainConfigs" >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) startDynamicBackends [ def {brigCfg = setFederationConfig}, @@ -165,161 +177,35 @@ testCreateConversationNonFullyConnected = do def {brigCfg = setFederationConfig} ] $ \dynDomains -> do - domains@[domainA, domainB, domainC] <- pure dynDomains - connectAllDomainsAndWaitToSync 1 domains - [u1, u2, u3] <- createAndConnectUsers [domainA, domainB, domainC] - -- stop federation between B and C - void $ deleteFedConn domainB domainC - void $ deleteFedConn domainC domainB + [domainA, domainB, domainC] <- pure dynDomains + + -- A is connected to B and C, but B and C are not connected to each other + void $ createFedConn domainA $ FedConn domainB "full_search" + void $ createFedConn domainB $ FedConn domainA "full_search" + void $ createFedConn domainA $ FedConn domainC "full_search" + void $ createFedConn domainC $ FedConn domainA "full_search" liftIO $ threadDelay (2 * 1000 * 1000) + + u1 <- randomUser domainA def + u2 <- randomUser domainB def + u3 <- randomUser domainC def + connectUsers2 u1 u2 + connectUsers2 u1 u3 + bindResponse (postConversation u1 (defProteus {qualifiedUsers = [u2, u3]})) $ \resp -> do resp.status `shouldMatchInt` 409 resp.json %. "non_federating_backends" `shouldMatchSet` [domainB, domainC] -testDefederationGroupConversation :: HasCallStack => App () -testDefederationGroupConversation = do - let setFederationConfig = - setField "optSettings.setFederationStrategy" "allowDynamic" - >=> removeField "optSettings.setFederationDomainConfigs" - >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) - startDynamicBackends - [ def {brigCfg = setFederationConfig}, - def {brigCfg = setFederationConfig} - ] - $ \dynDomains -> do - domains@[domainA, domainB] <- pure dynDomains - connectAllDomainsAndWaitToSync 1 domains - [uA, uB] <- createAndConnectUsers [domainA, domainB] - withWebSocket uA $ \ws -> do - -- create group conversation owned by domainB - convId <- bindResponse (postConversation uB (defProteus {qualifiedUsers = [uA]})) $ \r -> do - r.status `shouldMatchInt` 201 - r.json %. "qualified_id" - - -- check conversation exists and uB is a member from POV of uA - bindResponse (getConversation uA convId) $ \r -> do - r.status `shouldMatchInt` 200 - members <- r.json %. "members.others" & asList - qIds <- for members (\m -> m %. "qualified_id") - uBQId <- objQidObject uB - qIds `shouldMatchSet` [uBQId] - - -- check conversation exists and uA is a member from POV of uB - bindResponse (getConversation uB convId) $ \r -> do - r.status `shouldMatchInt` 200 - members <- r.json %. "members.others" & asList - qIds <- for members (\m -> m %. "qualified_id") - uAQId <- objQidObject uA - qIds `shouldMatchSet` [uAQId] - - -- domainA stops federating with domainB - void $ deleteFedConn domainA domainB - - -- assert conversation deleted from domainA - retryT $ - bindResponse (getConversation uA convId) $ \r -> - r.status `shouldMatchInt` 404 - - -- assert federation.delete event is sent twice - void $ - awaitNMatches - 2 - 3 - ( \n -> do - correctType <- nPayload n %. "type" `isEqual` "federation.delete" - if correctType - then nPayload n %. "domain" `isEqual` domainB - else pure False - ) - ws - - -- assert no conversation.delete event is sent to uA - eventPayloads <- - getNotifications uA "cA" def - >>= getJSON 200 - >>= \n -> n %. "notifications" & asList >>= \ns -> for ns nPayload - - forM_ eventPayloads $ \p -> - p %. "type" `shouldNotMatch` "conversation.delete" - -testDefederationOneOnOne :: HasCallStack => App () -testDefederationOneOnOne = do - let setFederationConfig = - setField "optSettings.setFederationStrategy" "allowDynamic" - >=> removeField "optSettings.setFederationDomainConfigs" - >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) - startDynamicBackends - [ def {brigCfg = setFederationConfig}, - def {brigCfg = setFederationConfig} - ] - $ \dynDomains -> do - domains@[domainA, domainB] <- pure dynDomains - connectAllDomainsAndWaitToSync 1 domains - [uA, uB] <- createAndConnectUsers [domainA, domainB] - -- figure out on which backend the 1:1 conversation is created - qConvId <- getConnection uA uB >>= \c -> c.json %. "qualified_conversation" - - -- check conversation exists and uB is a member from POV of uA - bindResponse (getConversation uA qConvId) $ \r -> do - r.status `shouldMatchInt` 200 - members <- r.json %. "members.others" & asList - qIds <- for members (\m -> m %. "qualified_id") - uBQId <- objQidObject uB - qIds `shouldMatchSet` [uBQId] - - -- check conversation exists and uA is a member from POV of uB - bindResponse (getConversation uB qConvId) $ \r -> do - r.status `shouldMatchInt` 200 - members <- r.json %. "members.others" & asList - qIds <- for members (\m -> m %. "qualified_id") - uAQId <- objQidObject uA - qIds `shouldMatchSet` [uAQId] - - conversationOwningDomain <- objDomain qConvId - - when (domainA == conversationOwningDomain) $ do - -- conversation is created on domainA - assertFederationTerminatingUserNoConvDeleteEvent uB qConvId domainB domainA - - when (domainB == conversationOwningDomain) $ do - -- conversation is created on domainB - assertFederationTerminatingUserNoConvDeleteEvent uA qConvId domainA domainB - - when (domainA /= conversationOwningDomain && domainB /= conversationOwningDomain) $ do - -- this should not happen - error "impossible" - where - assertFederationTerminatingUserNoConvDeleteEvent :: Value -> Value -> String -> String -> App () - assertFederationTerminatingUserNoConvDeleteEvent user convId ownDomain otherDomain = do - withWebSocket user $ \ws -> do - void $ deleteFedConn ownDomain otherDomain - - -- assert conversation deleted eventually - retryT $ - bindResponse (getConversation user convId) $ \r -> - r.status `shouldMatchInt` 404 - - -- assert federation.delete event is sent twice - void $ awaitNMatches 2 3 (\n -> nPayload n %. "type" `isEqual` "federation.delete") ws - - -- assert no conversation.delete event is sent to uA - eventPayloads <- - getNotifications user "user-client" def - >>= getJSON 200 - >>= \n -> n %. "notifications" & asList >>= \ns -> for ns nPayload - - forM_ eventPayloads $ \p -> - p %. "type" `shouldNotMatch` "conversation.delete" - testAddMembersFullyConnectedProteus :: HasCallStack => App () testAddMembersFullyConnectedProteus = do - withFederatingBackendsAllowDynamic 2 $ \(domainA, domainB, domainC) -> do + withFederatingBackendsAllowDynamic $ \(domainA, domainB, domainC) -> do + connectAllDomainsAndWaitToSync 2 [domainA, domainB, domainC] [u1, u2, u3] <- createAndConnectUsers [domainA, domainB, domainC] -- create conversation with no users cid <- postConversation u1 (defProteus {qualifiedUsers = []}) >>= getJSON 201 -- add members from remote backends members <- for [u2, u3] (%. "qualified_id") - bindResponse (addMembers u1 cid members) $ \resp -> do + bindResponse (addMembers u1 cid def {users = members}) $ \resp -> do resp.status `shouldMatchInt` 200 users <- resp.json %. "data.users" >>= asList addedUsers <- forM users (%. "qualified_id") @@ -327,25 +213,88 @@ testAddMembersFullyConnectedProteus = do testAddMembersNonFullyConnectedProteus :: HasCallStack => App () testAddMembersNonFullyConnectedProteus = do - withFederatingBackendsAllowDynamic 2 $ \(domainA, domainB, domainC) -> do - [u1, u2, u3] <- createAndConnectUsers [domainA, domainB, domainC] + withFederatingBackendsAllowDynamic $ \(domainA, domainB, domainC) -> do + void $ createFedConn domainA (FedConn domainB "full_search") + void $ createFedConn domainB (FedConn domainA "full_search") + void $ createFedConn domainA (FedConn domainC "full_search") + void $ createFedConn domainC (FedConn domainA "full_search") + liftIO $ threadDelay (2 * 1000 * 1000) -- wait for federation status to be updated + + -- add users + u1 <- randomUser domainA def + u2 <- randomUser domainB def + u3 <- randomUser domainC def + connectUsers2 u1 u2 + connectUsers2 u1 u3 + -- create conversation with no users cid <- postConversation u1 (defProteus {qualifiedUsers = []}) >>= getJSON 201 - -- stop federation between B and C - void $ deleteFedConn domainB domainC - void $ deleteFedConn domainC domainB - liftIO $ threadDelay (2 * 1000 * 1000) -- wait for federation status to be updated -- add members from remote backends members <- for [u2, u3] (%. "qualified_id") - bindResponse (addMembers u1 cid members) $ \resp -> do + bindResponse (addMembers u1 cid def {users = members}) $ \resp -> do resp.status `shouldMatchInt` 409 resp.json %. "non_federating_backends" `shouldMatchSet` [domainB, domainC] +testAddMember :: HasCallStack => App () +testAddMember = do + alice <- randomUser OwnDomain def + aliceId <- alice %. "qualified_id" + -- create conversation with no users + cid <- postConversation alice defProteus >>= getJSON 201 + bob <- randomUser OwnDomain def + bobId <- bob %. "qualified_id" + let addMember = addMembers alice cid def {role = Just "wire_member", users = [bobId]} + bindResponse addMember $ \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "not-connected" + connectUsers2 alice bob + bindResponse addMember $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "type" `shouldMatch` "conversation.member-join" + resp.json %. "qualified_from" `shouldMatch` objQidObject alice + resp.json %. "qualified_conversation" `shouldMatch` objQidObject cid + users <- resp.json %. "data.users" >>= asList + addedUsers <- forM users (%. "qualified_id") + addedUsers `shouldMatchSet` [bobId] + + -- check that both users can see the conversation + bindResponse (getConversation alice cid) $ \resp -> do + resp.status `shouldMatchInt` 200 + mems <- resp.json %. "members.others" & asList + mem <- assertOne mems + mem %. "qualified_id" `shouldMatch` bobId + mem %. "conversation_role" `shouldMatch` "wire_member" + + bindResponse (getConversation bob cid) $ \resp -> do + resp.status `shouldMatchInt` 200 + mems <- resp.json %. "members.others" & asList + mem <- assertOne mems + mem %. "qualified_id" `shouldMatch` aliceId + mem %. "conversation_role" `shouldMatch` "wire_admin" + +testAddMemberV1 :: HasCallStack => Domain -> App () +testAddMemberV1 domain = do + [alice, bob] <- createAndConnectUsers [OwnDomain, domain] + conv <- postConversation alice defProteus >>= getJSON 201 + bobId <- bob %. "qualified_id" + let opts = + def + { version = Just 1, + role = Just "wire_member", + users = [bobId] + } + bindResponse (addMembers alice conv opts) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "type" `shouldMatch` "conversation.member-join" + resp.json %. "qualified_from" `shouldMatch` objQidObject alice + resp.json %. "qualified_conversation" `shouldMatch` objQidObject conv + users <- resp.json %. "data.users" >>= asList + traverse (%. "qualified_id") users `shouldMatchSet` [bobId] + testConvWithUnreachableRemoteUsers :: HasCallStack => App () testConvWithUnreachableRemoteUsers = do let overrides = def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} - <> fullSearchWithAll ([alice, alex, bob, charlie, dylan], domains) <- startDynamicBackends [overrides, overrides] $ \domains -> do own <- make OwnDomain & asString @@ -366,7 +315,6 @@ testAddReachableWithUnreachableRemoteUsers :: HasCallStack => App () testAddReachableWithUnreachableRemoteUsers = do let overrides = def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} - <> fullSearchWithAll ([alex, bob], conv, domains) <- startDynamicBackends [overrides, overrides] $ \domains -> do own <- make OwnDomain & asString @@ -379,7 +327,7 @@ testAddReachableWithUnreachableRemoteUsers = do pure ([alex, bob], conv, domains) bobId <- bob %. "qualified_id" - bindResponse (addMembers alex conv [bobId]) $ \resp -> do + bindResponse (addMembers alex conv def {users = [bobId]}) $ \resp -> do -- This test is updated to reflect the changes in `performConversationJoin` -- `performConversationJoin` now does a full check between all federation members -- that will be in the conversation when adding users to a conversation. This is @@ -392,7 +340,6 @@ testAddUnreachable :: HasCallStack => App () testAddUnreachable = do let overrides = def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} - <> fullSearchWithAll ([alex, charlie], [charlieDomain, dylanDomain], conv) <- startDynamicBackends [overrides, overrides] $ \domains -> do own <- make OwnDomain & asString @@ -404,19 +351,36 @@ testAddUnreachable = do pure ([alex, charlie], domains, conv) charlieId <- charlie %. "qualified_id" - bindResponse (addMembers alex conv [charlieId]) $ \resp -> do + bindResponse (addMembers alex conv def {users = [charlieId]}) $ \resp -> do resp.status `shouldMatchInt` 533 -- All of the domains that are in the conversation, or will be in the conversation, -- need to be reachable so we can check that the graph for those domains is fully connected. resp.json %. "unreachable_backends" `shouldMatchSet` [charlieDomain, dylanDomain] +testGetOneOnOneConvInStatusSentFromRemote :: App () +testGetOneOnOneConvInStatusSentFromRemote = do + d1User <- randomUser OwnDomain def + let shouldBeLocal = True + (d2Usr, d2ConvId) <- generateRemoteAndConvIdWithDomain OtherDomain (not shouldBeLocal) d1User + bindResponse (postConnection d1User d2Usr) $ \r -> do + r.status `shouldMatchInt` 201 + r.json %. "status" `shouldMatch` "sent" + bindResponse (listConversationIds d1User def) $ \r -> do + r.status `shouldMatchInt` 200 + convIds <- r.json %. "qualified_conversations" & asList + filter ((==) d2ConvId) convIds `shouldMatch` [d2ConvId] + bindResponse (getConnections d1User) $ \r -> do + qConvIds <- r.json %. "connections" & asList >>= traverse (%. "qualified_conversation") + filter ((==) d2ConvId) qConvIds `shouldMatch` [d2ConvId] + resp <- getConversation d1User d2ConvId + resp.status `shouldMatchInt` 200 + testAddingUserNonFullyConnectedFederation :: HasCallStack => App () testAddingUserNonFullyConnectedFederation = do let overrides = def { brigCfg = setField "optSettings.setFederationStrategy" "allowDynamic" - >=> removeField "optSettings.setFederationDomainConfigs" } startDynamicBackends [overrides] $ \[dynBackend] -> do own <- asString OwnDomain @@ -424,10 +388,6 @@ testAddingUserNonFullyConnectedFederation = do -- Ensure that dynamic backend only federates with own domain, but not other -- domain. - -- - -- FUTUREWORK: deleteAllFedConns at the time of acquiring a backend, so - -- tests don't affect each other. - deleteAllFedConns dynBackend void $ createFedConn dynBackend (FedConn own "full_search") alice <- randomUser own def @@ -442,34 +402,16 @@ testAddingUserNonFullyConnectedFederation = do bobId <- bob %. "qualified_id" charlieId <- charlie %. "qualified_id" - bindResponse (addMembers alice conv [bobId, charlieId]) $ \resp -> do + bindResponse (addMembers alice conv def {users = [bobId, charlieId]}) $ \resp -> do resp.status `shouldMatchInt` 409 resp.json %. "non_federating_backends" `shouldMatchSet` [other, dynBackend] -testGetOneOnOneConvInStatusSentFromRemote :: App () -testGetOneOnOneConvInStatusSentFromRemote = do - d1User <- randomUser OwnDomain def - let shouldBeLocal = True - (d2Usr, d2ConvId) <- generateRemoteAndConvIdWithDomain OtherDomain (not shouldBeLocal) d1User - bindResponse (postConnection d1User d2Usr) $ \r -> do - r.status `shouldMatchInt` 201 - r.json %. "status" `shouldMatch` "sent" - bindResponse (listConversationIds d1User def) $ \r -> do - r.status `shouldMatchInt` 200 - convIds <- r.json %. "qualified_conversations" & asList - filter ((==) d2ConvId) convIds `shouldMatch` [d2ConvId] - bindResponse (getConnections d1User) $ \r -> do - qConvIds <- r.json %. "connections" & asList >>= traverse (%. "qualified_conversation") - filter ((==) d2ConvId) qConvIds `shouldMatch` [d2ConvId] - resp <- getConversation d1User d2ConvId - resp.status `shouldMatchInt` 200 - testMultiIngressGuestLinks :: HasCallStack => App () testMultiIngressGuestLinks = do do configuredURI <- readServiceConfig Galley & (%. "settings.conversationCodeURI") & asText - (user, _) <- createTeam OwnDomain + (user, _, _) <- createTeam OwnDomain 1 conv <- postConversation user (allowGuests defProteus) >>= getJSON 201 bindResponse (postConversationCode user conv Nothing Nothing) $ \resp -> do @@ -503,7 +445,7 @@ testMultiIngressGuestLinks = do } ) $ \domain -> do - (user, _) <- createTeam domain + (user, _, _) <- createTeam domain 1 conv <- postConversation user (allowGuests defProteus) >>= getJSON 201 bindResponse (postConversationCode user conv Nothing (Just "red.example.com")) $ \resp -> do @@ -529,3 +471,278 @@ testMultiIngressGuestLinks = do bindResponse (getConversationCode user conv (Just "unknown.example.com")) $ \resp -> do res <- getJSON 403 resp res %. "label" `shouldMatch` "access-denied" + +testAddUserWhenOtherBackendOffline :: HasCallStack => App () +testAddUserWhenOtherBackendOffline = do + let overrides = + def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} + ([alice, alex], conv) <- + startDynamicBackends [overrides] $ \domains -> do + own <- make OwnDomain & asString + [alice, alex, charlie] <- + createAndConnectUsers $ [own, own] <> domains + + let newConv = defProteus {qualifiedUsers = [charlie]} + conv <- postConversation alice newConv >>= getJSON 201 + pure ([alice, alex], conv) + bindResponse (addMembers alice conv def {users = [alex]}) $ \resp -> do + resp.status `shouldMatchInt` 200 + +testSynchroniseUserRemovalNotification :: HasCallStack => App () +testSynchroniseUserRemovalNotification = do + resourcePool <- asks resourcePool + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + runCodensity (acquireResources 1 resourcePool) $ \[dynBackend] -> do + (conv, charlie, client) <- + runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do + charlie <- randomUser dynBackend.berDomain def + client <- objId $ bindResponse (addClient charlie def) $ getJSON 201 + mapM_ (connectUsers2 charlie) [alice, bob] + conv <- + postConversation alice (defProteus {qualifiedUsers = [bob, charlie]}) + >>= getJSON 201 + pure (conv, charlie, client) + + let newConvName = "The new conversation name" + bindResponse (changeConversationName alice conv newConvName) $ \resp -> + resp.status `shouldMatchInt` 200 + bindResponse (removeMember alice conv charlie) $ \resp -> + resp.status `shouldMatchInt` 200 + runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do + nameNotif <- awaitNotification charlie client noValue 2 isConvNameChangeNotif + nameNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + nameNotif %. "payload.0.data.name" `shouldMatch` newConvName + leaveNotif <- awaitNotification charlie client noValue 2 isConvLeaveNotif + leaveNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + +testConvRenaming :: HasCallStack => App () +testConvRenaming = do + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + conv <- + postConversation alice (defProteus {qualifiedUsers = [bob]}) + >>= getJSON 201 + let newConvName = "The new conversation name" + withWebSockets [alice, bob] $ \wss -> do + for_ wss $ \ws -> do + void $ changeConversationName alice conv newConvName >>= getBody 200 + nameNotif <- awaitMatch 10 isConvNameChangeNotif ws + nameNotif %. "payload.0.data.name" `shouldMatch` newConvName + nameNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + +testReceiptModeWithRemotesOk :: HasCallStack => App () +testReceiptModeWithRemotesOk = do + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + conv <- + postConversation alice (defProteus {qualifiedUsers = [bob]}) + >>= getJSON 201 + withWebSockets [alice, bob] $ \wss -> do + void $ updateReceiptMode alice conv (43 :: Int) >>= getBody 200 + for_ wss $ \ws -> do + notif <- awaitMatch 10 isReceiptModeUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + notif %. "payload.0.data.receipt_mode" `shouldMatchInt` 43 + +testReceiptModeWithRemotesUnreachable :: HasCallStack => App () +testReceiptModeWithRemotesUnreachable = do + ownDomain <- asString OwnDomain + alice <- randomUser ownDomain def + conv <- startDynamicBackends [mempty] $ \[dynBackend] -> do + bob <- randomUser dynBackend def + connectUsers2 alice bob + postConversation alice (defProteus {qualifiedUsers = [bob]}) + >>= getJSON 201 + withWebSocket alice $ \ws -> do + void $ updateReceiptMode alice conv (43 :: Int) >>= getBody 200 + notif <- awaitMatch 10 isReceiptModeUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + notif %. "payload.0.data.receipt_mode" `shouldMatchInt` 43 + +testDeleteLocalMember :: HasCallStack => App () +testDeleteLocalMember = do + [alice, alex, bob] <- createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain] + conv <- + postConversation alice (defProteus {qualifiedUsers = [alex, bob]}) + >>= getJSON 201 + bindResponse (removeMember alice conv alex) $ \resp -> do + r <- getJSON 200 resp + r %. "type" `shouldMatch` "conversation.member-leave" + r %. "qualified_conversation" `shouldMatch` objQidObject conv + r %. "qualified_from" `shouldMatch` objQidObject alice + r %. "data.qualified_user_ids.0" `shouldMatch` objQidObject alex + -- Now that Alex is gone, try removing her once again + bindResponse (removeMember alice conv alex) $ \r -> do + r.status `shouldMatchInt` 204 + r.jsonBody `shouldMatch` (Nothing @Aeson.Value) + +testDeleteRemoteMember :: HasCallStack => App () +testDeleteRemoteMember = do + [alice, alex, bob] <- createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain] + conv <- + postConversation alice (defProteus {qualifiedUsers = [alex, bob]}) + >>= getJSON 201 + bindResponse (removeMember alice conv bob) $ \resp -> do + r <- getJSON 200 resp + r %. "type" `shouldMatch` "conversation.member-leave" + r %. "qualified_conversation" `shouldMatch` objQidObject conv + r %. "qualified_from" `shouldMatch` objQidObject alice + r %. "data.qualified_user_ids.0" `shouldMatch` objQidObject bob + -- Now that Bob is gone, try removing him once again + bindResponse (removeMember alice conv bob) $ \r -> do + r.status `shouldMatchInt` 204 + r.jsonBody `shouldMatch` (Nothing @Aeson.Value) + +testDeleteRemoteMemberRemoteUnreachable :: HasCallStack => App () +testDeleteRemoteMemberRemoteUnreachable = do + [alice, bob, bart] <- createAndConnectUsers [OwnDomain, OtherDomain, OtherDomain] + conv <- startDynamicBackends [mempty] $ \[dynBackend] -> do + charlie <- randomUser dynBackend def + connectUsers2 alice charlie + postConversation + alice + (defProteus {qualifiedUsers = [bob, bart, charlie]}) + >>= getJSON 201 + void $ withWebSockets [alice, bob] $ \wss -> do + void $ removeMember alice conv bob >>= getBody 200 + for wss $ \ws -> do + leaveNotif <- awaitMatch 10 isConvLeaveNotif ws + leaveNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + leaveNotif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + leaveNotif %. "payload.0.data.qualified_user_ids.0" `shouldMatch` objQidObject bob + -- Now that Bob is gone, try removing him once again + bindResponse (removeMember alice conv bob) $ \r -> do + r.status `shouldMatchInt` 204 + r.jsonBody `shouldMatch` (Nothing @Aeson.Value) + +testDeleteTeamConversationWithRemoteMembers :: HasCallStack => App () +testDeleteTeamConversationWithRemoteMembers = do + (alice, team, _) <- createTeam OwnDomain 1 + conv <- postConversation alice (defProteus {team = Just team}) >>= getJSON 201 + bob <- randomUser OtherDomain def + connectUsers2 alice bob + mem <- bob %. "qualified_id" + void $ addMembers alice conv def {users = [mem]} >>= getBody 200 + + void $ withWebSockets [alice, bob] $ \wss -> do + void $ deleteTeamConversation team conv alice >>= getBody 200 + for wss $ \ws -> do + notif <- awaitMatch 10 isConvDeleteNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + +testDeleteTeamConversationWithUnreachableRemoteMembers :: HasCallStack => App () +testDeleteTeamConversationWithUnreachableRemoteMembers = do + resourcePool <- asks resourcePool + (alice, team, _) <- createTeam OwnDomain 1 + conv <- postConversation alice (defProteus {team = Just team}) >>= getJSON 201 + + let assertNotification :: (HasCallStack, MakesValue n) => n -> App () + assertNotification notif = do + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + + runCodensity (acquireResources 1 resourcePool) $ \[dynBackend] -> do + (bob, bobClient) <- runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do + -- FUTUREWORK: get rid of this once the background worker is able to listen to all queues + do + ownDomain <- make OwnDomain & asString + otherDomain <- make OtherDomain & asString + let domains = [ownDomain, otherDomain, dynBackend.berDomain] + sequence_ + [ createFedConn x (FedConn y "full_search") + | x <- domains, + y <- domains, + x /= y + ] + + bob <- randomUser dynBackend.berDomain def + bobClient <- objId $ bindResponse (addClient bob def) $ getJSON 201 + connectUsers2 alice bob + mem <- bob %. "qualified_id" + void $ addMembers alice conv def {users = [mem]} >>= getBody 200 + pure (bob, bobClient) + withWebSocket alice $ \ws -> do + void $ deleteTeamConversation team conv alice >>= getBody 200 + notif <- awaitMatch 10 isConvDeleteNotif ws + assertNotification notif + void $ runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do + notif <- awaitNotification bob bobClient noValue 2 isConvDeleteNotif + assertNotification notif + +testLeaveConversationSuccess :: HasCallStack => App () +testLeaveConversationSuccess = do + [alice, bob, chad, dee] <- + createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain, OtherDomain] + [aClient, bClient] <- forM [alice, bob] $ \user -> + objId $ bindResponse (addClient user def) $ getJSON 201 + let overrides = + def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} + startDynamicBackends [overrides] $ \[dynDomain] -> do + eve <- randomUser dynDomain def + eClient <- objId $ bindResponse (addClient eve def) $ getJSON 201 + connectUsers2 alice eve + conv <- + postConversation + alice + ( defProteus + { qualifiedUsers = [bob, chad, dee, eve] + } + ) + >>= getJSON 201 + void $ removeMember chad conv chad >>= getBody 200 + assertLeaveNotification chad conv alice aClient chad + assertLeaveNotification chad conv bob bClient chad + assertLeaveNotification chad conv eve eClient chad + +testOnUserDeletedConversations :: HasCallStack => App () +testOnUserDeletedConversations = do + let overrides = + def {brigCfg = setField "optSettings.setFederationStrategy" "allowAll"} + startDynamicBackends [overrides] $ \[dynDomain] -> do + [ownDomain, otherDomain] <- forM [OwnDomain, OtherDomain] asString + [alice, alex, bob, bart, chad] <- + createAndConnectUsers [ownDomain, ownDomain, otherDomain, otherDomain, dynDomain] + bobId <- bob %. "qualified_id" + ooConvId <- do + l <- getAllConvs alice + let isWith users c = do + t <- (==) <$> (c %. "type" & asInt) <*> pure 2 + others <- c %. "members.others" & asList + qIds <- for others (%. "qualified_id") + pure $ qIds == users && t + c <- head <$> filterM (isWith [bobId]) l + c %. "qualified_id" + + mainConvBefore <- + postConversation alice (defProteus {qualifiedUsers = [alex, bob, bart, chad]}) + >>= getJSON 201 + + void $ withWebSocket alex $ \ws -> do + void $ deleteUser bob >>= getBody 200 + n <- awaitMatch 10 isConvLeaveNotif ws + n %. "payload.0.qualified_from" `shouldMatch` bobId + n %. "payload.0.qualified_conversation" `shouldMatch` (mainConvBefore %. "qualified_id") + + do + -- Bob is not in the one-to-one conversation with Alice any more + conv <- getConversation alice ooConvId >>= getJSON 200 + shouldBeEmpty $ conv %. "members.others" + do + -- Bob is not in the main conversation any more + mainConvAfter <- getConversation alice (mainConvBefore %. "qualified_id") >>= getJSON 200 + mems <- mainConvAfter %. "members.others" & asList + memIds <- for mems (%. "qualified_id") + expectedIds <- for [alex, bart, chad] (%. "qualified_id") + memIds `shouldMatchSet` expectedIds + +testUpdateConversationByRemoteAdmin :: HasCallStack => App () +testUpdateConversationByRemoteAdmin = do + [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OtherDomain, OtherDomain] + conv <- + postConversation alice (defProteus {qualifiedUsers = [bob, charlie]}) + >>= getJSON 201 + void $ updateRole alice bob "wire_admin" (conv %. "qualified_id") >>= getBody 200 + void $ withWebSockets [alice, bob, charlie] $ \wss -> do + void $ updateReceiptMode bob conv (41 :: Int) >>= getBody 200 + for_ wss $ \ws -> awaitMatch 10 isReceiptModeUpdateNotif ws diff --git a/integration/test/Test/Defederation.hs b/integration/test/Test/Defederation.hs deleted file mode 100644 index 513efe535f6..00000000000 --- a/integration/test/Test/Defederation.hs +++ /dev/null @@ -1,85 +0,0 @@ -module Test.Defederation where - -import API.BrigInternal -import API.BrigInternal qualified as Internal -import API.Galley (defProteus, getConversation, postConversation, qualifiedUsers) -import Control.Applicative -import Data.Aeson qualified as Aeson -import GHC.Stack -import SetupHelpers -import Testlib.Prelude - -testDefederationRemoteNotifications :: HasCallStack => App () -testDefederationRemoteNotifications = do - let remoteDomain = "example.example.com" - -- Setup federation between OtherDomain and the remote domain - bindResponse (createFedConn OtherDomain $ object ["domain" .= remoteDomain, "search_policy" .= "full_search"]) $ \resp -> - resp.status `shouldMatchInt` 200 - - -- Setup a remote user we can get notifications for. - user <- randomUser OtherDomain def - - withWebSocket user $ \ws -> do - -- Defederate from a domain that doesn't exist. This won't do anything to the databases - -- But it will send out notifications that we can wait on. - -- Begin the whole process at Brig, the same as an operator would. - void $ deleteFedConn OwnDomain remoteDomain - void $ awaitNMatches 2 3 (\n -> nPayload n %. "type" `isEqual` "federation.connectionRemoved") ws - -testDefederationNonFullyConnectedGraph :: HasCallStack => App () -testDefederationNonFullyConnectedGraph = do - let setFederationConfig = - setField "optSettings.setFederationStrategy" "allowDynamic" - >=> removeField "optSettings.setFederationDomainConfigs" - >=> setField "optSettings.setFederationDomainConfigsUpdateFreq" (Aeson.Number 1) - startDynamicBackends - [ def {brigCfg = setFederationConfig}, - def {brigCfg = setFederationConfig}, - def {brigCfg = setFederationConfig} - ] - $ \dynDomains -> do - domains@[domainA, domainB, domainC] <- pure dynDomains - connectAllDomainsAndWaitToSync 1 domains - - -- create a few extra users and connections to make sure that does not lead to any extra `connectionRemoved` notifications - [uA, uA2, _, _, uB, uC] <- createAndConnectUsers [domainA, domainA, domainA, domainA, domainB, domainC] >>= traverse objQidObject - - -- create group conversation owned by domainA with users from domainB and domainC - convId <- bindResponse (postConversation uA (defProteus {qualifiedUsers = [uA2, uB, uC]})) $ \r -> do - r.status `shouldMatchInt` 201 - r.json %. "qualified_id" - - -- check conversation exists on all backends - checkConv convId uA [uB, uC, uA2] - checkConv convId uB [uA, uC, uA2] - checkConv convId uC [uA, uB, uA2] - - withWebSocket uA $ \wsA -> do - -- one of the 2 non-conversation-owning domains (domainB and domainC) - -- defederate from the other non-conversation-owning domain - void $ Internal.deleteFedConn domainB domainC - - -- assert that clients from domainA receive federation.connectionRemoved events - -- Notifications being delivered exactly twice - void $ awaitNMatches 2 20 (isConnectionRemoved [domainB, domainC]) wsA - - -- remote members should be removed from local conversation eventually - retryT $ checkConv convId uA [uA2] - where - isConnectionRemoved :: [String] -> Value -> App Bool - isConnectionRemoved domains n = do - correctType <- nPayload n %. "type" `isEqual` "federation.connectionRemoved" - if correctType - then do - domsV <- nPayload n %. "domains" & asList - domsStr <- for domsV asString <&> sort - pure $ domsStr == sort domains - else pure False - - checkConv :: Value -> Value -> [Value] -> App () - checkConv convId user expectedOtherMembers = do - bindResponse (getConversation user convId) $ \r -> do - r.status `shouldMatchInt` 200 - members <- r.json %. "members.others" & asList - qIds <- for members (\m -> m %. "qualified_id") - qIds `shouldMatchSet` expectedOtherMembers diff --git a/integration/test/Test/Demo.hs b/integration/test/Test/Demo.hs index 5f16b790f96..e6b7b188fb8 100644 --- a/integration/test/Test/Demo.hs +++ b/integration/test/Test/Demo.hs @@ -9,6 +9,7 @@ import Control.Monad.Cont import GHC.Stack import SetupHelpers import Testlib.Prelude +import UnliftIO.Concurrent (threadDelay) -- | Legalhold clients cannot be deleted. testCantDeleteLHClient :: HasCallStack => App () @@ -42,7 +43,7 @@ testModifiedBrig = do testModifiedGalley :: HasCallStack => App () testModifiedGalley = do - (_user, tid) <- createTeam OwnDomain + (_user, tid, _) <- createTeam OwnDomain 1 let getFeatureStatus :: (MakesValue domain) => domain -> String -> App Value getFeatureStatus domain team = do @@ -55,7 +56,7 @@ testModifiedGalley = do withModifiedBackend def {galleyCfg = setField "settings.featureFlags.teamSearchVisibility" "enabled-by-default"} $ \domain -> do - (_user, tid') <- createTeam domain + (_user, tid', _) <- createTeam domain 1 getFeatureStatus domain tid' `shouldMatch` "enabled" testModifiedCannon :: HasCallStack => App () @@ -83,7 +84,7 @@ testModifiedServices = do } withModifiedBackend serviceMap $ \domain -> do - (_user, tid) <- createTeam domain + (_user, tid, _) <- createTeam domain 1 bindResponse (Internal.getTeamFeature domain "searchVisibility" tid) $ \res -> do res.status `shouldMatchInt` 200 res.json %. "status" `shouldMatch` "enabled" @@ -173,12 +174,16 @@ testIndependentESIndices = do testDynamicBackendsFederation :: HasCallStack => App () testDynamicBackendsFederation = do - startDynamicBackends [def <> fullSearchWithAll, def <> fullSearchWithAll] $ \dynDomains -> do + startDynamicBackends [def, def] $ \dynDomains -> do [aDynDomain, anotherDynDomain] <- pure dynDomains + _ <- Internal.createFedConn anotherDynDomain (Internal.FedConn aDynDomain "full_search") + threadDelay 2_000_000 + u1 <- randomUser aDynDomain def u2 <- randomUser anotherDynDomain def uid2 <- objId u2 Internal.refreshIndex anotherDynDomain + bindResponse (Public.searchContacts u1 (u2 %. "name") anotherDynDomain) $ \resp -> do resp.status `shouldMatchInt` 200 docs <- resp.json %. "documents" >>= asList diff --git a/integration/test/Test/Federation.hs b/integration/test/Test/Federation.hs index 3dc0123cfb4..4bb2b827094 100644 --- a/integration/test/Test/Federation.hs +++ b/integration/test/Test/Federation.hs @@ -4,6 +4,7 @@ module Test.Federation where import API.Brig qualified as API +import API.BrigInternal qualified as API import API.Galley import Control.Lens import Control.Monad.Codensity @@ -27,10 +28,22 @@ testNotificationsForOfflineBackends = do otherClient <- objId $ bindResponse (API.addClient otherUser def) $ getJSON 201 otherClient2 <- objId $ bindResponse (API.addClient otherUser2 def) $ getJSON 201 - -- We call it 'downBackend' because it is down for the most of this test + -- We call it 'downBackend' because it is down for most of this test -- except for setup and assertions. Perhaps there is a better name. runCodensity (acquireResources 1 resourcePool) $ \[downBackend] -> do (downUser1, downClient1, downUser2, upBackendConv, downBackendConv) <- runCodensity (startDynamicBackend downBackend mempty) $ \_ -> do + -- FUTUREWORK: get rid of this once the background worker is able to listen to all queues + do + ownDomain <- make OwnDomain & asString + otherDomain <- make OtherDomain & asString + let domains = [ownDomain, otherDomain, downBackend.berDomain] + sequence_ + [ API.createFedConn x (API.FedConn y "full_search") + | x <- domains, + y <- domains, + x /= y + ] + downUser1 <- randomUser downBackend.berDomain def downUser2 <- randomUser downBackend.berDomain def downClient1 <- objId $ bindResponse (API.addClient downUser1 def) $ getJSON 201 @@ -41,67 +54,74 @@ testNotificationsForOfflineBackends = do downBackendConv <- bindResponse (postConversation downUser1 (defProteus {qualifiedUsers = [otherUser, delUser]})) $ getJSON 201 pure (downUser1, downClient1, downUser2, upBackendConv, downBackendConv) - -- Even when a participating backend is down, messages to conversations - -- owned by other backends should go. - successfulMsgForOtherUsers <- mkProteusRecipients otherUser [(otherUser, [otherClient]), (otherUser2, [otherClient2])] "success message for other user" - successfulMsgForDownUser <- mkProteusRecipient downUser1 downClient1 "success message for down user" - let successfulMsg = - Proto.defMessage @Proto.QualifiedNewOtrMessage - & #sender . Proto.client .~ (delClient ^?! hex) - & #recipients .~ [successfulMsgForOtherUsers, successfulMsgForDownUser] - & #reportAll .~ Proto.defMessage - bindResponse (postProteusMessage delUser upBackendConv successfulMsg) assertSuccess - - -- When conversation owning backend is down, messages will fail to be sent. - failedMsgForOtherUser <- mkProteusRecipient otherUser otherClient "failed message for other user" - failedMsgForDownUser <- mkProteusRecipient downUser1 downClient1 "failed message for down user" - let failedMsg = - Proto.defMessage @Proto.QualifiedNewOtrMessage - & #sender . Proto.client .~ (delClient ^?! hex) - & #recipients .~ [failedMsgForOtherUser, failedMsgForDownUser] - & #reportAll .~ Proto.defMessage - bindResponse (postProteusMessage delUser downBackendConv failedMsg) $ \resp -> - -- Due to the way federation breaks in local env vs K8s, it can return 521 - -- (local) or 533 (K8s). - resp.status `shouldMatchOneOf` [Number 521, Number 533] - - -- Conversation creation with people from down backend should fail - bindResponse (postConversation delUser (defProteus {qualifiedUsers = [otherUser, downUser1]})) $ \resp -> - resp.status `shouldMatchInt` 533 - - -- Adding users to an up backend conversation should not work when one of - -- the participating backends is down. This is due to not being able to - -- check non-fully connected graph between all participating backends - otherUser3 <- randomUser OtherDomain def - connectUsers2 delUser otherUser3 - bindResponse (addMembers delUser upBackendConv [otherUser3]) $ \resp -> - resp.status `shouldMatchInt` 533 - - -- Adding users from down backend to a conversation should also fail - bindResponse (addMembers delUser upBackendConv [downUser2]) $ \resp -> - resp.status `shouldMatchInt` 533 - - -- Removing users from an up backend conversation should work even when one - -- of the participating backends is down. - bindResponse (removeMember delUser upBackendConv otherUser2) $ \resp -> - resp.status `shouldMatchInt` 200 - - -- User deletions should eventually make it to the other backend. - deleteUser delUser - - let isOtherUser2LeaveUpConvNotif = allPreds [isConvLeaveNotif, isNotifConv upBackendConv, isNotifForUser otherUser2] - isDelUserLeaveUpConvNotif = allPreds [isConvLeaveNotif, isNotifConv upBackendConv, isNotifForUser delUser] - - do - newMsgNotif <- awaitNotification otherUser otherClient noValue 1 isNewMessageNotif - newMsgNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject upBackendConv - newMsgNotif %. "payload.0.data.text" `shouldMatchBase64` "success message for other user" - - void $ awaitNotification otherUser otherClient (Just newMsgNotif) 1 isOtherUser2LeaveUpConvNotif - void $ awaitNotification otherUser otherClient (Just newMsgNotif) 1 isDelUserLeaveUpConvNotif - - delUserDeletedNotif <- nPayload $ awaitNotification otherUser otherClient (Just newMsgNotif) 1 isDeleteUserNotif - objQid delUserDeletedNotif `shouldMatch` objQid delUser + withWebSocket otherUser $ \ws -> do + -- Even when a participating backend is down, messages to conversations + -- owned by other backends should go. + successfulMsgForOtherUsers <- mkProteusRecipients otherUser [(otherUser, [otherClient]), (otherUser2, [otherClient2])] "success message for other user" + successfulMsgForDownUser <- mkProteusRecipient downUser1 downClient1 "success message for down user" + let successfulMsg = + Proto.defMessage @Proto.QualifiedNewOtrMessage + & #sender . Proto.client .~ (delClient ^?! hex) + & #recipients .~ [successfulMsgForOtherUsers, successfulMsgForDownUser] + & #reportAll .~ Proto.defMessage + bindResponse (postProteusMessage delUser upBackendConv successfulMsg) assertSuccess + + -- When the conversation owning backend is down, messages will fail to be sent. + failedMsgForOtherUser <- mkProteusRecipient otherUser otherClient "failed message for other user" + failedMsgForDownUser <- mkProteusRecipient downUser1 downClient1 "failed message for down user" + let failedMsg = + Proto.defMessage @Proto.QualifiedNewOtrMessage + & #sender . Proto.client .~ (delClient ^?! hex) + & #recipients .~ [failedMsgForOtherUser, failedMsgForDownUser] + & #reportAll .~ Proto.defMessage + bindResponse (postProteusMessage delUser downBackendConv failedMsg) $ \resp -> + -- Due to the way federation breaks in local env vs K8s, it can return 521 + -- (local) or 533 (K8s). + resp.status `shouldMatchOneOf` [Number 521, Number 533] + + -- Conversation creation with people from down backend should fail + bindResponse (postConversation delUser (defProteus {qualifiedUsers = [otherUser, downUser1]})) $ \resp -> + resp.status `shouldMatchInt` 533 + + -- Adding users to an up backend conversation should not work when one of + -- the participating backends is down. This is due to not being able to + -- check non-fully connected graph between all participating backends + -- however, if the backend of the user to be added is already part of the conversation, we do not need to do the check + -- and the user can be added as long as the backend is reachable + otherUser3 <- randomUser OtherDomain def + connectUsers2 delUser otherUser3 + bindResponse (addMembers delUser upBackendConv def {users = [otherUser3]}) $ \resp -> + resp.status `shouldMatchInt` 200 + + -- Adding users from down backend to a conversation should fail + bindResponse (addMembers delUser upBackendConv def {users = [downUser2]}) $ \resp -> + resp.status `shouldMatchInt` 533 + + -- Removing users from an up backend conversation should work even when one + -- of the participating backends is down. + bindResponse (removeMember delUser upBackendConv otherUser2) $ \resp -> + resp.status `shouldMatchInt` 200 + + -- Even removing a user from the down backend itself should work. + bindResponse (removeMember delUser upBackendConv delUser) $ \resp -> + resp.status `shouldMatchInt` 200 + + -- User deletions should eventually make it to the other backend. + deleteUser delUser + + let isOtherUser2LeaveUpConvNotif = allPreds [isConvLeaveNotif, isNotifConv upBackendConv, isNotifForUser otherUser2] + isDelUserLeaveUpConvNotif = allPreds [isConvLeaveNotif, isNotifConv upBackendConv, isNotifForUser delUser] + + do + newMsgNotif <- awaitMatch 10 isNewMessageNotif ws + newMsgNotif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject upBackendConv + newMsgNotif %. "payload.0.data.text" `shouldMatchBase64` "success message for other user" + + void $ awaitMatch 10 isOtherUser2LeaveUpConvNotif ws + void $ awaitMatch 10 isDelUserLeaveUpConvNotif ws + + delUserDeletedNotif <- nPayload $ awaitMatch 10 isDeleteUserNotif ws + objQid delUserDeletedNotif `shouldMatch` objQid delUser runCodensity (startDynamicBackend downBackend mempty) $ \_ -> do newMsgNotif <- awaitNotification downUser1 downClient1 noValue 5 isNewMessageNotif @@ -122,8 +142,3 @@ testNotificationsForOfflineBackends = do delUserDeletedNotif <- nPayload $ awaitNotification downUser1 downClient1 (Just newMsgNotif) 1 isDeleteUserNotif objQid delUserDeletedNotif `shouldMatch` objQid delUser - -allPreds :: (Applicative f) => [a -> f Bool] -> a -> f Bool -allPreds [] _ = pure True -allPreds [p] x = p x -allPreds (p1 : ps) x = (&&) <$> p1 x <*> allPreds ps x diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 82de95fa190..e1bbd3dc5d2 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -68,7 +68,7 @@ testStaleApplicationMessage otherDomain = do testMixedProtocolUpgrade :: HasCallStack => Domain -> App () testMixedProtocolUpgrade secondDomain = do - (alice, tid) <- createTeam OwnDomain + (alice, tid, _) <- createTeam OwnDomain 1 [bob, charlie] <- replicateM 2 (randomUser secondDomain def) connectUsers [alice, bob, charlie] @@ -119,7 +119,7 @@ testMixedProtocolNonTeam secondDomain = do testMixedProtocolAddUsers :: HasCallStack => Domain -> App () testMixedProtocolAddUsers secondDomain = do - (alice, tid) <- createTeam OwnDomain + (alice, tid, _) <- createTeam OwnDomain 1 [bob, charlie] <- replicateM 2 (randomUser secondDomain def) connectUsers [alice, bob, charlie] @@ -147,7 +147,7 @@ testMixedProtocolAddUsers secondDomain = do testMixedProtocolUserLeaves :: HasCallStack => Domain -> App () testMixedProtocolUserLeaves secondDomain = do - (alice, tid) <- createTeam OwnDomain + (alice, tid, _) <- createTeam OwnDomain 1 bob <- randomUser secondDomain def connectUsers [alice, bob] @@ -182,7 +182,7 @@ testMixedProtocolUserLeaves secondDomain = do testMixedProtocolAddPartialClients :: HasCallStack => Domain -> App () testMixedProtocolAddPartialClients secondDomain = do - (alice, tid) <- createTeam OwnDomain + (alice, tid, _) <- createTeam OwnDomain 1 bob <- randomUser secondDomain def connectUsers [alice, bob] @@ -220,7 +220,7 @@ testMixedProtocolAddPartialClients secondDomain = do testMixedProtocolRemovePartialClients :: HasCallStack => Domain -> App () testMixedProtocolRemovePartialClients secondDomain = do - (alice, tid) <- createTeam OwnDomain + (alice, tid, _) <- createTeam OwnDomain 1 bob <- randomUser secondDomain def connectUsers [alice, bob] @@ -245,7 +245,7 @@ testMixedProtocolRemovePartialClients secondDomain = do testMixedProtocolAppMessagesAreDenied :: HasCallStack => Domain -> App () testMixedProtocolAppMessagesAreDenied secondDomain = do - (alice, tid) <- createTeam OwnDomain + (alice, tid, _) <- createTeam OwnDomain 1 bob <- randomUser secondDomain def connectUsers [alice, bob] @@ -441,7 +441,7 @@ testJoinSubConv = do testDeleteParentOfSubConv :: HasCallStack => Domain -> App () testDeleteParentOfSubConv secondDomain = do - (alice, tid) <- createTeam OwnDomain + (alice, tid, _) <- createTeam OwnDomain 1 bob <- randomUser secondDomain def connectUsers [alice, bob] diff --git a/integration/test/Test/MLS/One2One.hs b/integration/test/Test/MLS/One2One.hs index a7de9fe5837..b876725cddc 100644 --- a/integration/test/Test/MLS/One2One.hs +++ b/integration/test/Test/MLS/One2One.hs @@ -38,7 +38,7 @@ testGetMLSOne2OneUnconnected otherDomain = do testGetMLSOne2OneSameTeam :: App () testGetMLSOne2OneSameTeam = do - (alice, _) <- createTeam OwnDomain + (alice, _, _) <- createTeam OwnDomain 1 bob <- addUserToTeam alice void $ getMLSOne2OneConversation alice bob >>= getJSON 200 diff --git a/integration/test/Test/MLS/SubConversation.hs b/integration/test/Test/MLS/SubConversation.hs index e86c01b4129..2de59d50f2f 100644 --- a/integration/test/Test/MLS/SubConversation.hs +++ b/integration/test/Test/MLS/SubConversation.hs @@ -28,7 +28,7 @@ testJoinSubConv = do testDeleteParentOfSubConv :: HasCallStack => Domain -> App () testDeleteParentOfSubConv secondDomain = do - (alice, tid) <- createTeam OwnDomain + (alice, tid, _) <- createTeam OwnDomain 1 bob <- randomUser secondDomain def connectUsers [alice, bob] diff --git a/integration/test/Test/MessageTimer.hs b/integration/test/Test/MessageTimer.hs new file mode 100644 index 00000000000..c2de56aff26 --- /dev/null +++ b/integration/test/Test/MessageTimer.hs @@ -0,0 +1,55 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.MessageTimer where + +import API.Galley +import Control.Monad.Codensity +import Control.Monad.Reader +import GHC.Stack +import Notifications +import SetupHelpers +import Testlib.Prelude +import Testlib.ResourcePool + +testMessageTimerChangeWithRemotes :: HasCallStack => App () +testMessageTimerChangeWithRemotes = do + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + conv <- postConversation alice defProteus {qualifiedUsers = [bob]} >>= getJSON 201 + withWebSockets [alice, bob] $ \wss -> do + void $ updateMessageTimer alice conv 1000 >>= getBody 200 + for_ wss $ \ws -> do + notif <- awaitMatch 10 isConvMsgTimerUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + +testMessageTimerChangeWithUnreachableRemotes :: HasCallStack => App () +testMessageTimerChangeWithUnreachableRemotes = do + resourcePool <- asks resourcePool + alice <- randomUser OwnDomain def + conv <- runCodensity (acquireResources 1 resourcePool) $ \[dynBackend] -> + runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do + bob <- randomUser dynBackend.berDomain def + connectUsers2 alice bob + postConversation alice (defProteus {qualifiedUsers = [bob]}) >>= getJSON 201 + withWebSocket alice $ \ws -> do + void $ updateMessageTimer alice conv 1000 >>= getBody 200 + notif <- awaitMatch 10 isConvMsgTimerUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice diff --git a/integration/test/Test/Roles.hs b/integration/test/Test/Roles.hs new file mode 100644 index 00000000000..bb524618259 --- /dev/null +++ b/integration/test/Test/Roles.hs @@ -0,0 +1,65 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Roles where + +import API.Galley +import Control.Monad.Reader +import GHC.Stack +import Notifications +import SetupHelpers +import Testlib.Prelude + +testRoleUpdateWithRemotesOk :: HasCallStack => App () +testRoleUpdateWithRemotesOk = do + [bob, charlie, alice] <- createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain] + conv <- + postConversation bob (defProteus {qualifiedUsers = [charlie, alice]}) + >>= getJSON 201 + adminRole <- make "wire_admin" + + withWebSockets [bob, charlie, alice] $ \wss -> do + void $ updateRole bob charlie adminRole conv >>= getBody 200 + bindResponse (getConversation bob conv) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "members.others.0.qualified_id" `shouldMatch` objQidObject charlie + resp.json %. "members.others.0.conversation_role" `shouldMatch` "wire_admin" + for_ wss $ \ws -> do + notif <- awaitMatch 10 isMemberUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject bob + +testRoleUpdateWithRemotesUnreachable :: HasCallStack => App () +testRoleUpdateWithRemotesUnreachable = do + [bob, charlie] <- createAndConnectUsers [OwnDomain, OwnDomain] + startDynamicBackends [mempty] $ \[dynBackend] -> do + alice <- randomUser dynBackend def + mapM_ (connectUsers2 alice) [bob, charlie] + conv <- + postConversation bob (defProteus {qualifiedUsers = [charlie, alice]}) + >>= getJSON 201 + adminRole <- make "wire_admin" + + withWebSockets [bob, charlie] $ \wss -> do + void $ updateRole bob charlie adminRole conv >>= getBody 200 + + for_ wss $ \ws -> do + notif <- awaitMatch 10 isMemberUpdateNotif ws + notif %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + notif %. "payload.0.qualified_from" `shouldMatch` objQidObject bob diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index 2e0ffaec544..71977405a5b 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -5,12 +5,14 @@ module Testlib.Env where import Control.Monad.Codensity import Control.Monad.IO.Class import Data.Default +import Data.Function ((&)) import Data.Functor import Data.IORef import Data.Map qualified as Map import Data.Set (Set) import Data.Set qualified as Set import Data.Yaml qualified as Yaml +import Database.CQL.IO qualified as Cassandra import Network.HTTP.Client qualified as HTTP import System.Exit import System.FilePath @@ -50,10 +52,16 @@ mkGlobalEnv cfgFile = do else Nothing manager <- HTTP.newManager HTTP.defaultManagerSettings + let cassSettings = + Cassandra.defSettings + & Cassandra.setContacts intConfig.cassandra.host [] + & Cassandra.setPortNumber (fromIntegral intConfig.cassandra.port) + cassClient <- Cassandra.init cassSettings resourcePool <- createBackendResourcePool (Map.elems intConfig.dynamicBackends) intConfig.rabbitmq + cassClient pure GlobalEnv { gServiceMap = diff --git a/integration/test/Testlib/Prelude.hs b/integration/test/Testlib/Prelude.hs index 05a04f366a3..27c9db153cd 100644 --- a/integration/test/Testlib/Prelude.hs +++ b/integration/test/Testlib/Prelude.hs @@ -66,6 +66,9 @@ module Testlib.Prelude -- * Functor (<$$>), (<$$$>), + + -- * Applicative + allPreds, ) where @@ -222,3 +225,11 @@ infix 4 <$$> (<$$$>) = fmap . fmap . fmap infix 4 <$$$> + +---------------------------------------------------------------------- +-- Applicative + +allPreds :: (Applicative f) => [a -> f Bool] -> a -> f Bool +allPreds [] _ = pure True +allPreds [p] x = p x +allPreds (p1 : ps) x = (&&) <$> p1 x <*> allPreds ps x diff --git a/integration/test/Testlib/ResourcePool.hs b/integration/test/Testlib/ResourcePool.hs index 582e36e7529..c7483ca9478 100644 --- a/integration/test/Testlib/ResourcePool.hs +++ b/integration/test/Testlib/ResourcePool.hs @@ -22,6 +22,7 @@ import Data.Set qualified as Set import Data.String import Data.Text qualified as T import Data.Tuple +import Database.CQL.IO import GHC.Stack (HasCallStack) import Network.AMQP.Extended import Network.RabbitMqAdmin @@ -46,13 +47,17 @@ acquireResources n pool = Codensity $ \f -> bracket acquire release $ \s -> do waitQSemN pool.sem n atomicModifyIORef pool.resources $ swap . Set.splitAt n -createBackendResourcePool :: [DynamicBackendConfig] -> RabbitMQConfig -> IO (ResourcePool BackendResource) -createBackendResourcePool dynConfs rabbitmq = +createBackendResourcePool :: [DynamicBackendConfig] -> RabbitMQConfig -> ClientState -> IO (ResourcePool BackendResource) +createBackendResourcePool dynConfs rabbitmq cassClient = let resources = backendResources dynConfs + cleanupBackend :: BackendResource -> IO () + cleanupBackend resource = do + deleteAllRabbitMQQueues rabbitmq resource + runClient cassClient $ deleteAllDynamicBackendConfigs resource in ResourcePool <$> newQSemN (length dynConfs) <*> newIORef resources - <*> pure (deleteAllRabbitMQQueues rabbitmq) + <*> pure cleanupBackend deleteAllRabbitMQQueues :: RabbitMQConfig -> BackendResource -> IO () deleteAllRabbitMQQueues rc resource = do @@ -68,6 +73,12 @@ deleteAllRabbitMQQueues rc resource = do for_ queues $ \queue -> deleteQueue client (T.pack resource.berVHost) queue.name +deleteAllDynamicBackendConfigs :: BackendResource -> Client () +deleteAllDynamicBackendConfigs resource = write cql (defQueryParams LocalQuorum ()) + where + cql :: PrepQuery W () () + cql = fromString $ "TRUNCATE " <> resource.berBrigKeyspace <> ".federation_remotes" + backendResources :: [DynamicBackendConfig] -> Set.Set BackendResource backendResources dynConfs = (zip dynConfs [1 ..]) diff --git a/integration/test/Testlib/RunServices.hs b/integration/test/Testlib/RunServices.hs index 5f2b5a3eb34..9a714b453e8 100644 --- a/integration/test/Testlib/RunServices.hs +++ b/integration/test/Testlib/RunServices.hs @@ -4,7 +4,6 @@ module Testlib.RunServices where import Control.Concurrent import Control.Monad.Codensity (lowerCodensity) -import SetupHelpers import System.Directory import System.Environment (getArgs) import System.Exit (exitWith) @@ -62,10 +61,6 @@ main = do lowerCodensity $ do _modifyEnv <- traverseConcurrentlyCodensity - ( \resource -> - -- We add the 'fullSerachWithAll' overrrides is a hack to get - -- around https://wearezeta.atlassian.net/browse/WPB-3796 - startDynamicBackend resource fullSearchWithAll - ) + (\r -> startDynamicBackend r mempty) [backendA, backendB] liftIO run diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index c452736a832..4e1d5e65fe0 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -112,7 +112,8 @@ data IntegrationConfig = IntegrationConfig { backendOne :: BackendConfig, backendTwo :: BackendConfig, dynamicBackends :: Map String DynamicBackendConfig, - rabbitmq :: RabbitMQConfig + rabbitmq :: RabbitMQConfig, + cassandra :: HostPort } deriving (Show, Generic) @@ -124,6 +125,7 @@ instance FromJSON IntegrationConfig where <*> o .: fromString "backendTwo" <*> o .: fromString "dynamicBackends" <*> o .: fromString "rabbitmq" + <*> o .: fromString "cassandra" data ServiceMap = ServiceMap { brig :: HostPort, diff --git a/libs/brig-types/brig-types.cabal b/libs/brig-types/brig-types.cabal index fb0afa4af77..faac2030515 100644 --- a/libs/brig-types/brig-types.cabal +++ b/libs/brig-types/brig-types.cabal @@ -156,8 +156,8 @@ test-suite brig-types-tests , brig-types , bytestring-conversion >=0.3.1 , imports + , openapi3 , QuickCheck >=2.9 - , swagger2 >=2.5 , tasty , tasty-hunit , tasty-quickcheck diff --git a/libs/brig-types/default.nix b/libs/brig-types/default.nix index 49028b0de48..173b83591b0 100644 --- a/libs/brig-types/default.nix +++ b/libs/brig-types/default.nix @@ -13,8 +13,8 @@ , gitignoreSource , imports , lib +, openapi3 , QuickCheck -, swagger2 , tasty , tasty-hunit , tasty-quickcheck @@ -47,8 +47,8 @@ mkDerivation { base bytestring-conversion imports + openapi3 QuickCheck - swagger2 tasty tasty-hunit tasty-quickcheck diff --git a/libs/brig-types/test/unit/Test/Brig/Roundtrip.hs b/libs/brig-types/test/unit/Test/Brig/Roundtrip.hs index 9ea421c6c2f..13cfc3570e6 100644 --- a/libs/brig-types/test/unit/Test/Brig/Roundtrip.hs +++ b/libs/brig-types/test/unit/Test/Brig/Roundtrip.hs @@ -20,7 +20,7 @@ module Test.Brig.Roundtrip where import Data.Aeson (FromJSON, ToJSON, parseJSON, toJSON) import Data.Aeson.Types (parseEither) import Data.ByteString.Conversion -import Data.Swagger (ToSchema, validatePrettyToJSON) +import Data.OpenApi (ToSchema, validatePrettyToJSON) import Imports import Test.Tasty (TestTree) import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty, (.&&.), (===)) @@ -40,7 +40,7 @@ testRoundTrip = testProperty msg trip testRoundTripWithSwagger :: forall a. - (Arbitrary a, Typeable a, ToJSON a, FromJSON a, ToSchema a, Eq a, Show a) => + (Arbitrary a, ToJSON a, FromJSON a, ToSchema a, Eq a, Show a) => TestTree testRoundTripWithSwagger = testProperty msg (trip .&&. scm) where diff --git a/libs/deriving-swagger2/default.nix b/libs/deriving-swagger2/default.nix index fdf39de254a..5359dbec579 100644 --- a/libs/deriving-swagger2/default.nix +++ b/libs/deriving-swagger2/default.nix @@ -8,12 +8,12 @@ , gitignoreSource , imports , lib -, swagger2 +, openapi3 }: mkDerivation { pname = "deriving-swagger2"; version = "0.1.0"; src = gitignoreSource ./.; - libraryHaskellDepends = [ base extra imports swagger2 ]; + libraryHaskellDepends = [ base extra imports openapi3 ]; license = lib.licenses.agpl3Only; } diff --git a/libs/deriving-swagger2/deriving-swagger2.cabal b/libs/deriving-swagger2/deriving-swagger2.cabal index 4d68184d8c4..6e5b3f9de4a 100644 --- a/libs/deriving-swagger2/deriving-swagger2.cabal +++ b/libs/deriving-swagger2/deriving-swagger2.cabal @@ -62,9 +62,9 @@ library -Wredundant-constraints -Wunused-packages build-depends: - base >=4 && <5 + base >=4 && <5 , extra , imports - , swagger2 >=0.6 + , openapi3 default-language: GHC2021 diff --git a/libs/deriving-swagger2/src/Deriving/Swagger.hs b/libs/deriving-swagger2/src/Deriving/Swagger.hs index 3f0fc3b56f9..95a0c121a3e 100644 --- a/libs/deriving-swagger2/src/Deriving/Swagger.hs +++ b/libs/deriving-swagger2/src/Deriving/Swagger.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE RankNTypes #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -22,10 +24,10 @@ module Deriving.Swagger where import Data.Char qualified as Char import Data.Kind (Constraint) import Data.List.Extra (stripSuffix) +import Data.OpenApi.Internal.Schema (GToSchema) +import Data.OpenApi.Internal.TypeShape +import Data.OpenApi.Schema import Data.Proxy (Proxy (..)) -import Data.Swagger (SchemaOptions, ToSchema (..), constructorTagModifier, defaultSchemaOptions, fieldLabelModifier, genericDeclareNamedSchema) -import Data.Swagger.Internal.Schema (GToSchema) -import Data.Swagger.Internal.TypeShape (TypeHasSimpleShape) import GHC.Generics (Generic (Rep)) import GHC.TypeLits (ErrorMessage (Text), KnownSymbol, Symbol, TypeError, symbolVal) import Imports @@ -81,6 +83,7 @@ import Imports -- | A newtype wrapper which gives ToSchema instances with modified options. -- 't' has to have an instance of the 'SwaggerOptions' class. newtype CustomSwagger t a = CustomSwagger {unCustomSwagger :: a} + deriving (Generic, Typeable) class SwaggerOptions xs where swaggerOptions :: SchemaOptions @@ -94,14 +97,7 @@ instance (StringModifier f, SwaggerOptions xs) => SwaggerOptions (FieldLabelModi instance (StringModifier f, SwaggerOptions xs) => SwaggerOptions (ConstructorTagModifier f ': xs) where swaggerOptions = (swaggerOptions @xs) {constructorTagModifier = getStringModifier @f} -instance - ( SwaggerOptions t, - Generic a, - GToSchema (Rep a), - TypeHasSimpleShape a "genericDeclareNamedSchemaUnrestricted" - ) => - ToSchema (CustomSwagger t a) - where +instance (SwaggerOptions t, Generic a, Typeable a, GToSchema (Rep a), Typeable (CustomSwagger t a), TypeHasSimpleShape a "genericDeclareNamedSchemaUnrestricted") => ToSchema (CustomSwagger t a) where declareNamedSchema _ = genericDeclareNamedSchema (swaggerOptions @t) (Proxy @a) -- ** Specify __what__ to modify diff --git a/libs/extended/default.nix b/libs/extended/default.nix index d2fd00ab9cb..b44a955a35f 100644 --- a/libs/extended/default.nix +++ b/libs/extended/default.nix @@ -27,8 +27,8 @@ , servant , servant-client , servant-client-core +, servant-openapi3 , servant-server -, servant-swagger , temporary , text , tinylog @@ -60,8 +60,8 @@ mkDerivation { servant servant-client servant-client-core + servant-openapi3 servant-server - servant-swagger text tinylog unliftio diff --git a/libs/extended/extended.cabal b/libs/extended/extended.cabal index 2271f8d1312..389b59b9447 100644 --- a/libs/extended/extended.cabal +++ b/libs/extended/extended.cabal @@ -98,8 +98,8 @@ library , servant , servant-client , servant-client-core + , servant-openapi3 , servant-server - , servant-swagger , text , tinylog , unliftio diff --git a/libs/extended/src/Servant/API/Extended.hs b/libs/extended/src/Servant/API/Extended.hs index 322b029f1b4..c1e87f38beb 100644 --- a/libs/extended/src/Servant/API/Extended.hs +++ b/libs/extended/src/Servant/API/Extended.hs @@ -31,8 +31,8 @@ import Network.Wai import Servant.API import Servant.API.ContentTypes import Servant.API.Modifiers +import Servant.OpenApi import Servant.Server.Internal -import Servant.Swagger import Prelude () -- | Like 'ReqBody'', but takes parsers that throw 'ServerError', not 'String'. @tag@ is used @@ -108,10 +108,10 @@ instance Right v -> pure v instance - HasSwagger (ReqBody' '[Required, Strict] cts a :> api) => - HasSwagger (ReqBodyCustomError cts tag a :> api) + HasOpenApi (ReqBody' '[Required, Strict] cts a :> api) => + HasOpenApi (ReqBodyCustomError cts tag a :> api) where - toSwagger Proxy = toSwagger (Proxy @(ReqBody' '[Required, Strict] cts a :> api)) + toOpenApi Proxy = toOpenApi (Proxy @(ReqBody' '[Required, Strict] cts a :> api)) instance RoutesToPaths rest => RoutesToPaths (ReqBodyCustomError' mods list tag a :> rest) where getRoutes = getRoutes @rest diff --git a/libs/extended/src/Servant/API/Extended/RawM.hs b/libs/extended/src/Servant/API/Extended/RawM.hs index 9f1e1a6395f..f5108d12329 100644 --- a/libs/extended/src/Servant/API/Extended/RawM.hs +++ b/libs/extended/src/Servant/API/Extended/RawM.hs @@ -10,11 +10,11 @@ import Data.Proxy import Imports import Network.Wai import Servant.API (Raw) +import Servant.OpenApi import Servant.Server hiding (respond) import Servant.Server.Internal.Delayed import Servant.Server.Internal.RouteResult import Servant.Server.Internal.Router -import Servant.Swagger type ApplicationM m = Request -> (Response -> IO ResponseReceived) -> m ResponseReceived @@ -51,8 +51,8 @@ instance HasServer RawM context where hoistServerWithContext _ _ f srvM req respond = f (srvM req respond) -instance HasSwagger RawM where - toSwagger _ = toSwagger (Proxy @Raw) +instance HasOpenApi RawM where + toOpenApi _ = toOpenApi (Proxy @Raw) instance RoutesToPaths RawM where getRoutes = [] diff --git a/libs/schema-profunctor/default.nix b/libs/schema-profunctor/default.nix index a498d97378b..bede1bdeae6 100644 --- a/libs/schema-profunctor/default.nix +++ b/libs/schema-profunctor/default.nix @@ -14,8 +14,8 @@ , insert-ordered-containers , lens , lib +, openapi3 , profunctors -, swagger2 , tasty , tasty-hunit , text @@ -34,8 +34,8 @@ mkDerivation { containers imports lens + openapi3 profunctors - swagger2 text transformers vector @@ -47,7 +47,7 @@ mkDerivation { imports insert-ordered-containers lens - swagger2 + openapi3 tasty tasty-hunit text diff --git a/libs/schema-profunctor/schema-profunctor.cabal b/libs/schema-profunctor/schema-profunctor.cabal index c9c534c0165..236a68a841b 100644 --- a/libs/schema-profunctor/schema-profunctor.cabal +++ b/libs/schema-profunctor/schema-profunctor.cabal @@ -69,8 +69,8 @@ library , containers , imports , lens + , openapi3 , profunctors - , swagger2 >=2 && <2.9 , text , transformers , vector @@ -139,8 +139,8 @@ test-suite schemas-tests , imports , insert-ordered-containers , lens + , openapi3 , schema-profunctor - , swagger2 , tasty , tasty-hunit , text diff --git a/libs/schema-profunctor/src/Data/Schema.hs b/libs/schema-profunctor/src/Data/Schema.hs index 548ed8bfbe0..9ae1187481f 100644 --- a/libs/schema-profunctor/src/Data/Schema.hs +++ b/libs/schema-profunctor/src/Data/Schema.hs @@ -62,7 +62,6 @@ module Data.Schema fieldOverF, fieldWithDocModifierF, array, - pair, set, nonEmptyArray, map_, @@ -101,12 +100,11 @@ import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty qualified as NonEmpty import Data.Map qualified as Map import Data.Monoid hiding (Product) +import Data.OpenApi qualified as S +import Data.OpenApi.Declare qualified as S import Data.Profunctor (Star (..)) import Data.Proxy (Proxy (..)) import Data.Set qualified as Set -import Data.Swagger qualified as S -import Data.Swagger.Declare qualified as S -import Data.Swagger.Internal qualified as S import Data.Text qualified as T import Data.Text.Lazy qualified as TL import Data.Vector qualified as V @@ -464,24 +462,6 @@ array sch = SchemaP (SchemaDoc s) (SchemaIn r) (SchemaOut w) s = mkArray (schemaDoc sch) w x = A.Array . V.fromList <$> mapM (schemaOut sch) x --- | A schema for a JSON pair. --- This is serialised as JSON array of exactly 2 elements --- of the same type. Any more or less is an error. -pair :: - (HasArray ndoc doc, HasName ndoc) => - ValueSchema ndoc a -> - ValueSchema doc (a, a) -pair sch = SchemaP (SchemaDoc s) (SchemaIn r) (SchemaOut w) - where - name = maybe "pair" ("pair of " <>) (getName (schemaDoc sch)) - r = A.withArray (T.unpack name) $ \arr -> do - l <- mapM (schemaIn sch) $ V.toList arr - case l of - [a, b] -> pure (a, b) - _ -> fail $ "Expected exactly 2 elements, but got " <> show (length l) - s = mkArray (schemaDoc sch) - w (a, b) = A.Array . V.fromList <$> mapM (schemaOut sch) [a, b] - set :: (HasArray ndoc doc, HasName ndoc, Ord a) => ValueSchema ndoc a -> @@ -643,7 +623,7 @@ text name = (A.withText (T.unpack name) pure) (pure . A.String) where - d = mempty & S.type_ ?~ S.SwaggerString + d = mempty & S.type_ ?~ S.OpenApiString -- | A schema for a textual value with possible failure. parsedText :: @@ -783,7 +763,7 @@ instance HasSchemaRef doc => HasField doc SwaggerDoc where where f ref = mempty - & S.type_ ?~ S.SwaggerObject + & S.type_ ?~ S.OpenApiObject & S.properties . at name ?~ ref & S.required .~ [name] @@ -799,8 +779,8 @@ instance HasSchemaRef ndoc => HasArray ndoc SwaggerDoc where f :: S.Referenced S.Schema -> S.Schema f ref = mempty - & S.type_ ?~ S.SwaggerArray - & S.items ?~ S.SwaggerItemsObject ref + & S.type_ ?~ S.OpenApiArray + & S.items ?~ S.OpenApiItemsObject ref instance HasSchemaRef ndoc => HasMap ndoc SwaggerDoc where mkMap = fmap f . schemaRef @@ -808,7 +788,7 @@ instance HasSchemaRef ndoc => HasMap ndoc SwaggerDoc where f :: S.Referenced S.Schema -> S.Schema f ref = mempty - & S.type_ ?~ S.SwaggerObject + & S.type_ ?~ S.OpenApiObject & S.additionalProperties ?~ S.AdditionalPropertiesSchema ref class HasMinItems s a where @@ -818,19 +798,19 @@ instance HasMinItems SwaggerDoc (Maybe Integer) where minItems = declared . S.minItems instance HasEnum Text NamedSwaggerDoc where - mkEnum = mkSwaggerEnum S.SwaggerString + mkEnum = mkSwaggerEnum S.OpenApiString instance HasEnum Integer NamedSwaggerDoc where - mkEnum = mkSwaggerEnum S.SwaggerInteger + mkEnum = mkSwaggerEnum S.OpenApiInteger instance HasEnum Natural NamedSwaggerDoc where - mkEnum = mkSwaggerEnum S.SwaggerInteger + mkEnum = mkSwaggerEnum S.OpenApiInteger instance HasEnum Bool NamedSwaggerDoc where - mkEnum = mkSwaggerEnum S.SwaggerBoolean + mkEnum = mkSwaggerEnum S.OpenApiBoolean mkSwaggerEnum :: - S.SwaggerType 'S.SwaggerKindSchema -> + S.OpenApiType -> Text -> [A.Value] -> NamedSwaggerDoc @@ -858,11 +838,12 @@ class ToSchema a where -- Newtype wrappers for deriving via newtype Schema a = Schema {getSchema :: a} + deriving (Generic) schemaToSwagger :: forall a. ToSchema a => Proxy a -> Declare S.NamedSchema schemaToSwagger _ = runDeclare (schemaDoc (schema @a)) -instance ToSchema a => S.ToSchema (Schema a) where +instance (Typeable a, ToSchema a) => S.ToSchema (Schema a) where declareNamedSchema _ = schemaToSwagger (Proxy @a) -- | JSON serialiser for an instance of 'ToSchema'. @@ -939,8 +920,14 @@ instance S.HasSchema d S.Schema => S.HasSchema (SchemaP d v w a b) S.Schema wher instance S.HasDescription NamedSwaggerDoc (Maybe Text) where description = declared . S.schema . S.description +instance S.HasDeprecated NamedSwaggerDoc (Maybe Bool) where + deprecated = declared . S.schema . S.deprecated + instance {-# OVERLAPPABLE #-} S.HasDescription s a => S.HasDescription (WithDeclare s) a where description = declared . S.description +instance {-# OVERLAPPABLE #-} S.HasDeprecated s a => S.HasDeprecated (WithDeclare s) a where + deprecated = declared . S.deprecated + instance {-# OVERLAPPABLE #-} S.HasExample s a => S.HasExample (WithDeclare s) a where example = declared . S.example diff --git a/libs/schema-profunctor/test/unit/Test/Data/Schema.hs b/libs/schema-profunctor/test/unit/Test/Data/Schema.hs index 5ee7af68a77..d29b69b2365 100644 --- a/libs/schema-profunctor/test/unit/Test/Data/Schema.hs +++ b/libs/schema-profunctor/test/unit/Test/Data/Schema.hs @@ -27,10 +27,10 @@ import Data.Aeson.QQ import Data.Aeson.Types qualified as A import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap import Data.List.NonEmpty (NonEmpty ((:|))) +import Data.OpenApi qualified as S +import Data.OpenApi.Declare qualified as S import Data.Proxy import Data.Schema hiding (getName) -import Data.Swagger qualified as S -import Data.Swagger.Declare qualified as S import Data.Text qualified as Text import Imports import Test.Tasty @@ -290,7 +290,7 @@ testNonEmptySchema = Nothing -> assertFailure "expected schema to have a property called 'nl'" Just (S.Ref _) -> assertFailure "expected property 'nl' to have inline schema" Just (S.Inline nlSch) -> do - assertEqual "type should be Array" (Just S.SwaggerArray) (nlSch ^. S.type_) + assertEqual "type should be Array" (Just S.OpenApiArray) (nlSch ^. S.type_) assertEqual "minItems should be 1" (Just 1) (nlSch ^. S.minItems) testRefField :: TestTree @@ -332,7 +332,7 @@ testEnumType = assertEqual "Text enum has Swagger type \"string\"" (s1 ^. S.type_) - (Just S.SwaggerString) + (Just S.OpenApiString) let e2 :: ValueSchema NamedSwaggerDoc Integer e2 = enum @Integer "IntEnum" (element (3 :: Integer) (3 :: Integer)) @@ -340,7 +340,7 @@ testEnumType = assertEqual "Integer enum has Swagger type \"integer\"" (s2 ^. S.type_) - (Just S.SwaggerInteger) + (Just S.OpenApiInteger) testNullable :: TestTree testNullable = diff --git a/libs/types-common/default.nix b/libs/types-common/default.nix index 8b4da990200..a5c57f5f05d 100644 --- a/libs/types-common/default.nix +++ b/libs/types-common/default.nix @@ -32,6 +32,7 @@ , lens-datetime , lib , mime +, openapi3 , optparse-applicative , pem , protobuf @@ -40,7 +41,6 @@ , random , schema-profunctor , servant-server -, swagger2 , tagged , tasty , tasty-hunit @@ -86,6 +86,7 @@ mkDerivation { lens lens-datetime mime + openapi3 optparse-applicative pem protobuf @@ -94,7 +95,6 @@ mkDerivation { random schema-profunctor servant-server - swagger2 tagged tasty tasty-hunit diff --git a/libs/types-common/src/Data/Code.hs b/libs/types-common/src/Data/Code.hs index 1820d85f403..ba176629701 100644 --- a/libs/types-common/src/Data/Code.hs +++ b/libs/types-common/src/Data/Code.hs @@ -31,11 +31,11 @@ import Data.Aeson.TH import Data.Bifunctor (Bifunctor (first)) import Data.ByteString.Conversion import Data.Json.Util +import Data.OpenApi qualified as S +import Data.OpenApi.ParamSchema import Data.Proxy (Proxy (Proxy)) import Data.Range import Data.Schema -import Data.Swagger qualified as S -import Data.Swagger.ParamSchema import Data.Text (pack) import Data.Text.Ascii import Data.Text.Encoding (encodeUtf8) diff --git a/libs/types-common/src/Data/CommaSeparatedList.hs b/libs/types-common/src/Data/CommaSeparatedList.hs index 8e3ebd0edd8..8c13c49f4cf 100644 --- a/libs/types-common/src/Data/CommaSeparatedList.hs +++ b/libs/types-common/src/Data/CommaSeparatedList.hs @@ -22,9 +22,9 @@ module Data.CommaSeparatedList where import Control.Lens ((?~)) import Data.Bifunctor qualified as Bifunctor import Data.ByteString.Conversion (FromByteString, List, fromList, parser, runParser) +import Data.OpenApi import Data.Proxy (Proxy (..)) import Data.Range (Bounds, Range) -import Data.Swagger (CollectionFormat (CollectionCSV), SwaggerItems (SwaggerItemsPrimitive), SwaggerType (SwaggerString), ToParamSchema (..), items, type_) import Data.Text qualified as Text import Data.Text.Encoding (encodeUtf8) import Imports @@ -40,10 +40,10 @@ instance FromByteString (List a) => FromHttpApiData (CommaSeparatedList a) where CommaSeparatedList . fromList <$> Bifunctor.first Text.pack (runParser parser $ encodeUtf8 t) instance ToParamSchema (CommaSeparatedList a) where - toParamSchema _ = mempty & type_ ?~ SwaggerString + toParamSchema _ = mempty & type_ ?~ OpenApiString -- | TODO: is this obsoleted by the instances in "Data.Range"? instance (ToParamSchema a, ToParamSchema (Range n m [a])) => ToParamSchema (Range n m (CommaSeparatedList a)) where toParamSchema _ = toParamSchema (Proxy @(Range n m [a])) - & items ?~ SwaggerItemsPrimitive (Just CollectionCSV) (toParamSchema (Proxy @a)) + & items ?~ OpenApiItemsArray [Inline $ toParamSchema (Proxy @a)] diff --git a/libs/types-common/src/Data/Domain.hs b/libs/types-common/src/Data/Domain.hs index 8f96dc18bcb..6f9d0884405 100644 --- a/libs/types-common/src/Data/Domain.hs +++ b/libs/types-common/src/Data/Domain.hs @@ -31,8 +31,8 @@ import Data.ByteString qualified as BS import Data.ByteString.Builder qualified as Builder import Data.ByteString.Char8 qualified as BS.Char8 import Data.ByteString.Conversion +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Encoding qualified as Text.E import Imports hiding (isAlphaNum) diff --git a/libs/types-common/src/Data/Handle.hs b/libs/types-common/src/Data/Handle.hs index 0d1e5220076..29d1570cc32 100644 --- a/libs/types-common/src/Data/Handle.hs +++ b/libs/types-common/src/Data/Handle.hs @@ -31,8 +31,8 @@ import Data.Bifunctor (Bifunctor (first)) import Data.ByteString qualified as BS import Data.ByteString.Conversion (FromByteString (parser), ToByteString) import Data.Hashable (Hashable) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Encoding qualified as Text.E import Imports diff --git a/libs/types-common/src/Data/Id.hs b/libs/types-common/src/Data/Id.hs index cae7c686ce7..528725f888b 100644 --- a/libs/types-common/src/Data/Id.hs +++ b/libs/types-common/src/Data/Id.hs @@ -71,11 +71,11 @@ import Data.ByteString.Lazy qualified as L import Data.Char qualified as Char import Data.Default (Default (..)) import Data.Hashable (Hashable) +import Data.OpenApi qualified as S +import Data.OpenApi.Internal.ParamSchema (ToParamSchema (..)) import Data.ProtocolBuffers.Internal import Data.Proxy import Data.Schema -import Data.Swagger qualified as S -import Data.Swagger.Internal.ParamSchema (ToParamSchema (..)) import Data.Text qualified as T import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Text.Lazy (toStrict) diff --git a/libs/types-common/src/Data/Json/Util.hs b/libs/types-common/src/Data/Json/Util.hs index 62e7168d728..408dfe41cbc 100644 --- a/libs/types-common/src/Data/Json/Util.hs +++ b/libs/types-common/src/Data/Json/Util.hs @@ -62,8 +62,8 @@ import Data.ByteString.Builder qualified as BB import Data.ByteString.Conversion qualified as BS import Data.ByteString.Lazy qualified as L import Data.Fixed +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import Data.Text.Encoding.Error qualified as Text @@ -161,7 +161,7 @@ instance ToJSONObject A.Object where instance S.ToParamSchema A.Object where toParamSchema _ = - mempty & S.type_ ?~ S.SwaggerString + mempty & S.type_ ?~ S.OpenApiString instance ToSchema A.Object where schema = @@ -209,7 +209,7 @@ instance ToHttpApiData Base64ByteString where toUrlPiece = Text.decodeUtf8With Text.lenientDecode . B64U.encodeUnpadded . fromBase64ByteString instance S.ToParamSchema Base64ByteString where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString -- base64("example") ~> "ZXhhbXBsZQo=" base64SchemaN :: ValueSchema NamedSwaggerDoc ByteString @@ -245,7 +245,7 @@ instance ToHttpApiData Base64ByteStringL where toUrlPiece = toUrlPiece . base64ToStrict instance S.ToParamSchema Base64ByteStringL where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString base64SchemaLN :: ValueSchema NamedSwaggerDoc LByteString base64SchemaLN = L.toStrict .= fmap L.fromStrict base64SchemaN diff --git a/libs/types-common/src/Data/LegalHold.hs b/libs/types-common/src/Data/LegalHold.hs index 7b328820e6c..02955c03f3d 100644 --- a/libs/types-common/src/Data/LegalHold.hs +++ b/libs/types-common/src/Data/LegalHold.hs @@ -20,8 +20,8 @@ module Data.LegalHold where import Cassandra.CQL import Control.Lens ((?~)) import Data.Aeson hiding (constructorTagModifier) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Test.QuickCheck diff --git a/libs/types-common/src/Data/List1.hs b/libs/types-common/src/Data/List1.hs index f77d578fed9..8a1d31555d2 100644 --- a/libs/types-common/src/Data/List1.hs +++ b/libs/types-common/src/Data/List1.hs @@ -25,8 +25,8 @@ import Cassandra import Data.Aeson import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty qualified as N +import Data.OpenApi qualified as Swagger import Data.Schema as S -import Data.Swagger qualified as Swagger import Imports import Test.QuickCheck (Arbitrary) import Test.QuickCheck.Instances () @@ -72,8 +72,8 @@ instance ToSchema a => ToSchema (List1 a) where instance Swagger.ToParamSchema (List1 a) where toParamSchema _ = mempty - { Swagger._paramSchemaType = Just Swagger.SwaggerArray, - Swagger._paramSchemaMinLength = Just 1 + { Swagger._schemaType = Just Swagger.OpenApiArray, + Swagger._schemaMinLength = Just 1 } instance (Cql a) => Cql (List1 a) where diff --git a/libs/types-common/src/Data/Misc.hs b/libs/types-common/src/Data/Misc.hs index 1b81d37aa31..8acd18dee2b 100644 --- a/libs/types-common/src/Data/Misc.hs +++ b/libs/types-common/src/Data/Misc.hs @@ -77,9 +77,9 @@ import Data.ByteString.Char8 (unpack) import Data.ByteString.Conversion import Data.ByteString.Lazy (toStrict) import Data.IP (IP (IPv4, IPv6), toIPv4, toIPv6b) +import Data.OpenApi qualified as S import Data.Range import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Encoding (decodeUtf8, encodeUtf8) import GHC.TypeLits (Nat) @@ -100,7 +100,7 @@ newtype IpAddr = IpAddr {ipAddr :: IP} deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema IpAddr) instance S.ToParamSchema IpAddr where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString instance FromHttpApiData IpAddr where parseQueryParam p = first Text.pack (runParser parser (encodeUtf8 p)) @@ -296,7 +296,7 @@ data Rsa newtype Fingerprint a = Fingerprint { fingerprintBytes :: ByteString } - deriving stock (Eq, Show, Generic) + deriving stock (Eq, Show, Generic, Typeable) deriving newtype (FromByteString, ToByteString, NFData) deriving via @@ -314,7 +314,7 @@ deriving via deriving via (Schema (Fingerprint a)) instance - (ToSchema (Fingerprint a)) => + (Typeable (Fingerprint a), ToSchema (Fingerprint a)) => S.ToSchema (Fingerprint a) instance ToSchema (Fingerprint Rsa) where @@ -378,7 +378,7 @@ deriving via (Schema (PlainTextPassword' tag)) instance ToSchema (PlainTextPassw deriving via (Schema (PlainTextPassword' tag)) instance ToSchema (PlainTextPassword' tag) => ToJSON (PlainTextPassword' tag) -deriving via (Schema (PlainTextPassword' tag)) instance ToSchema (PlainTextPassword' tag) => S.ToSchema (PlainTextPassword' tag) +deriving via (Schema (PlainTextPassword' tag)) instance (KnownNat tag, ToSchema (PlainTextPassword' tag)) => S.ToSchema (PlainTextPassword' tag) instance Show (PlainTextPassword' minLen) where show _ = "PlainTextPassword' " diff --git a/libs/types-common/src/Data/Nonce.hs b/libs/types-common/src/Data/Nonce.hs index 91befc4c3e8..1f094bab764 100644 --- a/libs/types-common/src/Data/Nonce.hs +++ b/libs/types-common/src/Data/Nonce.hs @@ -31,10 +31,10 @@ import Data.Aeson qualified as A import Data.ByteString.Base64.URL qualified as Base64 import Data.ByteString.Conversion import Data.ByteString.Lazy (fromStrict, toStrict) +import Data.OpenApi qualified as S +import Data.OpenApi.ParamSchema import Data.Proxy (Proxy (Proxy)) import Data.Schema -import Data.Swagger qualified as S -import Data.Swagger.ParamSchema import Data.UUID as UUID (UUID, fromByteString, toByteString) import Data.UUID.V4 (nextRandom) import Imports diff --git a/libs/types-common/src/Data/Qualified.hs b/libs/types-common/src/Data/Qualified.hs index 964f91e1ef5..1c1ba088e10 100644 --- a/libs/types-common/src/Data/Qualified.hs +++ b/libs/types-common/src/Data/Qualified.hs @@ -48,15 +48,16 @@ module Data.Qualified ) where -import Control.Lens (Lens, lens, (?~)) +import Control.Lens (Lens, lens, over, (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Bifunctor (first) import Data.Domain (Domain) import Data.Handle (Handle (..)) import Data.Id import Data.Map qualified as Map +import Data.OpenApi (deprecated) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports hiding (local) import Test.QuickCheck (Arbitrary (arbitrary)) @@ -163,8 +164,11 @@ isLocal loc = foldQualified loc (const True) (const False) ---------------------------------------------------------------------- -deprecatedSchema :: S.HasDescription doc (Maybe Text) => Text -> ValueSchema doc a -> ValueSchema doc a -deprecatedSchema new = doc . description ?~ ("Deprecated, use " <> new) +deprecatedSchema :: (S.HasDeprecated doc (Maybe Bool), S.HasDescription doc (Maybe Text)) => Text -> ValueSchema doc a -> ValueSchema doc a +deprecatedSchema new = + over doc $ + (description ?~ ("Deprecated, use " <> new)) + . (deprecated ?~ True) qualifiedSchema :: HasSchemaRef doc => @@ -198,7 +202,7 @@ instance KnownIdTag t => ToJSON (Qualified (Id t)) where instance KnownIdTag t => FromJSON (Qualified (Id t)) where parseJSON = schemaParseJSON -instance KnownIdTag t => S.ToSchema (Qualified (Id t)) where +instance (Typeable t, KnownIdTag t) => S.ToSchema (Qualified (Id t)) where declareNamedSchema = schemaToSwagger instance ToJSON (Qualified Handle) where diff --git a/libs/types-common/src/Data/Range.hs b/libs/types-common/src/Data/Range.hs index e4c5be14781..898df2142c1 100644 --- a/libs/types-common/src/Data/Range.hs +++ b/libs/types-common/src/Data/Range.hs @@ -74,13 +74,13 @@ import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty qualified as N import Data.List1 (List1, toNonEmpty) import Data.Map qualified as Map +import Data.OpenApi (Schema, ToParamSchema (..)) +import Data.OpenApi qualified as S import Data.Proxy -import Data.Schema +import Data.Schema hiding (Schema) import Data.Sequence (Seq) import Data.Sequence qualified as Seq import Data.Set qualified as Set -import Data.Swagger (ParamSchema, ToParamSchema (..)) -import Data.Swagger qualified as S import Data.Text qualified as T import Data.Text.Ascii (AsciiChar, AsciiChars, AsciiText, fromAsciiChars) import Data.Text.Ascii qualified as Ascii @@ -152,6 +152,9 @@ numRangedSchemaDocModifier n m = S.schema %~ ((S.minimum_ ?~ fromIntegral n) . ( instance S.HasSchema d S.Schema => HasRangedSchemaDocModifier d [a] where rangedSchemaDocModifier _ = listRangedSchemaDocModifier +-- Sets are similar to lists, so use that as our defininition +instance S.HasSchema d S.Schema => HasRangedSchemaDocModifier d (Set a) where rangedSchemaDocModifier _ = listRangedSchemaDocModifier + instance S.HasSchema d S.Schema => HasRangedSchemaDocModifier d Text where rangedSchemaDocModifier _ = stringRangedSchemaDocModifier instance S.HasSchema d S.Schema => HasRangedSchemaDocModifier d String where rangedSchemaDocModifier _ = stringRangedSchemaDocModifier @@ -232,7 +235,7 @@ instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m TL.Text) where & S.maxLength ?~ fromKnownNat (Proxy @n) & S.minLength ?~ fromKnownNat (Proxy @m) -instance S.ToSchema a => S.ToSchema (Range n m a) where +instance (KnownNat n, S.ToSchema a, KnownNat m) => S.ToSchema (Range n m a) where declareNamedSchema _ = S.declareNamedSchema (Proxy @a) @@ -316,7 +319,7 @@ rappend (Range a) (Range b) = Range (a <> b) rsingleton :: a -> Range 1 1 [a] rsingleton = Range . pure -rangedNumToParamSchema :: forall a n m t. (ToParamSchema a, Num a, KnownNat n, KnownNat m) => Proxy (Range n m a) -> ParamSchema t +rangedNumToParamSchema :: forall a n m. (ToParamSchema a, Num a, KnownNat n, KnownNat m) => Proxy (Range n m a) -> Schema rangedNumToParamSchema _ = toParamSchema (Proxy @a) & S.minimum_ ?~ fromKnownNat (Proxy @n) diff --git a/libs/types-common/src/Data/Text/Ascii.hs b/libs/types-common/src/Data/Text/Ascii.hs index 70712eabdba..0fac4b07e2f 100644 --- a/libs/types-common/src/Data/Text/Ascii.hs +++ b/libs/types-common/src/Data/Text/Ascii.hs @@ -86,8 +86,8 @@ import Data.ByteString.Base64.URL qualified as B64Url import Data.ByteString.Char8 qualified as C8 import Data.ByteString.Conversion import Data.Hashable (Hashable) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Encoding (decodeLatin1, decodeUtf8') import Imports @@ -156,7 +156,7 @@ instance AsciiChars c => ToJSON (AsciiText c) where instance AsciiChars c => FromJSON (AsciiText c) where parseJSON = schemaParseJSON -instance AsciiChars c => S.ToSchema (AsciiText c) where +instance (Typeable c, AsciiChars c) => S.ToSchema (AsciiText c) where declareNamedSchema = schemaToSwagger instance AsciiChars c => Cql (AsciiText c) where diff --git a/libs/types-common/types-common.cabal b/libs/types-common/types-common.cabal index 9f2eb9391c7..4ce602225f1 100644 --- a/libs/types-common/types-common.cabal +++ b/libs/types-common/types-common.cabal @@ -116,6 +116,7 @@ library , lens >=4.10 , lens-datetime >=0.3 , mime >=0.4.0.2 + , openapi3 , optparse-applicative >=0.10 , pem , protobuf >=0.2 @@ -124,7 +125,6 @@ library , random >=1.1 , schema-profunctor , servant-server - , swagger2 , tagged >=0.8 , tasty >=0.11 , tasty-hunit diff --git a/libs/wai-utilities/default.nix b/libs/wai-utilities/default.nix index 33988b17bfd..bc345ab3586 100644 --- a/libs/wai-utilities/default.nix +++ b/libs/wai-utilities/default.nix @@ -18,12 +18,12 @@ , lib , metrics-core , metrics-wai +, openapi3 , pipes , prometheus-client , schema-profunctor , servant-server , streaming-commons -, swagger2 , text , tinylog , types-common @@ -52,12 +52,12 @@ mkDerivation { kan-extensions metrics-core metrics-wai + openapi3 pipes prometheus-client schema-profunctor servant-server streaming-commons - swagger2 text tinylog types-common diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Headers.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Headers.hs index 2cf2b2e644e..f1673e7de13 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Headers.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Headers.hs @@ -18,7 +18,7 @@ module Network.Wai.Utilities.Headers where import Data.ByteString.Conversion (FromByteString (..), ToByteString (..), fromByteString', toByteString') -import Data.Swagger.ParamSchema (ToParamSchema (..)) +import Data.OpenApi.ParamSchema (ToParamSchema (..)) import Data.Text as T import Imports import Servant (FromHttpApiData (..), Proxy (Proxy), ToHttpApiData (..)) diff --git a/libs/wai-utilities/wai-utilities.cabal b/libs/wai-utilities/wai-utilities.cabal index 44a3769dbf1..1c1ae75cbcc 100644 --- a/libs/wai-utilities/wai-utilities.cabal +++ b/libs/wai-utilities/wai-utilities.cabal @@ -86,12 +86,12 @@ library , kan-extensions , metrics-core >=0.1 , metrics-wai >=0.5.7 + , openapi3 , pipes >=4.1 , prometheus-client , schema-profunctor , servant-server , streaming-commons >=0.1 - , swagger2 , text >=0.11 , tinylog >=0.8 , types-common >=0.12 diff --git a/libs/wire-api-federation/default.nix b/libs/wire-api-federation/default.nix index 9e3b9f2162d..67434424c15 100644 --- a/libs/wire-api-federation/default.nix +++ b/libs/wire-api-federation/default.nix @@ -26,6 +26,7 @@ , lib , metrics-wai , mtl +, openapi3 , QuickCheck , schema-profunctor , servant @@ -34,7 +35,6 @@ , servant-server , singletons , singletons-th -, swagger2 , text , time , transformers @@ -66,6 +66,7 @@ mkDerivation { lens metrics-wai mtl + openapi3 QuickCheck schema-profunctor servant @@ -73,7 +74,6 @@ mkDerivation { servant-client-core servant-server singletons-th - swagger2 text time transformers diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs index 944a7afbccf..2f8d3a479cf 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs @@ -153,7 +153,6 @@ type GalleyApi = "get-one2one-conversation" GetOne2OneConversationRequest GetOne2OneConversationResponse - :<|> FedEndpoint "on-connection-removed" Domain EmptyResponse data TypingDataUpdateRequest = TypingDataUpdateRequest { typingStatus :: TypingStatus, diff --git a/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs b/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs index 3560e2c5e44..3fa1aba2871 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs @@ -77,7 +77,7 @@ sendNotification env component path body = runFederatorClient env . void $ clientIn (Proxy @BackendNotificationAPI) (Proxy @(FederatorClient c)) (withoutFirstSlash path) body -enqueue :: Q.Channel -> Domain -> Domain -> Q.DeliveryMode -> FedQueueClient c () -> IO () +enqueue :: Q.Channel -> Domain -> Domain -> Q.DeliveryMode -> FedQueueClient c a -> IO a enqueue channel originDomain targetDomain deliveryMode (FedQueueClient action) = runReaderT action FedQueueEnv {..} diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Version.hs b/libs/wire-api-federation/src/Wire/API/Federation/Version.hs index 3a433e3fb2a..0f3e113db95 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Version.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Version.hs @@ -21,10 +21,10 @@ module Wire.API.Federation.Version where import Control.Lens ((?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.OpenApi qualified as S import Data.Schema import Data.Set qualified as Set import Data.Singletons.TH -import Data.Swagger qualified as S import Imports import Wire.API.VersionInfo diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index e324cc5f178..4d4646fdb5a 100644 --- a/libs/wire-api-federation/wire-api-federation.cabal +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -97,6 +97,7 @@ library , lens , metrics-wai , mtl + , openapi3 , QuickCheck >=2.13 , schema-profunctor , servant >=0.16 @@ -104,7 +105,6 @@ library , servant-client-core , servant-server , singletons-th - , swagger2 , text >=0.11 , time >=1.8 , transformers diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index cd89ac4b9e1..6605ae69cf4 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -61,6 +61,7 @@ , metrics-wai , mime , mtl +, openapi3 , pem , polysemy , pretty @@ -81,13 +82,12 @@ , servant-client-core , servant-conduit , servant-multipart +, servant-openapi3 , servant-server -, servant-swagger , singletons , singletons-base , singletons-th , sop-core -, swagger2 , tagged , tasty , tasty-hspec @@ -167,6 +167,7 @@ mkDerivation { metrics-wai mime mtl + openapi3 pem polysemy proto-lens @@ -185,13 +186,12 @@ mkDerivation { servant-client-core servant-conduit servant-multipart + servant-openapi3 servant-server - servant-swagger singletons singletons-base singletons-th sop-core - swagger2 tagged text time @@ -239,6 +239,7 @@ mkDerivation { lens memory metrics-wai + openapi3 pem pretty process @@ -249,7 +250,6 @@ mkDerivation { schema-profunctor servant servant-server - swagger2 tasty tasty-hspec tasty-hunit diff --git a/libs/wire-api/src/Wire/API/Asset.hs b/libs/wire-api/src/Wire/API/Asset.hs index d8505a038f1..1658056c6d0 100644 --- a/libs/wire-api/src/Wire/API/Asset.hs +++ b/libs/wire-api/src/Wire/API/Asset.hs @@ -74,11 +74,11 @@ import Data.ByteString.Conversion import Data.ByteString.Lazy qualified as LBS import Data.Id import Data.Json.Util (UTCTimeMillis (fromUTCTimeMillis), toUTCTimeMillis) +import Data.OpenApi qualified as S import Data.Proxy import Data.Qualified import Data.SOP import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as T import Data.Text.Ascii (AsciiBase64Url) import Data.Text.Encoding qualified as T @@ -109,7 +109,7 @@ deriving via Schema (Asset' key) instance ToSchema (Asset' key) => (ToJSON (Asse deriving via Schema (Asset' key) instance ToSchema (Asset' key) => (FromJSON (Asset' key)) -deriving via Schema (Asset' key) instance ToSchema (Asset' key) => (S.ToSchema (Asset' key)) +deriving via Schema (Asset' key) instance (Typeable key, ToSchema (Asset' key)) => (S.ToSchema (Asset' key)) -- Generate expiry time with millisecond precision instance Arbitrary key => Arbitrary (Asset' key) where @@ -394,7 +394,7 @@ instance FromHttpApiData (AssetLocation Absolute) where instance S.ToParamSchema (AssetLocation r) where toParamSchema _ = mempty - & S.type_ ?~ S.SwaggerString + & S.type_ ?~ S.OpenApiString & S.format ?~ "url" -- | An asset as returned by the download API: if the asset is local, only a diff --git a/libs/wire-api/src/Wire/API/Call/Config.hs b/libs/wire-api/src/Wire/API/Call/Config.hs index a458891e34c..18289ca1706 100644 --- a/libs/wire-api/src/Wire/API/Call/Config.hs +++ b/libs/wire-api/src/Wire/API/Call/Config.hs @@ -81,8 +81,8 @@ import Data.ByteString.Conversion qualified as BC import Data.IP qualified as IP import Data.List.NonEmpty (NonEmpty) import Data.Misc (HttpsUrl (..), IpAddr (IpAddr), Port (..)) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Ascii import Data.Text.Encoding qualified as TE diff --git a/libs/wire-api/src/Wire/API/Connection.hs b/libs/wire-api/src/Wire/API/Connection.hs index a093c10c72e..138b6c3eb4b 100644 --- a/libs/wire-api/src/Wire/API/Connection.hs +++ b/libs/wire-api/src/Wire/API/Connection.hs @@ -45,10 +45,10 @@ import Control.Lens ((?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Id import Data.Json.Util (UTCTimeMillis) +import Data.OpenApi qualified as S import Data.Qualified (Qualified (qUnqualified), deprecatedSchema) import Data.Range import Data.Schema -import Data.Swagger qualified as S import Data.Text as Text import Imports import Servant.API @@ -142,7 +142,7 @@ data Relation deriving (FromJSON, ToJSON, S.ToSchema) via (Schema Relation) instance S.ToParamSchema Relation where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString -- | 'updateConnectionInternal', requires knowledge of the previous state (before -- 'MissingLegalholdConsent'), but the clients don't need that information. To avoid having diff --git a/libs/wire-api/src/Wire/API/Conversation.hs b/libs/wire-api/src/Wire/API/Conversation.hs index 0a92e663928..22a715678fb 100644 --- a/libs/wire-api/src/Wire/API/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Conversation.hs @@ -96,12 +96,13 @@ import Data.List.NonEmpty (NonEmpty) import Data.List1 import Data.Map qualified as Map import Data.Misc +import Data.OpenApi (deprecated) +import Data.OpenApi qualified as S import Data.Qualified import Data.Range (Range, fromRange, rangedSchema) import Data.SOP import Data.Schema import Data.Set qualified as Set -import Data.Swagger qualified as S import Data.UUID qualified as UUID import Data.UUID.V5 qualified as UUIDV5 import Imports @@ -568,14 +569,15 @@ instance ToSchema AccessRole where instance ToSchema AccessRoleLegacy where schema = - (S.schema . description ?~ desc) $ - enum @Text "AccessRoleLegacy" $ - mconcat - [ element "private" PrivateAccessRole, - element "team" TeamAccessRole, - element "activated" ActivatedAccessRole, - element "non_activated" NonActivatedAccessRole - ] + (S.schema . S.deprecated ?~ True) $ + (S.schema . description ?~ desc) $ + enum @Text "AccessRoleLegacy" $ + mconcat + [ element "private" PrivateAccessRole, + element "team" TeamAccessRole, + element "activated" ActivatedAccessRole, + element "non_activated" NonActivatedAccessRole + ] where desc = "Which users can join conversations (deprecated, use `access_role_v2` instead).\ @@ -671,7 +673,9 @@ newConvSchema sch = <$> newConvUsers .= ( fieldWithDocModifier "users" - (description ?~ usersDesc) + ( (deprecated ?~ True) + . (description ?~ usersDesc) + ) (array schema) <|> pure [] ) diff --git a/libs/wire-api/src/Wire/API/Conversation/Action.hs b/libs/wire-api/src/Wire/API/Conversation/Action.hs index f7e56a80b65..4730ba2d47f 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Action.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Action.hs @@ -38,10 +38,10 @@ import Data.Aeson.KeyMap qualified as A import Data.Id import Data.Kind import Data.List.NonEmpty qualified as NonEmptyList +import Data.OpenApi qualified as S import Data.Qualified (Qualified) import Data.Schema hiding (tag) import Data.Singletons.TH -import Data.Swagger qualified as S import Data.Time.Clock import Imports import Wire.API.Conversation @@ -71,7 +71,11 @@ data SomeConversationAction where instance Show SomeConversationAction where show (SomeConversationAction tag action) = - $(sCases ''ConversationActionTag [|tag|] [|show action|]) + "SomeConversationAction {tag = " + <> show (fromSing tag) + <> ", action = " + <> $(sCases ''ConversationActionTag [|tag|] [|show action|]) + <> "}" instance Eq SomeConversationAction where (SomeConversationAction tag1 action1) == (SomeConversationAction tag2 action2) = diff --git a/libs/wire-api/src/Wire/API/Conversation/Bot.hs b/libs/wire-api/src/Wire/API/Conversation/Bot.hs index 2fd4a442cb3..f46a83869d4 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Bot.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Bot.hs @@ -28,8 +28,8 @@ where import Data.Aeson qualified as A import Data.Id +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.API.Event.Conversation (Event) import Wire.API.User.Client.Prekey (Prekey) diff --git a/libs/wire-api/src/Wire/API/Conversation/Code.hs b/libs/wire-api/src/Wire/API/Conversation/Code.hs index b99b4012df2..51a142ddd09 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Code.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Code.hs @@ -40,8 +40,8 @@ import Data.ByteString.Conversion (toByteString') -- FUTUREWORK: move content of Data.Code here? import Data.Code as Code import Data.Misc +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import URI.ByteString qualified as URI import Wire.Arbitrary (Arbitrary, GenericUniform (..)) diff --git a/libs/wire-api/src/Wire/API/Conversation/Member.hs b/libs/wire-api/src/Wire/API/Conversation/Member.hs index fcf8b92c5da..f07f619b3e5 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Member.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Member.hs @@ -40,9 +40,10 @@ import Control.Lens ((?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A import Data.Id +import Data.OpenApi (deprecated) +import Data.OpenApi qualified as S import Data.Qualified import Data.Schema -import Data.Swagger qualified as S import Imports import Test.QuickCheck qualified as QC import Wire.API.Conversation.Role @@ -165,7 +166,7 @@ instance ToSchema OtherMember where <* (qUnqualified . omQualifiedId) .= optional (field "id" schema) <*> omService .= maybe_ (optFieldWithDocModifier "service" (description ?~ desc) schema) <*> omConvRoleName .= (field "conversation_role" schema <|> pure roleNameWireAdmin) - <* const (0 :: Int) .= optional (fieldWithDocModifier "status" (description ?~ "deprecated") schema) -- TODO: remove + <* const (0 :: Int) .= optional (fieldWithDocModifier "status" ((deprecated ?~ True) . (description ?~ "deprecated")) schema) -- TODO: remove where desc = "The reference to the owning service, if the member is a 'bot'." diff --git a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs index 86f19ee90dc..5eb4a3dc66e 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs @@ -38,8 +38,8 @@ where import Control.Arrow import Control.Lens (Traversal', makePrisms, (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Data.Time.Clock import Imports import Wire.API.Conversation.Action.Tag diff --git a/libs/wire-api/src/Wire/API/Conversation/Role.hs b/libs/wire-api/src/Wire/API/Conversation/Role.hs index 1df79697f80..edb97c23f42 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Role.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Role.hs @@ -68,11 +68,11 @@ import Data.Aeson.TH qualified as A import Data.Attoparsec.Text import Data.ByteString.Conversion import Data.Hashable +import Data.OpenApi qualified as S import Data.Range (fromRange, genRangeText) import Data.Schema import Data.Set qualified as Set import Data.Singletons.TH -import Data.Swagger qualified as S import Deriving.Swagger qualified as S import GHC.TypeLits import Imports diff --git a/libs/wire-api/src/Wire/API/Conversation/Typing.hs b/libs/wire-api/src/Wire/API/Conversation/Typing.hs index 65e728c87c6..076dbde5e47 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Typing.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Typing.hs @@ -21,8 +21,8 @@ module Wire.API.Conversation.Typing where import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.Arbitrary (Arbitrary, GenericUniform (..)) diff --git a/libs/wire-api/src/Wire/API/CustomBackend.hs b/libs/wire-api/src/Wire/API/CustomBackend.hs index 73c3a525a06..f7c12e0140d 100644 --- a/libs/wire-api/src/Wire/API/CustomBackend.hs +++ b/libs/wire-api/src/Wire/API/CustomBackend.hs @@ -24,8 +24,8 @@ where import Control.Lens ((?~)) import Data.Misc (HttpsUrl) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Deriving.Aeson import Imports import Wire.Arbitrary (Arbitrary, GenericUniform (..)) diff --git a/libs/wire-api/src/Wire/API/Deprecated.hs b/libs/wire-api/src/Wire/API/Deprecated.hs new file mode 100644 index 00000000000..c68120be996 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Deprecated.hs @@ -0,0 +1,60 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.API.Deprecated + ( Deprecated, + ) +where + +import Control.Lens +import Data.Kind (Type) +import Data.Metrics.Servant +import Data.OpenApi hiding (HasServer) +import Imports +import Servant +import Servant.Client +import Servant.OpenApi + +-- Annotate that the route is deprecated +data Deprecated deriving (Typeable) + +-- All of these instances are very similar to the instances +-- for Summary. These don't impact the API directly, but are +-- for marking the deprecated flag in the openapi output. +instance HasLink sub => HasLink (Deprecated :> sub :: Type) where + type MkLink (Deprecated :> sub) a = MkLink sub a + toLink = + let simpleToLink toA _ = toLink toA (Proxy :: Proxy sub) + in simpleToLink + +instance HasOpenApi api => HasOpenApi (Deprecated :> api :: Type) where + toOpenApi _ = + toOpenApi (Proxy @api) + & allOperations . deprecated ?~ True + +instance HasServer api ctx => HasServer (Deprecated :> api) ctx where + type ServerT (Deprecated :> api) m = ServerT api m + route _ = route $ Proxy @api + hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy @api) pc nt s + +instance HasClient m api => HasClient m (Deprecated :> api) where + type Client m (Deprecated :> api) = Client m api + clientWithRoute pm _ = clientWithRoute pm (Proxy :: Proxy api) + hoistClientMonad pm _ f cl = hoistClientMonad pm (Proxy :: Proxy api) f cl + +instance (RoutesToPaths rest) => RoutesToPaths (Deprecated :> rest) where + getRoutes = getRoutes @rest diff --git a/libs/wire-api/src/Wire/API/Error.hs b/libs/wire-api/src/Wire/API/Error.hs index 304f11596ec..8946a785721 100644 --- a/libs/wire-api/src/Wire/API/Error.hs +++ b/libs/wire-api/src/Wire/API/Error.hs @@ -49,12 +49,13 @@ where import Control.Lens (at, (%~), (.~), (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A +import Data.HashMap.Strict.InsOrd import Data.Kind import Data.Metrics.Servant +import Data.OpenApi qualified as S import Data.Proxy import Data.SOP import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Lazy qualified as LT import GHC.TypeLits @@ -65,7 +66,7 @@ import Network.Wai.Utilities.JSONResponse import Polysemy import Polysemy.Error import Servant -import Servant.Swagger +import Servant.OpenApi import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named (Named) import Wire.API.Routes.Version @@ -185,23 +186,23 @@ instance (HasServer api ctx) => HasServer (CanThrowMany es :> api) ctx where hoistServerWithContext _ = hoistServerWithContext (Proxy @api) instance - (HasSwagger api, IsSwaggerError e) => - HasSwagger (CanThrow e :> api) + (HasOpenApi api, IsSwaggerError e) => + HasOpenApi (CanThrow e :> api) where - toSwagger _ = addToSwagger @e (toSwagger (Proxy @api)) + toOpenApi _ = addToOpenApi @e (toOpenApi (Proxy @api)) type instance SpecialiseToVersion v (CanThrowMany es :> api) = CanThrowMany es :> SpecialiseToVersion v api -instance HasSwagger api => HasSwagger (CanThrowMany '() :> api) where - toSwagger _ = toSwagger (Proxy @api) +instance HasOpenApi api => HasOpenApi (CanThrowMany '() :> api) where + toOpenApi _ = toOpenApi (Proxy @api) instance - (HasSwagger (CanThrowMany es :> api), IsSwaggerError e) => - HasSwagger (CanThrowMany '(e, es) :> api) + (HasOpenApi (CanThrowMany es :> api), IsSwaggerError e) => + HasOpenApi (CanThrowMany '(e, es) :> api) where - toSwagger _ = addToSwagger @e (toSwagger (Proxy @(CanThrowMany es :> api))) + toOpenApi _ = addToOpenApi @e (toOpenApi (Proxy @(CanThrowMany es :> api))) type family DeclaredErrorEffects api :: EffectRow where DeclaredErrorEffects (CanThrow e :> api) = (ErrorEffect e ': DeclaredErrorEffects api) @@ -211,15 +212,23 @@ type family DeclaredErrorEffects api :: EffectRow where DeclaredErrorEffects (Named n api) = DeclaredErrorEffects api DeclaredErrorEffects api = '[] -errorResponseSwagger :: forall e. KnownError e => S.Response +errorResponseSwagger :: forall e. (Typeable e, KnownError e) => S.Response errorResponseSwagger = mempty & S.description .~ (eMessage err <> " (label: `" <> eLabel err <> "`)") - & S.schema ?~ S.Inline (S.toSchema (Proxy @(SStaticError e))) + -- Defaulting this to JSON, as openapi3 needs something to map a schema against. + -- This _should_ be overridden with the actual media types once we are at the + -- point of rendering out the schemas for MultiVerb. + -- Check the instance of `S.HasOpenApi (MultiVerb method (cs :: [Type]) as r)` + & S.content .~ singleton mediaType mediaTypeObject where err = dynError @e + mediaType = contentType $ Proxy @JSON + mediaTypeObject = + mempty + & S.schema ?~ S.Inline (S.toSchema (Proxy @(SStaticError e))) -addErrorResponseToSwagger :: Int -> S.Response -> S.Swagger -> S.Swagger +addErrorResponseToSwagger :: Int -> S.Response -> S.OpenApi -> S.OpenApi addErrorResponseToSwagger code resp = S.allOperations . S.responses @@ -233,7 +242,7 @@ addErrorResponseToSwagger code resp = addRef (Just (S.Inline resp1)) = S.Inline (combineResponseSwagger resp1 resp) addRef (Just r@(S.Ref _)) = r -addStaticErrorToSwagger :: forall e. KnownError e => S.Swagger -> S.Swagger +addStaticErrorToSwagger :: forall e. (Typeable e, KnownError e) => S.OpenApi -> S.OpenApi addStaticErrorToSwagger = addErrorResponseToSwagger (fromIntegral (eCode (dynError @e))) @@ -244,7 +253,7 @@ type family MapError (e :: k) :: StaticError type family ErrorEffect (e :: k) :: Effect class IsSwaggerError e where - addToSwagger :: S.Swagger -> S.Swagger + addToOpenApi :: S.OpenApi -> S.OpenApi -- | An effect for a static error type with no data. type ErrorS e = Error (Tagged e ()) @@ -323,7 +332,7 @@ instance KnownError (MapError e) => AsConstructor '[] (ErrorResponse e) where toConstructor _ = Nil fromConstructor _ = dynError @(MapError e) -instance KnownError (MapError e) => IsSwaggerResponse (ErrorResponse e) where +instance (KnownError (MapError e), Typeable (MapError e)) => IsSwaggerResponse (ErrorResponse e) where responseSwagger = pure $ errorResponseSwagger @(MapError e) instance diff --git a/libs/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index 8544a58d50d..044d3478174 100644 --- a/libs/wire-api/src/Wire/API/Error/Brig.hs +++ b/libs/wire-api/src/Wire/API/Error/Brig.hs @@ -17,6 +17,7 @@ module Wire.API.Error.Brig where +import Data.Data import Wire.API.Error data BrigError @@ -86,8 +87,12 @@ data BrigError | ServiceDisabled | InvalidBot -instance KnownError (MapError e) => IsSwaggerError (e :: BrigError) where - addToSwagger = addStaticErrorToSwagger @(MapError e) +instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: BrigError) where + addToOpenApi = addStaticErrorToSwagger @(MapError e) + +type instance MapError 'ServiceDisabled = 'StaticError 403 "service-disabled" "The desired service is currently disabled." + +type instance MapError 'InvalidBot = 'StaticError 403 "invalid-bot" "The targeted user is not a bot." type instance MapError 'ServiceDisabled = 'StaticError 403 "service-disabled" "The desired service is currently disabled." diff --git a/libs/wire-api/src/Wire/API/Error/Cannon.hs b/libs/wire-api/src/Wire/API/Error/Cannon.hs index 7cdca830697..6dea237c1fc 100644 --- a/libs/wire-api/src/Wire/API/Error/Cannon.hs +++ b/libs/wire-api/src/Wire/API/Error/Cannon.hs @@ -17,14 +17,15 @@ module Wire.API.Error.Cannon where +import Data.Data import Wire.API.Error data CannonError = ClientGone | PresenceNotRegistered -instance KnownError (MapError e) => IsSwaggerError (e :: CannonError) where - addToSwagger = addStaticErrorToSwagger @(MapError e) +instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: CannonError) where + addToOpenApi = addStaticErrorToSwagger @(MapError e) type instance MapError 'ClientGone = 'StaticError 410 "general" "client gone" diff --git a/libs/wire-api/src/Wire/API/Error/Cargohold.hs b/libs/wire-api/src/Wire/API/Error/Cargohold.hs index 26087509d12..0c4f17015cc 100644 --- a/libs/wire-api/src/Wire/API/Error/Cargohold.hs +++ b/libs/wire-api/src/Wire/API/Error/Cargohold.hs @@ -17,6 +17,7 @@ module Wire.API.Error.Cargohold where +import Data.Typeable import Wire.API.Error data CargoholdError @@ -26,8 +27,8 @@ data CargoholdError | InvalidLength | NoMatchingAssetEndpoint -instance KnownError (MapError e) => IsSwaggerError (e :: CargoholdError) where - addToSwagger = addStaticErrorToSwagger @(MapError e) +instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: CargoholdError) where + addToOpenApi = addStaticErrorToSwagger @(MapError e) type instance MapError 'AssetNotFound = 'StaticError 404 "not-found" "Asset not found" diff --git a/libs/wire-api/src/Wire/API/Error/Empty.hs b/libs/wire-api/src/Wire/API/Error/Empty.hs index 474841ef1fd..290c75c978d 100644 --- a/libs/wire-api/src/Wire/API/Error/Empty.hs +++ b/libs/wire-api/src/Wire/API/Error/Empty.hs @@ -18,7 +18,7 @@ module Wire.API.Error.Empty where import Control.Lens ((.~)) -import Data.Swagger qualified as S +import Data.OpenApi qualified as S import Data.Text qualified as Text import GHC.TypeLits import Imports diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index bbdbee41c99..59b72799992 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -37,11 +37,12 @@ import Control.Lens ((%~), (.~), (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Containers.ListUtils import Data.Domain +import Data.HashMap.Strict.InsOrd (singleton) +import Data.OpenApi qualified as S import Data.Proxy import Data.Qualified import Data.Schema import Data.Singletons.TH (genSingletons) -import Data.Swagger qualified as S import Data.Tagged import GHC.TypeLits import Imports @@ -51,6 +52,7 @@ import Network.Wai.Utilities.JSONResponse import Polysemy import Polysemy.Error import Prelude.Singletons (Show_) +import Servant.API.ContentTypes (JSON, contentType) import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Brig qualified as BrigError @@ -143,8 +145,8 @@ data GalleyError $(genSingletons [''GalleyError]) -instance KnownError (MapError e) => IsSwaggerError (e :: GalleyError) where - addToSwagger = addStaticErrorToSwagger @(MapError e) +instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: GalleyError) where + addToOpenApi = addStaticErrorToSwagger @(MapError e) instance KnownError (MapError e) => APIError (Tagged (e :: GalleyError) ()) where toResponse _ = toResponse $ dynError @(MapError e) @@ -334,7 +336,7 @@ type instance MapError 'VerificationCodeAuthFailed = 'StaticError 403 "code-auth type instance MapError 'VerificationCodeRequired = 'StaticError 403 "code-authentication-required" "Verification code required" instance IsSwaggerError AuthenticationError where - addToSwagger = + addToOpenApi = addStaticErrorToSwagger @(MapError 'ReAuthFailed) . addStaticErrorToSwagger @(MapError 'VerificationCodeAuthFailed) . addStaticErrorToSwagger @(MapError 'VerificationCodeRequired) @@ -362,7 +364,7 @@ data TeamFeatureError instance IsSwaggerError TeamFeatureError where -- Do not display in Swagger - addToSwagger = id + addToOpenApi = id type instance MapError 'AppLockInactivityTimeoutTooLow = 'StaticError 400 "inactivity-timeout-too-low" "Applock inactivity timeout must be at least 30 seconds" @@ -412,7 +414,7 @@ type instance ErrorEffect MLSProposalFailure = Error MLSProposalFailure -- Proposal failures are only reported generically in Swagger instance IsSwaggerError MLSProposalFailure where - addToSwagger = S.allOperations . S.description %~ Just . (<> desc) . fold + addToOpenApi = S.allOperations . S.description %~ Just . (<> desc) . fold where desc = "\n\n**Note**: this endpoint can execute proposals, and therefore \ @@ -463,11 +465,16 @@ instance ToSchema NonFederatingBackends where nonFederatingBackendsFromList instance IsSwaggerError NonFederatingBackends where - addToSwagger = + addToOpenApi = addErrorResponseToSwagger (HTTP.statusCode nonFederatingBackendsStatus) $ mempty & S.description .~ "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" - & S.schema ?~ S.Inline (S.toSchema (Proxy @NonFederatingBackends)) + & S.content .~ singleton mediaType mediaTypeObject + where + mediaType = contentType $ Proxy @JSON + mediaTypeObject = + mempty + & S.schema ?~ S.Inline (S.toSchema (Proxy @NonFederatingBackends)) type instance ErrorEffect NonFederatingBackends = Error NonFederatingBackends @@ -500,11 +507,18 @@ instance ToSchema UnreachableBackends where <$> (.backends) .= field "unreachable_backends" (array schema) instance IsSwaggerError UnreachableBackends where - addToSwagger = + addToOpenApi = addErrorResponseToSwagger (HTTP.statusCode unreachableBackendsStatus) $ mempty & S.description .~ "Some domains are unreachable" - & S.schema ?~ S.Inline (S.toSchema (Proxy @UnreachableBackends)) + -- Defaulting this to JSON, as openapi3 needs something to map a schema against. + -- This _should_ be overridden with the actual media types once we are at the + -- point of rendering out the schemas for MultiVerb. + -- Check the instance of `S.HasOpenApi (MultiVerb method (cs :: [Type]) as r)` + & S.content .~ singleton mediaType mediaTypeObject + where + mediaType = contentType $ Proxy @JSON + mediaTypeObject = mempty & S.schema ?~ S.Inline (S.toSchema (Proxy @UnreachableBackends)) type instance ErrorEffect UnreachableBackends = Error UnreachableBackends diff --git a/libs/wire-api/src/Wire/API/Error/Gundeck.hs b/libs/wire-api/src/Wire/API/Error/Gundeck.hs index f28432f45f1..ac9b6ce363f 100644 --- a/libs/wire-api/src/Wire/API/Error/Gundeck.hs +++ b/libs/wire-api/src/Wire/API/Error/Gundeck.hs @@ -17,6 +17,7 @@ module Wire.API.Error.Gundeck where +import Data.Typeable import Wire.API.Error data GundeckError @@ -28,8 +29,8 @@ data GundeckError | TokenNotFound | NotificationNotFound -instance KnownError (MapError e) => IsSwaggerError (e :: GundeckError) where - addToSwagger = addStaticErrorToSwagger @(MapError e) +instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: GundeckError) where + addToOpenApi = addStaticErrorToSwagger @(MapError e) type instance MapError 'AddTokenErrorNoBudget = 'StaticError 413 "sns-thread-budget-reached" "Too many concurrent calls to SNS; is SNS down?" diff --git a/libs/wire-api/src/Wire/API/Event/Conversation.hs b/libs/wire-api/src/Wire/API/Event/Conversation.hs index 6e31d4525ea..6a15c287ced 100644 --- a/libs/wire-api/src/Wire/API/Event/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Event/Conversation.hs @@ -71,10 +71,11 @@ import Data.Aeson qualified as A import Data.Aeson.KeyMap qualified as KeyMap import Data.Id import Data.Json.Util +import Data.OpenApi (deprecated) +import Data.OpenApi qualified as S import Data.Qualified import Data.SOP import Data.Schema -import Data.Swagger qualified as S import Data.Time import Imports import Test.QuickCheck qualified as QC @@ -241,7 +242,9 @@ instance ToSchema SimpleMembers where .= optional ( fieldWithDocModifier "user_ids" - (description ?~ "deprecated") + ( (description ?~ "deprecated") + . (deprecated ?~ True) + ) (array schema) ) diff --git a/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs b/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs index e6982d8f45f..32e67dcfaf6 100644 --- a/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs +++ b/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs @@ -26,8 +26,8 @@ import Data.Aeson (toJSON) import Data.Aeson qualified as A import Data.Aeson.KeyMap qualified as KeyMap import Data.Json.Util (ToJSONObject (toJSONObject)) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import GHC.TypeLits (KnownSymbol) import Imports import Test.QuickCheck.Gen (oneof) diff --git a/libs/wire-api/src/Wire/API/Event/Federation.hs b/libs/wire-api/src/Wire/API/Event/Federation.hs index cc38b1e2795..55130a9ec84 100644 --- a/libs/wire-api/src/Wire/API/Event/Federation.hs +++ b/libs/wire-api/src/Wire/API/Event/Federation.hs @@ -1,85 +1,52 @@ -{-# LANGUAGE TemplateHaskell #-} - module Wire.API.Event.Federation ( Event (..), + EventType (..), ) where -import Control.Arrow ((&&&)) -import Control.Lens (makePrisms) import Data.Aeson (FromJSON, ToJSON) import Data.Aeson qualified as A import Data.Aeson.KeyMap qualified as KeyMap import Data.Domain import Data.Json.Util (ToJSONObject (toJSONObject)) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports -import Test.QuickCheck.Gen import Wire.Arbitrary -data Event - = FederationDelete Domain - | FederationConnectionRemoved (Domain, Domain) - deriving stock (Eq, Show, Generic) - -$(makePrisms ''Event) +data Event = Event + { _eventType :: EventType, + _eventDomain :: Domain + } + deriving (Eq, Show, Ord, Generic) instance Arbitrary Event where arbitrary = - oneof - [ FederationDelete <$> arbitrary, - FederationConnectionRemoved <$> arbitrary - ] + Event + <$> arbitrary + <*> arbitrary data EventType - = FederationTypeDelete - | FederationTypeConnectionRemoved - deriving (Eq, Show, Ord, Enum, Bounded, Generic) + = FederationDelete + deriving (Eq, Show, Ord, Generic) deriving (Arbitrary) via (GenericUniform EventType) deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema EventType instance ToSchema EventType where schema = - enum @Text "FederationEventType" $ + enum @Text "EventType" $ mconcat - [ element "federation.delete" FederationTypeDelete, - element "federation.connectionRemoved" FederationTypeConnectionRemoved + [ element "federation.delete" FederationDelete ] -eventType :: Event -> EventType -eventType (FederationDelete _) = FederationTypeDelete -eventType (FederationConnectionRemoved _) = FederationTypeConnectionRemoved - -taggedEventDataSchema :: ObjectSchema SwaggerDoc (EventType, Event) -taggedEventDataSchema = - bind - (fst .= field "type" schema) - -- The fields we need to look at change based on the event - -- type, so we need to dispatch here to get monadic-ish behaviour. - -- - -- federation.delete is expecting a "domain" field that contains a bare domain string. - -- federation.connectionRemoved is expecting a "domains" field that contains exactly a pair of domains in a list - ( snd .= dispatch dataSchema - ) - where - dataSchema :: EventType -> ObjectSchema SwaggerDoc Event - dataSchema FederationTypeDelete = tag _FederationDelete deleteSchema - dataSchema FederationTypeConnectionRemoved = tag _FederationConnectionRemoved connectionRemovedSchema - --- These schemas have different fields they are targeting. -deleteSchema :: ObjectSchema SwaggerDoc Domain -deleteSchema = field "domain" schema - -connectionRemovedSchema :: ObjectSchema SwaggerDoc (Domain, Domain) -connectionRemovedSchema = field "domains" (pair schema) - --- Schemas for the events, as they have different structures. eventObjectSchema :: ObjectSchema SwaggerDoc Event -eventObjectSchema = snd <$> (eventType &&& id) .= taggedEventDataSchema +eventObjectSchema = + Event + <$> _eventType .= field "type" schema + <*> _eventDomain .= field "domain" schema instance ToSchema Event where - schema = object "FederationEvent" eventObjectSchema + schema = object "Event" eventObjectSchema instance ToJSONObject Event where toJSONObject = diff --git a/libs/wire-api/src/Wire/API/Event/Team.hs b/libs/wire-api/src/Wire/API/Event/Team.hs index a6404dd851b..d5dac32eb39 100644 --- a/libs/wire-api/src/Wire/API/Event/Team.hs +++ b/libs/wire-api/src/Wire/API/Event/Team.hs @@ -42,8 +42,8 @@ import Data.Aeson.KeyMap qualified as KeyMap import Data.Aeson.Types (Parser) import Data.Id (ConvId, TeamId, UserId) import Data.Json.Util +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Data.Time (UTCTime) import Imports import Test.QuickCheck qualified as QC diff --git a/libs/wire-api/src/Wire/API/FederationStatus.hs b/libs/wire-api/src/Wire/API/FederationStatus.hs index 257d95c96b3..b0e7a3c9859 100644 --- a/libs/wire-api/src/Wire/API/FederationStatus.hs +++ b/libs/wire-api/src/Wire/API/FederationStatus.hs @@ -10,8 +10,8 @@ import Data.Aeson (FromJSON (..), ToJSON (..), (.:)) import Data.Aeson qualified as A import Data.Aeson.Types qualified as A import Data.Domain +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.Arbitrary diff --git a/libs/wire-api/src/Wire/API/FederationUpdate.hs b/libs/wire-api/src/Wire/API/FederationUpdate.hs index 0bafee435af..aeb700fa52b 100644 --- a/libs/wire-api/src/Wire/API/FederationUpdate.hs +++ b/libs/wire-api/src/Wire/API/FederationUpdate.hs @@ -2,21 +2,18 @@ module Wire.API.FederationUpdate ( syncFedDomainConfigs, SyncFedDomainConfigsCallback (..), emptySyncFedDomainConfigsCallback, - deleteFederationRemoteGalley, - fetch, ) where import Control.Concurrent.Async import Control.Exception import Control.Retry qualified as R -import Data.Domain import Data.Set qualified as Set import Data.Text import Data.Typeable (cast) import Imports import Network.HTTP.Client (defaultManagerSettings, newManager) -import Servant.Client (BaseUrl (BaseUrl), ClientEnv (ClientEnv), ClientError, ClientM, Scheme (Http), runClientM) +import Servant.Client (BaseUrl (BaseUrl), ClientEnv (ClientEnv), ClientError, Scheme (Http), runClientM) import Servant.Client.Internal.HttpClient (defaultMakeClientRequest) import System.Logger qualified as L import Util.Options @@ -34,9 +31,6 @@ syncFedDomainConfigs (Endpoint h p) log' cb = do updateDomainsThread <- async $ loop log' clientEnv cb ioref pure (ioref, updateDomainsThread) -deleteFedRemoteGalley :: Domain -> ClientM () -deleteFedRemoteGalley dom = namedClient @IAPI.API @"delete-federation-remote-from-galley" dom - -- | Initial function for getting the set of domains from brig, and an update interval initialize :: L.Logger -> ClientEnv -> IO FederationDomainConfigs initialize logger clientEnv = @@ -56,9 +50,6 @@ initialize logger clientEnv = Just c -> pure c Nothing -> throwIO $ ErrorCall "*** Failed to reach brig for federation setup, giving up!" -deleteFederationRemoteGalley :: Domain -> ClientEnv -> IO (Either ClientError ()) -deleteFederationRemoteGalley dom = runClientM $ deleteFedRemoteGalley dom - loop :: L.Logger -> ClientEnv -> SyncFedDomainConfigsCallback -> IORef FederationDomainConfigs -> IO () loop logger clientEnv (SyncFedDomainConfigsCallback callback) env = forever $ catch go $ \(e :: SomeException) -> do diff --git a/libs/wire-api/src/Wire/API/Internal/BulkPush.hs b/libs/wire-api/src/Wire/API/Internal/BulkPush.hs index 8c07c074a2c..0ffb9eec618 100644 --- a/libs/wire-api/src/Wire/API/Internal/BulkPush.hs +++ b/libs/wire-api/src/Wire/API/Internal/BulkPush.hs @@ -20,9 +20,9 @@ module Wire.API.Internal.BulkPush where import Control.Lens import Data.Aeson import Data.Id +import Data.OpenApi qualified as Swagger import Data.Schema (ValueSchema) import Data.Schema qualified as S -import Data.Swagger qualified as Swagger import Imports import Wire.API.Internal.Notification diff --git a/libs/wire-api/src/Wire/API/Internal/Notification.hs b/libs/wire-api/src/Wire/API/Internal/Notification.hs index 3c252180668..849c8125460 100644 --- a/libs/wire-api/src/Wire/API/Internal/Notification.hs +++ b/libs/wire-api/src/Wire/API/Internal/Notification.hs @@ -45,8 +45,8 @@ import Control.Lens (makeLenses) import Data.Aeson import Data.Id import Data.List1 +import Data.OpenApi qualified as Swagger import Data.Schema qualified as S -import Data.Swagger qualified as Swagger import Imports hiding (cs) import Wire.API.Notification diff --git a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs index 339f00f0122..1f358b58e61 100644 --- a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs +++ b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs @@ -54,10 +54,10 @@ import Data.Aeson.Types qualified as Aeson import Data.Bifunctor import Data.ByteArray hiding (index) import Data.ByteArray qualified as BA +import Data.OpenApi qualified as S +import Data.OpenApi.Internal.Schema qualified as S import Data.Proxy import Data.Schema -import Data.Swagger qualified as S -import Data.Swagger.Internal.Schema qualified as S import Data.Text qualified as T import Data.Text.Lazy qualified as LT import Data.Text.Lazy.Builder qualified as LT @@ -82,7 +82,7 @@ instance ToSchema CipherSuite where instance S.ToParamSchema CipherSuite where toParamSchema _ = mempty - & S.type_ ?~ S.SwaggerNumber + & S.type_ ?~ S.OpenApiNumber instance FromHttpApiData CipherSuite where parseUrlPiece t = do @@ -253,7 +253,7 @@ instance FromJSONKey SignatureSchemeTag where fromJSONKey = Aeson.FromJSONKeyTextParser parseSignatureScheme instance S.ToParamSchema SignatureSchemeTag where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString instance FromHttpApiData SignatureSchemeTag where parseQueryParam = note "Unknown signature scheme" . signatureSchemeFromName diff --git a/libs/wire-api/src/Wire/API/MLS/CommitBundle.hs b/libs/wire-api/src/Wire/API/MLS/CommitBundle.hs index 905a5bcb45c..ccfe3006284 100644 --- a/libs/wire-api/src/Wire/API/MLS/CommitBundle.hs +++ b/libs/wire-api/src/Wire/API/MLS/CommitBundle.hs @@ -18,7 +18,7 @@ module Wire.API.MLS.CommitBundle (CommitBundle (..)) where import Control.Applicative -import Data.Swagger qualified as S +import Data.OpenApi qualified as S import Data.Text qualified as T import Imports import Wire.API.MLS.GroupInfo diff --git a/libs/wire-api/src/Wire/API/MLS/Credential.hs b/libs/wire-api/src/Wire/API/MLS/Credential.hs index ecfda8810ba..1e9dd7d0b80 100644 --- a/libs/wire-api/src/Wire/API/MLS/Credential.hs +++ b/libs/wire-api/src/Wire/API/MLS/Credential.hs @@ -32,9 +32,9 @@ import Data.ByteString.Base64.URL qualified as B64URL import Data.ByteString.Lazy qualified as L import Data.Domain import Data.Id +import Data.OpenApi qualified as S import Data.Qualified -import Data.Schema hiding (HasField) -import Data.Swagger qualified as S +import Data.Schema import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.UUID @@ -114,7 +114,7 @@ instance ToSchema ClientIdentity where <*> ciClient .= field "client_id" schema instance S.ToParamSchema ClientIdentity where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString instance FromHttpApiData ClientIdentity where parseHeader = decodeMLS' @@ -186,7 +186,7 @@ instance FromJSONKey SignaturePurpose where either fail pure . signaturePurposeFromName instance S.ToParamSchema SignaturePurpose where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString instance FromHttpApiData SignaturePurpose where parseQueryParam = first T.pack . signaturePurposeFromName diff --git a/libs/wire-api/src/Wire/API/MLS/Group.hs b/libs/wire-api/src/Wire/API/MLS/Group.hs index 2153b6d1b7a..afe0c3049ae 100644 --- a/libs/wire-api/src/Wire/API/MLS/Group.hs +++ b/libs/wire-api/src/Wire/API/MLS/Group.hs @@ -19,8 +19,8 @@ module Wire.API.MLS.Group where import Data.Aeson qualified as A import Data.Json.Util +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Servant import Wire.API.MLS.Serialisation diff --git a/libs/wire-api/src/Wire/API/MLS/GroupInfo.hs b/libs/wire-api/src/Wire/API/MLS/GroupInfo.hs index 2c0182f0dd4..1865918c2d7 100644 --- a/libs/wire-api/src/Wire/API/MLS/GroupInfo.hs +++ b/libs/wire-api/src/Wire/API/MLS/GroupInfo.hs @@ -25,7 +25,7 @@ where import Data.Binary.Get import Data.Binary.Put import Data.ByteString.Lazy qualified as LBS -import Data.Swagger qualified as S +import Data.OpenApi qualified as S import GHC.Records import Imports import Wire.API.MLS.CipherSuite diff --git a/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs b/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs index c3bc1855b69..1402ff17b9a 100644 --- a/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs +++ b/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs @@ -40,10 +40,10 @@ import Data.Bifunctor import Data.ByteString.Lazy qualified as LBS import Data.Id import Data.Json.Util +import Data.OpenApi qualified as S import Data.Qualified import Data.Range import Data.Schema hiding (HasField) -import Data.Swagger qualified as S import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.X509 qualified as X509 diff --git a/libs/wire-api/src/Wire/API/MLS/Keys.hs b/libs/wire-api/src/Wire/API/MLS/Keys.hs index 70e1f40c156..179ec9909cd 100644 --- a/libs/wire-api/src/Wire/API/MLS/Keys.hs +++ b/libs/wire-api/src/Wire/API/MLS/Keys.hs @@ -29,8 +29,8 @@ import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.ByteArray import Data.Json.Util import Data.Map qualified as Map +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential diff --git a/libs/wire-api/src/Wire/API/MLS/LeafNode.hs b/libs/wire-api/src/Wire/API/MLS/LeafNode.hs index a41da43b08a..e7ebab85580 100644 --- a/libs/wire-api/src/Wire/API/MLS/LeafNode.hs +++ b/libs/wire-api/src/Wire/API/MLS/LeafNode.hs @@ -28,7 +28,7 @@ module Wire.API.MLS.LeafNode where import Data.Binary -import Data.Swagger qualified as S +import Data.OpenApi qualified as S import GHC.Records import Imports import Test.QuickCheck diff --git a/libs/wire-api/src/Wire/API/MLS/Message.hs b/libs/wire-api/src/Wire/API/MLS/Message.hs index f8b62a192aa..d9e6aa1f624 100644 --- a/libs/wire-api/src/Wire/API/MLS/Message.hs +++ b/libs/wire-api/src/Wire/API/MLS/Message.hs @@ -42,8 +42,8 @@ import Control.Lens ((?~)) import Data.Aeson qualified as A import Data.Binary import Data.Json.Util +import Data.OpenApi qualified as S import Data.Schema hiding (HasField) -import Data.Swagger qualified as S import GHC.Records import Imports hiding (cs) import Test.QuickCheck hiding (label) diff --git a/libs/wire-api/src/Wire/API/MLS/PublicGroupState.hs b/libs/wire-api/src/Wire/API/MLS/PublicGroupState.hs index cd75cf96320..81ee1095616 100644 --- a/libs/wire-api/src/Wire/API/MLS/PublicGroupState.hs +++ b/libs/wire-api/src/Wire/API/MLS/PublicGroupState.hs @@ -22,7 +22,7 @@ import Data.Binary import Data.Binary.Get import Data.Binary.Put import Data.ByteString.Lazy qualified as LBS -import Data.Swagger qualified as S +import Data.OpenApi qualified as S import Imports import Test.QuickCheck hiding (label) import Wire.API.MLS.CipherSuite diff --git a/libs/wire-api/src/Wire/API/MLS/Serialisation.hs b/libs/wire-api/src/Wire/API/MLS/Serialisation.hs index 51c611ecfb0..7ae47e8493a 100644 --- a/libs/wire-api/src/Wire/API/MLS/Serialisation.hs +++ b/libs/wire-api/src/Wire/API/MLS/Serialisation.hs @@ -67,9 +67,9 @@ import Data.ByteString qualified as BS import Data.ByteString.Lazy qualified as LBS import Data.Json.Util import Data.Kind +import Data.OpenApi qualified as S import Data.Proxy import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Debug.Trace import Imports diff --git a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs index e47835cff95..29fde7700e2 100644 --- a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs +++ b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs @@ -29,9 +29,9 @@ import Data.Aeson qualified as A import Data.ByteString.Conversion import Data.Id import Data.Json.Util +import Data.OpenApi qualified as S import Data.Qualified import Data.Schema hiding (HasField) -import Data.Swagger qualified as S import Data.Text qualified as T import Data.Time.Clock import GHC.Records diff --git a/libs/wire-api/src/Wire/API/MLS/Welcome.hs b/libs/wire-api/src/Wire/API/MLS/Welcome.hs index dc4097dd12d..08028e31219 100644 --- a/libs/wire-api/src/Wire/API/MLS/Welcome.hs +++ b/libs/wire-api/src/Wire/API/MLS/Welcome.hs @@ -17,7 +17,7 @@ module Wire.API.MLS.Welcome where -import Data.Swagger qualified as S +import Data.OpenApi qualified as S import Imports hiding (cs) import Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit diff --git a/libs/wire-api/src/Wire/API/MakesFederatedCall.hs b/libs/wire-api/src/Wire/API/MakesFederatedCall.hs index fbc133d6728..ba24fd4ee16 100644 --- a/libs/wire-api/src/Wire/API/MakesFederatedCall.hs +++ b/libs/wire-api/src/Wire/API/MakesFederatedCall.hs @@ -31,20 +31,22 @@ module Wire.API.MakesFederatedCall ) where +import Control.Lens ((<>~)) import Data.Aeson import Data.Constraint +import Data.HashSet.InsOrd (singleton) import Data.Kind import Data.Metrics.Servant +import Data.OpenApi qualified as S import Data.Proxy import Data.Schema -import Data.Swagger.Operation (addExtensions) import Data.Text qualified as T import GHC.TypeLits import Imports import Servant.API import Servant.Client +import Servant.OpenApi import Servant.Server -import Servant.Swagger import Test.QuickCheck (Arbitrary) import TransitiveAnns.Types import Unsafe.Coerce (unsafeCoerce) @@ -158,24 +160,32 @@ type instance -- | 'MakesFederatedCall' annotates the swagger documentation with an extension -- tag @x-wire-makes-federated-calls-to@. -instance (HasSwagger api, KnownSymbol name, KnownSymbol (ShowComponent comp)) => HasSwagger (MakesFederatedCall comp name :> api :: Type) where - toSwagger _ = - toSwagger (Proxy @api) - & addExtensions - mergeJSONArray - [ ( "wire-makes-federated-call-to", - Array - [ Array - [ String $ T.pack $ symbolVal $ Proxy @(ShowComponent comp), - String $ T.pack $ symbolVal $ Proxy @name - ] - ] +instance (HasOpenApi api, KnownSymbol name, KnownSymbol (ShowComponent comp)) => HasOpenApi (MakesFederatedCall comp name :> api :: Type) where + toOpenApi _ = + toOpenApi (Proxy @api) + -- Since extensions aren't in the openapi3 library yet, + -- and the PRs for their support seem be going no where quickly, I'm using + -- tags instead. https://github.com/biocad/openapi3/pull/43 + -- Basically, this is similar to the old system, except we don't have nested JSON to + -- work with. So I'm using the magic string and sticking the call name on the end + -- and sticking the component in the description. This ordering is important as we + -- can't have duplicate tag names on an object. + + -- Set the tags at the top of OpenApi object + & S.tags + <>~ singleton + ( S.Tag + name + (pure $ T.pack (symbolVal $ Proxy @(ShowComponent comp))) + Nothing ) - ] - -mergeJSONArray :: Value -> Value -> Value -mergeJSONArray (Array x) (Array y) = Array $ x <> y -mergeJSONArray _ _ = error "impossible! bug in construction of federated calls JSON" + -- Set the tags on the specific path we're looking at + -- This is where the tag is actually registered on the path + -- so it can be picked up by fedcalls. + & S.allOperations . S.tags <>~ setName + where + name = "wire-makes-federated-call-to-" <> T.pack (symbolVal $ Proxy @name) + setName = singleton name instance HasClient m api => HasClient m (MakesFederatedCall comp name :> api :: Type) where type Client m (MakesFederatedCall comp name :> api) = Client m api diff --git a/libs/wire-api/src/Wire/API/Message.hs b/libs/wire-api/src/Wire/API/Message.hs index e258cc3e74a..3b651796e21 100644 --- a/libs/wire-api/src/Wire/API/Message.hs +++ b/libs/wire-api/src/Wire/API/Message.hs @@ -67,6 +67,7 @@ import Data.Domain (Domain, domainText, mkDomain) import Data.Id import Data.Json.Util import Data.Map.Strict qualified as Map +import Data.OpenApi qualified as S import Data.ProtoLens qualified as ProtoLens import Data.ProtoLens.Field qualified as ProtoLens import Data.ProtocolBuffers qualified as Protobuf @@ -74,7 +75,6 @@ import Data.Qualified (Qualified (..)) import Data.Schema import Data.Serialize (runGet) import Data.Set qualified as Set -import Data.Swagger qualified as S import Data.Text.Read qualified as Reader import Data.UUID qualified as UUID import Imports @@ -553,7 +553,7 @@ data IgnoreMissing deriving (Show, Eq) instance S.ToParamSchema IgnoreMissing where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString instance FromHttpApiData IgnoreMissing where parseQueryParam = \case @@ -566,7 +566,7 @@ data ReportMissing | ReportMissingList (Set UserId) instance S.ToParamSchema ReportMissing where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString instance FromHttpApiData ReportMissing where parseQueryParam = \case diff --git a/libs/wire-api/src/Wire/API/Notification.hs b/libs/wire-api/src/Wire/API/Notification.hs index b404e05db61..1b7601bce10 100644 --- a/libs/wire-api/src/Wire/API/Notification.hs +++ b/libs/wire-api/src/Wire/API/Notification.hs @@ -47,10 +47,10 @@ import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap import Data.Id import Data.Json.Util import Data.List.NonEmpty (NonEmpty) +import Data.OpenApi (ToParamSchema (..)) +import Data.OpenApi qualified as S import Data.SOP import Data.Schema -import Data.Swagger (ToParamSchema (..)) -import Data.Swagger qualified as S import Data.Time.Clock (UTCTime) import Data.UUID qualified as UUID import Imports diff --git a/libs/wire-api/src/Wire/API/OAuth.hs b/libs/wire-api/src/Wire/API/OAuth.hs index d75c663d7a2..8b0a8617ad3 100644 --- a/libs/wire-api/src/Wire/API/OAuth.hs +++ b/libs/wire-api/src/Wire/API/OAuth.hs @@ -30,11 +30,11 @@ import Data.ByteString.Lazy (toStrict) import Data.Either.Combinators (mapLeft) import Data.HashMap.Strict qualified as HM import Data.Id as Id +import Data.OpenApi (ToParamSchema (..)) +import Data.OpenApi qualified as S import Data.Range import Data.Schema import Data.Set qualified as Set -import Data.Swagger (ToParamSchema (..)) -import Data.Swagger qualified as S import Data.Text qualified as T import Data.Text.Ascii import Data.Text.Encoding qualified as TE @@ -44,7 +44,7 @@ import GHC.TypeLits (Nat, symbolVal) import Imports hiding (exp, head) import Prelude.Singletons (Show_) import Servant hiding (Handler, JSON, Tagged, addHeader, respond) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Test.QuickCheck (Arbitrary (..)) import URI.ByteString import URI.ByteString.QQ qualified as URI.QQ @@ -640,8 +640,8 @@ data OAuthError | OAuthInvalidRefreshToken | OAuthInvalidGrant -instance KnownError (MapError e) => IsSwaggerError (e :: OAuthError) where - addToSwagger = addStaticErrorToSwagger @(MapError e) +instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: OAuthError) where + addToOpenApi = addStaticErrorToSwagger @(MapError e) type instance MapError 'OAuthClientNotFound = 'StaticError 404 "not-found" "OAuth client not found" diff --git a/libs/wire-api/src/Wire/API/Properties.hs b/libs/wire-api/src/Wire/API/Properties.hs index 70e03c71437..debcf9016d7 100644 --- a/libs/wire-api/src/Wire/API/Properties.hs +++ b/libs/wire-api/src/Wire/API/Properties.hs @@ -30,7 +30,7 @@ import Data.Aeson (FromJSON (..), ToJSON (..), Value) import Data.Aeson qualified as A import Data.ByteString.Conversion import Data.Hashable (Hashable) -import Data.Swagger qualified as S +import Data.OpenApi qualified as S import Data.Text.Ascii import Imports import Servant @@ -43,7 +43,7 @@ instance S.ToSchema PropertyKeysAndValues where declareNamedSchema _ = pure $ S.NamedSchema (Just "PropertyKeysAndValues") $ - mempty & S.type_ ?~ S.SwaggerObject + mempty & S.type_ ?~ S.OpenApiObject newtype PropertyKey = PropertyKey {propertyKeyName :: AsciiPrintable} @@ -64,7 +64,7 @@ newtype PropertyKey = PropertyKey instance S.ToParamSchema PropertyKey where toParamSchema _ = mempty - & S.type_ ?~ S.SwaggerString + & S.type_ ?~ S.OpenApiString & S.format ?~ "printable" -- | A raw, unparsed property value. diff --git a/libs/wire-api/src/Wire/API/Provider/Bot.hs b/libs/wire-api/src/Wire/API/Provider/Bot.hs index 0db3cabeaff..e8a1f5b1c4a 100644 --- a/libs/wire-api/src/Wire/API/Provider/Bot.hs +++ b/libs/wire-api/src/Wire/API/Provider/Bot.hs @@ -34,8 +34,8 @@ import Control.Lens (makeLenses) import Data.Aeson qualified as A import Data.Handle (Handle) import Data.Id +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.API.Conversation.Member (OtherMember (..)) import Wire.API.User.Profile (ColourId, Name) diff --git a/libs/wire-api/src/Wire/API/Provider/Service.hs b/libs/wire-api/src/Wire/API/Provider/Service.hs index 28a1e5609a1..c1110a58f25 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service.hs @@ -61,11 +61,11 @@ import Data.Id import Data.Json.Util ((#)) import Data.List1 (List1) import Data.Misc (HttpsUrl (..), PlainTextPassword6) +import Data.OpenApi qualified as S import Data.PEM (PEM, pemParseBS, pemWriteLBS) import Data.Proxy import Data.Range (Range) import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Ascii import Data.Text.Encoding qualified as Text diff --git a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs index 522c519ff87..07f0910ce5e 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs @@ -41,13 +41,18 @@ where import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON)) import Data.Aeson qualified as JSON +import Data.ByteString (toStrict) import Data.ByteString.Builder qualified as BB import Data.ByteString.Char8 qualified as C8 import Data.ByteString.Conversion -import Data.Range (Range, fromRange) +import Data.OpenApi qualified as S +import Data.Range (Range, fromRange, rangedSchema) import Data.Range qualified as Range +import Data.Schema import Data.Set qualified as Set +import Data.Text.Encoding (decodeUtf8With) import Data.Text.Encoding qualified as Text +import Data.Text.Encoding.Error (lenientDecode) import Data.Type.Ord import GHC.TypeLits (KnownNat, Nat) import Imports @@ -173,6 +178,16 @@ instance FromJSON ServiceTag where JSON.withText "ServiceTag" $ either fail pure . runParser parser . Text.encodeUtf8 +instance ToSchema ServiceTag where + schema = enum @Text "" . mconcat $ (\a -> element (decodeUtf8With lenientDecode $ toStrict $ toByteString a) a) <$> [minBound ..] + +instance S.ToParamSchema ServiceTag where + toParamSchema _ = + mempty + { S._schemaType = Just S.OpenApiString, + S._schemaEnum = Just (toJSON <$> [(minBound :: ServiceTag) ..]) + } + -------------------------------------------------------------------------------- -- Bounded ServiceTag Queries @@ -181,6 +196,19 @@ newtype QueryAnyTags (m :: Nat) (n :: Nat) = QueryAnyTags {queryAnyTagsRange :: Range m n (Set (QueryAllTags m n))} deriving stock (Eq, Show, Ord) +instance (m <= n) => S.ToParamSchema (QueryAnyTags m n) where + toParamSchema _ = + mempty + { S._schemaType = Just S.OpenApiString, + S._schemaEnum = Just (toJSON <$> [(minBound :: ServiceTag) ..]) + } + +instance (KnownNat n, KnownNat m, m <= n) => ToSchema (QueryAnyTags m n) where + schema = + let sch :: ValueSchema NamedSwaggerDoc (Range m n (Set (QueryAllTags m n))) + sch = fromRange .= rangedSchema (named "QueryAnyTags" $ set schema) + in queryAnyTagsRange .= (QueryAnyTags <$> sch) + instance (KnownNat m, KnownNat n, m <= n) => Arbitrary (QueryAnyTags m n) where arbitrary = QueryAnyTags <$> arbitrary @@ -236,6 +264,12 @@ instance (KnownNat m, KnownNat n, m <= n) => FromByteString (QueryAllTags m n) w rs <- either fail pure (Range.checkedEither (Set.fromList ts)) pure $! QueryAllTags rs +instance (KnownNat m, KnownNat n, m <= n) => ToSchema (QueryAllTags m n) where + schema = + let sch :: ValueSchema NamedSwaggerDoc (Range m n (Set ServiceTag)) + sch = fromRange .= rangedSchema (named "QueryAllTags" $ set schema) + in queryAllTagsRange .= fmap QueryAllTags sch + -------------------------------------------------------------------------------- -- ServiceTag Matchers diff --git a/libs/wire-api/src/Wire/API/Push/V2/Token.hs b/libs/wire-api/src/Wire/API/Push/V2/Token.hs index ee8e828670d..0cf7b292af4 100644 --- a/libs/wire-api/src/Wire/API/Push/V2/Token.hs +++ b/libs/wire-api/src/Wire/API/Push/V2/Token.hs @@ -47,10 +47,10 @@ import Data.Aeson qualified as A import Data.Attoparsec.ByteString (takeByteString) import Data.ByteString.Conversion import Data.Id +import Data.OpenApi (ToParamSchema) +import Data.OpenApi qualified as S import Data.SOP import Data.Schema -import Data.Swagger (ToParamSchema) -import Data.Swagger qualified as S import Generics.SOP qualified as GSOP import Imports import Servant diff --git a/libs/wire-api/src/Wire/API/RawJson.hs b/libs/wire-api/src/Wire/API/RawJson.hs index fd0517ea289..08529ded900 100644 --- a/libs/wire-api/src/Wire/API/RawJson.hs +++ b/libs/wire-api/src/Wire/API/RawJson.hs @@ -20,7 +20,7 @@ module Wire.API.RawJson where import Control.Lens -import Data.Swagger qualified as Swagger +import Data.OpenApi qualified as Swagger import Imports import Servant import Test.QuickCheck @@ -43,6 +43,6 @@ instance Swagger.ToSchema RawJson where declareNamedSchema _ = pure . Swagger.NamedSchema (Just "RawJson") $ mempty - & Swagger.type_ ?~ Swagger.SwaggerObject + & Swagger.type_ ?~ Swagger.OpenApiObject & Swagger.description ?~ "Any JSON as plain string. The object structure is not specified in this schema." diff --git a/libs/wire-api/src/Wire/API/Routes/API.hs b/libs/wire-api/src/Wire/API/Routes/API.hs index b43569bf76f..23ac38e6fed 100644 --- a/libs/wire-api/src/Wire/API/Routes/API.hs +++ b/libs/wire-api/src/Wire/API/Routes/API.hs @@ -31,14 +31,14 @@ where import Data.Domain import Data.Kind +import Data.OpenApi qualified as S import Data.Proxy -import Data.Swagger qualified as S import Imports import Polysemy import Polysemy.Error import Polysemy.Internal import Servant hiding (Union) -import Servant.Swagger +import Servant.OpenApi import Wire.API.Error import Wire.API.Routes.Named import Wire.API.Routes.Version @@ -47,8 +47,8 @@ class ServiceAPI service (v :: Version) where type ServiceAPIRoutes service type SpecialisedAPIRoutes v service :: Type type SpecialisedAPIRoutes v service = SpecialiseToVersion v (ServiceAPIRoutes service) - serviceSwagger :: HasSwagger (SpecialisedAPIRoutes v service) => S.Swagger - serviceSwagger = toSwagger (Proxy @(SpecialisedAPIRoutes v service)) + serviceSwagger :: HasOpenApi (SpecialisedAPIRoutes v service) => S.OpenApi + serviceSwagger = toOpenApi (Proxy @(SpecialisedAPIRoutes v service)) instance ServiceAPI VersionAPITag v where type ServiceAPIRoutes VersionAPITag = VersionAPI diff --git a/libs/wire-api/src/Wire/API/Routes/AssetBody.hs b/libs/wire-api/src/Wire/API/Routes/AssetBody.hs index 2b6989d308b..4998c10f538 100644 --- a/libs/wire-api/src/Wire/API/Routes/AssetBody.hs +++ b/libs/wire-api/src/Wire/API/Routes/AssetBody.hs @@ -25,13 +25,13 @@ where import Conduit import Data.ByteString.Lazy qualified as LBS -import Data.Swagger -import Data.Swagger.Internal.Schema +import Data.OpenApi +import Data.OpenApi.Internal.Schema import Imports import Network.HTTP.Media ((//)) import Servant import Servant.Conduit () -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () data MultipartMixed diff --git a/libs/wire-api/src/Wire/API/Routes/Bearer.hs b/libs/wire-api/src/Wire/API/Routes/Bearer.hs index 545db5254df..64a1baed79f 100644 --- a/libs/wire-api/src/Wire/API/Routes/Bearer.hs +++ b/libs/wire-api/src/Wire/API/Routes/Bearer.hs @@ -21,11 +21,11 @@ import Control.Lens ((<>~)) import Data.ByteString qualified as BS import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap import Data.Metrics.Servant -import Data.Swagger hiding (Header) +import Data.OpenApi hiding (HasServer, Header) import Data.Text.Encoding qualified as T import Imports import Servant -import Servant.Swagger +import Servant.OpenApi import Wire.API.Routes.Version newtype Bearer a = Bearer {unBearer :: a} @@ -47,9 +47,9 @@ type instance SpecialiseToVersion v (Bearer a :> api) = Bearer a :> SpecialiseToVersion v api -instance HasSwagger api => HasSwagger (Bearer a :> api) where - toSwagger _ = - toSwagger (Proxy @api) +instance HasOpenApi api => HasOpenApi (Bearer a :> api) where + toOpenApi _ = + toOpenApi (Proxy @api) & security <>~ [SecurityRequirement $ InsOrdHashMap.singleton "ZAuth" []] instance RoutesToPaths api => RoutesToPaths (Bearer a :> api) where diff --git a/libs/wire-api/src/Wire/API/Routes/CSV.hs b/libs/wire-api/src/Wire/API/Routes/CSV.hs index 0d09941545c..0345336c378 100644 --- a/libs/wire-api/src/Wire/API/Routes/CSV.hs +++ b/libs/wire-api/src/Wire/API/Routes/CSV.hs @@ -17,6 +17,10 @@ module Wire.API.Routes.CSV where +import Control.Lens +import Data.OpenApi qualified as O +import Data.OpenApi.Internal.Schema +import Imports import Network.HTTP.Media.MediaType import Servant.API @@ -24,3 +28,11 @@ data CSV instance Accept CSV where contentType _ = "text" // "csv" + +instance ToSchema CSV where + declareNamedSchema _ = + plain $ + mempty + & O.title ?~ "CSV" + & O.type_ ?~ O.OpenApiString + & O.format ?~ "text/csv" diff --git a/libs/wire-api/src/Wire/API/Routes/Cookies.hs b/libs/wire-api/src/Wire/API/Routes/Cookies.hs index 10383904ccb..2449f074c76 100644 --- a/libs/wire-api/src/Wire/API/Routes/Cookies.hs +++ b/libs/wire-api/src/Wire/API/Routes/Cookies.hs @@ -27,7 +27,7 @@ import Data.Text.Encoding qualified as T import GHC.TypeLits import Imports import Servant -import Servant.Swagger +import Servant.OpenApi import Web.Cookie (parseCookies) import Wire.API.Routes.Version @@ -63,8 +63,8 @@ type instance SpecialiseToVersion v (Cookies cs :> api) = Cookies cs :> SpecialiseToVersion v api -instance HasSwagger api => HasSwagger (Cookies cs :> api) where - toSwagger _ = toSwagger (Proxy @api) +instance HasOpenApi api => HasOpenApi (Cookies cs :> api) where + toOpenApi _ = toOpenApi (Proxy @api) class CookieArgs (cs :: [Type]) where -- example: AddArgs ["foo" :: Foo, "bar" :: Bar] a = Foo -> Bar -> a diff --git a/libs/wire-api/src/Wire/API/Routes/FederationDomainConfig.hs b/libs/wire-api/src/Wire/API/Routes/FederationDomainConfig.hs index 5ce5e7ca871..8530f78275a 100644 --- a/libs/wire-api/src/Wire/API/Routes/FederationDomainConfig.hs +++ b/libs/wire-api/src/Wire/API/Routes/FederationDomainConfig.hs @@ -26,8 +26,8 @@ where import Control.Lens ((?~)) import Data.Aeson (FromJSON, ToJSON) import Data.Domain (Domain) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import GHC.Generics import Imports import Wire.API.User.Search (FederatedUserSearchPolicy) diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs index 241864cfe8c..d9e95e0dbc7 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -43,14 +43,14 @@ import Data.CommaSeparatedList import Data.Domain (Domain) import Data.Handle (Handle) import Data.Id as Id +import Data.OpenApi (HasInfo (info), HasTitle (title), OpenApi) +import Data.OpenApi qualified as S import Data.Qualified (Qualified) import Data.Schema hiding (swaggerDoc) -import Data.Swagger (HasInfo (info), HasTitle (title), Swagger) -import Data.Swagger qualified as S import Imports hiding (head) import Servant hiding (Handler, WithStatus, addHeader, respond) -import Servant.Swagger (HasSwagger (toSwagger)) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi (HasOpenApi (toOpenApi)) +import Servant.OpenApi.Internal.Orphans () import Wire.API.Connection import Wire.API.Error import Wire.API.Error.Brig @@ -645,41 +645,11 @@ type FederationRemotesAPI = :> ReqBody '[JSON] FederationDomainConfig :> Put '[JSON] () ) - :<|> Named - "delete-federation-remotes" - ( Description FederationRemotesAPIDescription - :> Description FederationRemotesAPIDeleteDescription - :> "federation" - :> "remotes" - :> Capture "domain" Domain - :> Delete '[JSON] () - ) - -- This is nominally similar to delete-federation-remotes, - -- but is called from Galley to delete the one-on-one coversations. - -- This is needed as Galley doesn't have access to the tables - -- that hold these values. We don't want these deletes to happen - -- in delete-federation-remotes as brig might fall over and leave - -- some records hanging around. Galley uses a Rabbit queue to track - -- what is has done and can recover from a service falling over. - :<|> Named - "delete-federation-remote-from-galley" - ( Description FederationRemotesAPIDescription - :> Description FederationRemotesAPIDeleteDescription - :> "federation" - :> "remote" - :> Capture "domain" Domain - :> "galley" - :> Delete '[JSON] () - ) type FederationRemotesAPIDescription = "See https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections for background. " -type FederationRemotesAPIDeleteDescription = - "**WARNING!** If you remove a remote connection, all users from that remote will be removed from local conversations, and all \ - \group conversations hosted by that remote will be removed from the local backend. This cannot be reverted! " - -swaggerDoc :: Swagger +swaggerDoc :: OpenApi swaggerDoc = - toSwagger (Proxy @API) + toOpenApi (Proxy @API) & info . title .~ "Wire-Server internal brig API" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/Connection.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/Connection.hs index f9226607259..7f3d76810cf 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/Connection.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/Connection.hs @@ -21,9 +21,9 @@ module Wire.API.Routes.Internal.Brig.Connection where import Data.Aeson (FromJSON, ToJSON) import Data.Id +import Data.OpenApi qualified as S import Data.Qualified import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.API.Connection diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/EJPD.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/EJPD.hs index efd26df2ee0..93db38b2974 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/EJPD.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/EJPD.hs @@ -28,7 +28,7 @@ where import Data.Aeson hiding (json) import Data.Handle (Handle) import Data.Id (TeamId, UserId) -import Data.Swagger (ToSchema) +import Data.OpenApi (ToSchema) import Deriving.Swagger (CamelToSnake, CustomSwagger (..), FieldLabelModifier, StripSuffix) import Imports hiding (head) import Test.QuickCheck (Arbitrary) diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs index a8a2747af7d..8974da4c27c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs @@ -20,7 +20,7 @@ module Wire.API.Routes.Internal.Brig.OAuth where import Data.Id (OAuthClientId) import Servant (JSON) import Servant hiding (Handler, JSON, Tagged, addHeader, respond) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.OAuth import Wire.API.Routes.Named (Named (..)) 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 6b45b977e68..0cca4948901 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 @@ -19,7 +19,7 @@ module Wire.API.Routes.Internal.Brig.SearchIndex where import Servant (JSON) import Servant hiding (Handler, JSON, Tagged, addHeader, respond) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Routes.Named (Named (..)) type ISearchIndexAPI = diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Cannon.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Cannon.hs index ff0fe916a1a..b8f1652bc7a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Cannon.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Cannon.hs @@ -2,10 +2,10 @@ module Wire.API.Routes.Internal.Cannon where import Control.Lens ((.~)) import Data.Id -import Data.Swagger (HasInfo (info), HasTitle (title), Swagger) +import Data.OpenApi (HasInfo (info), HasTitle (title), OpenApi) import Imports import Servant -import Servant.Swagger (HasSwagger (toSwagger)) +import Servant.OpenApi (HasOpenApi (toOpenApi)) import Wire.API.Error import Wire.API.Error.Cannon import Wire.API.Internal.BulkPush @@ -59,7 +59,7 @@ type API = ) ) -swaggerDoc :: Swagger +swaggerDoc :: OpenApi swaggerDoc = - toSwagger (Proxy @API) + toOpenApi (Proxy @API) & info . title .~ "Wire-Server internal cannon API" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs index 825623ac9c6..cb9599b441e 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs @@ -18,10 +18,10 @@ module Wire.API.Routes.Internal.Cargohold where import Control.Lens -import Data.Swagger +import Data.OpenApi import Imports import Servant -import Servant.Swagger +import Servant.OpenApi import Wire.API.Routes.MultiVerb type InternalAPI = @@ -29,7 +29,7 @@ type InternalAPI = :> "status" :> MultiVerb 'GET '() '[RespondEmpty 200 "OK"] () -swaggerDoc :: Swagger +swaggerDoc :: OpenApi swaggerDoc = - toSwagger (Proxy @InternalAPI) + toOpenApi (Proxy @InternalAPI) & info . title .~ "Wire-Server internal cargohold API" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs index f5d38d88c02..0189df23a34 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -19,13 +19,13 @@ module Wire.API.Routes.Internal.Galley where import Control.Lens ((.~)) import Data.Id as Id +import Data.OpenApi (OpenApi, info, title) import Data.Range -import Data.Swagger (Swagger, info, title) import GHC.TypeLits (AppendSymbol) import Imports hiding (head) import Servant hiding (JSON, WithStatus) import Servant qualified hiding (WithStatus) -import Servant.Swagger +import Servant.OpenApi import Wire.API.ApplyMods import Wire.API.Conversation import Wire.API.Conversation.Role @@ -445,7 +445,7 @@ type IFederationAPI = :> Get '[Servant.JSON] FederationStatus ) -swaggerDoc :: Swagger +swaggerDoc :: OpenApi swaggerDoc = - toSwagger (Proxy @InternalAPI) + toOpenApi (Proxy @InternalAPI) & info . title .~ "Wire-Server internal galley API" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley/ConversationsIntra.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/ConversationsIntra.hs index cd81ed7473e..b644906cd95 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley/ConversationsIntra.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/ConversationsIntra.hs @@ -26,9 +26,9 @@ where import Data.Aeson qualified as A import Data.Aeson.Types (FromJSON, ToJSON) import Data.Id (ConvId, UserId) +import Data.OpenApi qualified as Swagger import Data.Qualified import Data.Schema -import Data.Swagger qualified as Swagger import Imports data DesiredMembership = Included | Excluded diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamFeatureNoConfigMulti.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamFeatureNoConfigMulti.hs index fdb40b05aec..9f96c0b024c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamFeatureNoConfigMulti.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamFeatureNoConfigMulti.hs @@ -24,8 +24,8 @@ where import Data.Aeson qualified as A import Data.Id +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.API.Team.Feature qualified as Public diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamsIntra.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamsIntra.hs index 09432560ea5..0bc3ae5a593 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamsIntra.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamsIntra.hs @@ -31,8 +31,8 @@ import Control.Lens ((?~)) import Data.Aeson import Data.Currency qualified as Currency import Data.Json.Util +import Data.OpenApi qualified as Swagger import Data.Schema qualified as S -import Data.Swagger qualified as Swagger import Data.Time (UTCTime) import Imports import Test.QuickCheck.Arbitrary (Arbitrary) diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs b/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs index 69d114dca82..ffde2e561c3 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs @@ -19,11 +19,12 @@ module Wire.API.Routes.Internal.LegalHold where import Control.Lens import Data.Id +import Data.OpenApi (OpenApi) +import Data.OpenApi.Lens import Data.Proxy -import Data.Swagger import Imports import Servant.API hiding (Header, WithStatus) -import Servant.Swagger +import Servant.OpenApi import Wire.API.Team.Feature type InternalLegalHoldAPI = @@ -38,7 +39,7 @@ type InternalLegalHoldAPI = :> Put '[] NoContent ) -swaggerDoc :: Swagger +swaggerDoc :: OpenApi swaggerDoc = - toSwagger (Proxy @InternalLegalHoldAPI) + toOpenApi (Proxy @InternalLegalHoldAPI) & info . title .~ "Wire-Server internal legalhold API" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs index 63f2358f5e1..8cc2207031c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs @@ -19,10 +19,10 @@ module Wire.API.Routes.Internal.Spar where import Control.Lens import Data.Id -import Data.Swagger +import Data.OpenApi import Imports import Servant -import Servant.Swagger +import Servant.OpenApi import Wire.API.User import Wire.API.User.Saml @@ -34,7 +34,7 @@ type InternalAPI = :<|> "scim" :> "userinfos" :> ReqBody '[JSON] UserSet :> Post '[JSON] ScimUserInfos ) -swaggerDoc :: Swagger +swaggerDoc :: OpenApi swaggerDoc = - toSwagger (Proxy @InternalAPI) + toOpenApi (Proxy @InternalAPI) & info . title .~ "Wire-Server internal spar API" diff --git a/libs/wire-api/src/Wire/API/Routes/LowLevelStream.hs b/libs/wire-api/src/Wire/API/Routes/LowLevelStream.hs index d9287bd5fa9..f39080b54f7 100644 --- a/libs/wire-api/src/Wire/API/Routes/LowLevelStream.hs +++ b/libs/wire-api/src/Wire/API/Routes/LowLevelStream.hs @@ -17,13 +17,13 @@ module Wire.API.Routes.LowLevelStream where -import Control.Lens (at, (.~), (?~)) +import Control.Lens (at, (.~), (?~), _Just) import Data.ByteString.Char8 as B8 import Data.CaseInsensitive qualified as CI import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap import Data.Metrics.Servant +import Data.OpenApi qualified as S import Data.Proxy -import Data.Swagger qualified as S import Data.Text qualified as Text import GHC.TypeLits import Imports @@ -33,10 +33,10 @@ import Network.Wai import Servant.API import Servant.API.ContentTypes import Servant.API.Status +import Servant.OpenApi as S +import Servant.OpenApi.Internal as S import Servant.Server hiding (respond) import Servant.Server.Internal -import Servant.Swagger as S -import Servant.Swagger.Internal as S import Wire.API.Routes.Version -- FUTUREWORK: make it possible to generate headers at runtime @@ -90,27 +90,30 @@ type instance LowLevelStream m s h d t instance - (Accept ctype, KnownNat status, KnownSymbol desc, SwaggerMethod method) => - HasSwagger (LowLevelStream method status headers desc ctype) + (S.ToSchema ctype, Accept ctype, KnownNat status, KnownSymbol desc, OpenApiMethod method) => + HasOpenApi (LowLevelStream method status headers desc ctype) where - toSwagger _ = + toOpenApi _ = mempty & S.paths . at "/" ?~ ( mempty & method ?~ ( mempty - & S.produces ?~ S.MimeList [contentType (Proxy @ctype)] & S.responses . S.responses .~ fmap S.Inline responses ) ) where - method = S.swaggerMethod (Proxy @method) + method = S.openApiMethod (Proxy @method) responses = InsOrdHashMap.singleton (fromIntegral (natVal (Proxy @status))) $ mempty & S.description .~ Text.pack (symbolVal (Proxy @desc)) + & S.content + .~ InsOrdHashMap.singleton + (contentType $ Proxy @ctype) + (mempty & S.schema . _Just . S._Inline .~ S.toSchema (Proxy @ctype)) instance RoutesToPaths (LowLevelStream method status headers desc ctype) where getRoutes = [] diff --git a/libs/wire-api/src/Wire/API/Routes/MultiTablePaging.hs b/libs/wire-api/src/Wire/API/Routes/MultiTablePaging.hs index e0438210f77..0fc48cdaf06 100644 --- a/libs/wire-api/src/Wire/API/Routes/MultiTablePaging.hs +++ b/libs/wire-api/src/Wire/API/Routes/MultiTablePaging.hs @@ -26,10 +26,10 @@ where import Control.Lens ((?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Kind +import Data.OpenApi qualified as S import Data.Proxy import Data.Range import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import GHC.TypeLits import Imports @@ -77,7 +77,10 @@ deriving via deriving via Schema (GetMultiTablePageRequest name tables max def) instance - RequestSchemaConstraint name tables max def => S.ToSchema (GetMultiTablePageRequest name tables max def) + ( Typeable tables, + RequestSchemaConstraint name tables max def + ) => + S.ToSchema (GetMultiTablePageRequest name tables max def) instance RequestSchemaConstraint name tables max def => ToSchema (GetMultiTablePageRequest name tables max def) where schema = @@ -126,7 +129,7 @@ deriving via deriving via (Schema (MultiTablePage name resultsKey tables a)) instance - PageSchemaConstraints name resultsKey tables a => + (Typeable tables, Typeable a, PageSchemaConstraints name resultsKey tables a) => S.ToSchema (MultiTablePage name resultsKey tables a) instance diff --git a/libs/wire-api/src/Wire/API/Routes/MultiTablePaging/State.hs b/libs/wire-api/src/Wire/API/Routes/MultiTablePaging/State.hs index 197e44a959b..7d43b3009be 100644 --- a/libs/wire-api/src/Wire/API/Routes/MultiTablePaging/State.hs +++ b/libs/wire-api/src/Wire/API/Routes/MultiTablePaging/State.hs @@ -26,9 +26,9 @@ import Data.Attoparsec.ByteString qualified as AB import Data.ByteString qualified as BS import Data.ByteString.Base64.URL qualified as Base64Url import Data.Either.Combinators (mapLeft) +import Data.OpenApi qualified as S import Data.Proxy import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import GHC.TypeLits diff --git a/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs b/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs index db16fb8fc01..ed24bbfdbe5 100644 --- a/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs +++ b/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs @@ -41,6 +41,7 @@ module Wire.API.Routes.MultiVerb ResponseType, IsResponse (..), IsSwaggerResponse (..), + IsSwaggerResponseList (..), simpleResponseSwagger, combineResponseSwagger, ResponseTypes, @@ -54,18 +55,18 @@ import Control.Lens hiding (Context, (<|)) import Data.ByteString.Builder import Data.ByteString.Lazy qualified as LBS import Data.CaseInsensitive qualified as CI -import Data.Containers.ListUtils import Data.Either.Combinators (leftToMaybe) -import Data.HashMap.Strict.InsOrd (InsOrdHashMap) +import Data.HashMap.Strict.InsOrd (InsOrdHashMap, unionWith) import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap import Data.Kind import Data.Metrics.Servant +import Data.OpenApi hiding (HasServer, Response, contentType) +import Data.OpenApi qualified as S +import Data.OpenApi.Declare qualified as S import Data.Proxy import Data.SOP import Data.Sequence (Seq, (<|), pattern (:<|)) import Data.Sequence qualified as Seq -import Data.Swagger qualified as S -import Data.Swagger.Declare qualified as S import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import Data.Typeable @@ -82,10 +83,10 @@ import Servant.API.ContentTypes import Servant.API.Status (KnownStatus (..)) import Servant.Client import Servant.Client.Core hiding (addHeader) +import Servant.OpenApi as S +import Servant.OpenApi.Internal as S import Servant.Server import Servant.Server.Internal -import Servant.Swagger as S -import Servant.Swagger.Internal as S import Servant.Types.SourceT type Declare = S.Declare (S.Definitions S.Schema) @@ -191,19 +192,25 @@ instance (AllMimeRender cs a, AllMimeUnrender cs a, KnownStatus s) => IsResponse Nothing -> empty Just f -> either UnrenderError UnrenderSuccess (f (responseBody output)) -simpleResponseSwagger :: forall a desc. (S.ToSchema a, KnownSymbol desc) => Declare S.Response +simpleResponseSwagger :: forall a cs desc. (S.ToSchema a, KnownSymbol desc, AllMime cs) => Declare S.Response simpleResponseSwagger = do ref <- S.declareSchemaRef (Proxy @a) + let resps :: InsOrdHashMap M.MediaType MediaTypeObject + resps = InsOrdHashMap.fromList $ (,MediaTypeObject (pure ref) Nothing mempty mempty) <$> cs pure $ mempty & S.description .~ Text.pack (symbolVal (Proxy @desc)) - & S.schema ?~ ref + & S.content .~ resps + where + cs :: [M.MediaType] + cs = allMime $ Proxy @cs instance (KnownSymbol desc, S.ToSchema a) => IsSwaggerResponse (Respond s desc a) where - responseSwagger = simpleResponseSwagger @a @desc + -- Defaulting this to JSON, as openapi3 needs something to map a schema against. + responseSwagger = simpleResponseSwagger @a @'[JSON] @desc type instance ResponseType (RespondAs ct s desc a) = a @@ -248,10 +255,10 @@ instance KnownStatus s => IsResponse cs (RespondAs '() s desc ()) where guard (responseStatusCode output == statusVal (Proxy @s)) instance - (KnownSymbol desc, S.ToSchema a) => + (KnownSymbol desc, S.ToSchema a, Accept ct) => IsSwaggerResponse (RespondAs (ct :: Type) s desc a) where - responseSwagger = simpleResponseSwagger @a @desc + responseSwagger = simpleResponseSwagger @a @'[ct] @desc instance (KnownSymbol desc) => @@ -348,8 +355,8 @@ instance -- FUTUREWORK: should we concatenate all the matching headers instead of just -- taking the first one? extractHeaders hs = do - let name = headerName @name - (hs0, hs1) = Seq.partition (\(h, _) -> h == name) hs + let name' = headerName @name + (hs0, hs1) = Seq.partition (\(h, _) -> h == name') hs x <- case hs0 of Seq.Empty -> empty ((_, h) :<| _) -> either (const empty) pure (parseHeader h) @@ -378,11 +385,11 @@ instance (KnownSymbol name, KnownSymbol desc, S.ToParamSchema a) => ToResponseHeader (DescHeader name desc a) where - toResponseHeader _ = (name, S.Header (Just desc) sch) + toResponseHeader _ = (name', S.Header (Just desc) Nothing Nothing Nothing Nothing Nothing mempty sch) where - name = Text.pack (symbolVal (Proxy @name)) + name' = Text.pack (symbolVal (Proxy @name)) desc = Text.pack (symbolVal (Proxy @desc)) - sch = S.toParamSchema (Proxy @a) + sch = pure $ Inline $ S.toParamSchema (Proxy @a) instance ToResponseHeader h => ToResponseHeader (OptHeader h) where toResponseHeader _ = toResponseHeader (Proxy @h) @@ -419,7 +426,7 @@ instance where responseSwagger = fmap - (S.headers .~ toAllResponseHeaders (Proxy @hs)) + (S.headers .~ fmap S.Inline (toAllResponseHeaders (Proxy @hs))) (responseSwagger @r) class IsSwaggerResponseList as where @@ -477,7 +484,17 @@ combineResponseSwagger :: S.Response -> S.Response -> S.Response combineResponseSwagger r1 r2 = r1 & S.description <>~ ("\n\n" <> r2 ^. S.description) - & S.schema . _Just . S._Inline %~ flip combineSwaggerSchema (r2 ^. S.schema . _Just . S._Inline) + & S.content %~ flip (unionWith combineMediaTypeObject) (r2 ^. S.content) + +combineMediaTypeObject :: S.MediaTypeObject -> S.MediaTypeObject -> S.MediaTypeObject +combineMediaTypeObject m1 m2 = + m1 & S.schema .~ merge (m1 ^. S.schema) (m2 ^. S.schema) + where + merge Nothing a = a + merge a Nothing = a + merge (Just (Inline a)) (Just (Inline b)) = pure $ Inline $ combineSwaggerSchema a b + merge a@(Just (Ref _)) _ = a + merge _ a@(Just (Ref _)) = a combineSwaggerSchema :: S.Schema -> S.Schema -> S.Schema combineSwaggerSchema s1 s2 @@ -698,44 +715,61 @@ instance fromUnion (S (S x)) = case x of {} instance - (SwaggerMethod method, IsSwaggerResponseList as) => - S.HasSwagger (MultiVerb method '() as r) + (OpenApiMethod method, IsSwaggerResponseList as) => + S.HasOpenApi (MultiVerb method '() as r) where - toSwagger _ = + toOpenApi _ = mempty - & S.definitions <>~ defs + & S.components . S.schemas <>~ defs & S.paths . at "/" ?~ ( mempty & method ?~ ( mempty - & S.responses . S.responses .~ fmap S.Inline responses + & S.responses . S.responses .~ refResps ) ) where - method = S.swaggerMethod (Proxy @method) - (defs, responses) = S.runDeclare (responseListSwagger @as) mempty + method = S.openApiMethod (Proxy @method) + (defs, resps) = S.runDeclare (responseListSwagger @as) mempty + refResps = S.Inline <$> resps instance - (SwaggerMethod method, IsSwaggerResponseList as, AllMime cs) => - S.HasSwagger (MultiVerb method (cs :: [Type]) as r) + (OpenApiMethod method, IsSwaggerResponseList as, AllMime cs) => + S.HasOpenApi (MultiVerb method (cs :: [Type]) as r) where - toSwagger _ = + toOpenApi _ = mempty - & S.definitions <>~ defs + & S.components . S.schemas <>~ defs & S.paths . at "/" ?~ ( mempty & method ?~ ( mempty - & S.produces ?~ S.MimeList (nubOrd cs) - & S.responses . S.responses .~ fmap S.Inline responses + & S.responses . S.responses .~ refResps ) ) where - method = S.swaggerMethod (Proxy @method) + method = S.openApiMethod (Proxy @method) + -- This has our content types. cs = allMime (Proxy @cs) - (defs, responses) = S.runDeclare (responseListSwagger @as) mempty + -- This has our schemas + (defs, resps) = S.runDeclare (responseListSwagger @as) mempty + -- We need to zip them together, and stick it all back into the contentMap + -- Since we have a single schema per type, and are only changing the content-types, + -- we should be able to pick a schema out of the resps' map, and then use it for + -- all of the values of cs + addMime :: S.Response -> S.Response + addMime resp = + resp + & S.content + %~ + -- pick out an element from the map, if any exist. + -- These will all have the same schemas, and we are reapplying the content types. + foldMap (\c -> InsOrdHashMap.fromList $ (,c) <$> cs) + . listToMaybe + . toList + refResps = S.Inline . addMime <$> resps class Typeable a => IsWaiBody a where responseToWai :: ResponseF a -> Wai.Response diff --git a/libs/wire-api/src/Wire/API/Routes/Named.hs b/libs/wire-api/src/Wire/API/Routes/Named.hs index 79136daebfa..f76ada19664 100644 --- a/libs/wire-api/src/Wire/API/Routes/Named.hs +++ b/libs/wire-api/src/Wire/API/Routes/Named.hs @@ -22,14 +22,15 @@ module Wire.API.Routes.Named where import Control.Lens ((%~)) import Data.Kind import Data.Metrics.Servant +import Data.OpenApi.Lens hiding (HasServer) +import Data.OpenApi.Operation import Data.Proxy -import Data.Swagger import GHC.TypeLits import Imports import Servant import Servant.Client import Servant.Client.Core (clientIn) -import Servant.Swagger +import Servant.OpenApi -- | See http://docs.wire.com/developer/developer/servant.html#named-and-internal-route-ids-in-swagger newtype Named name x = Named {unnamed :: x} @@ -46,9 +47,9 @@ instance {-# OVERLAPPABLE #-} KnownSymbol a => RenderableSymbol a where instance {-# OVERLAPPING #-} (RenderableSymbol a, RenderableSymbol b) => RenderableSymbol '(a, b) where renderSymbol = "(" <> (renderSymbol @a) <> ", " <> (renderSymbol @b) <> ")" -instance (HasSwagger api, RenderableSymbol name) => HasSwagger (Named name api) where - toSwagger _ = - toSwagger (Proxy @api) +instance (HasOpenApi api, RenderableSymbol name) => HasOpenApi (Named name api) where + toOpenApi _ = + toOpenApi (Proxy @api) & allOperations . description %~ (Just (dscr <> "\n\n") <>) where dscr :: Text diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index a9d5ab6646b..68886e65407 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -44,18 +44,19 @@ import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap import Data.Id as Id import Data.Kind import Data.Metrics.Servant +import Data.OpenApi hiding (HasServer, Header, Server) +import Data.OpenApi qualified as S import Data.Qualified -import Data.Swagger hiding (Header) import GHC.Base (Symbol) import GHC.TypeLits (KnownSymbol) import Imports hiding (All, head) import Network.Wai qualified as Wai import Servant hiding (Handler, JSON, addHeader, respond) import Servant.API.Modifiers +import Servant.OpenApi (HasOpenApi (toOpenApi)) import Servant.Server.Internal.Delayed import Servant.Server.Internal.DelayedIO import Servant.Server.Internal.Router (Router) -import Servant.Swagger (HasSwagger (toSwagger)) import Wire.API.OAuth qualified as OAuth import Wire.API.Routes.Version @@ -223,15 +224,15 @@ type ZHostValue = Text type ZOptHostHeader = Header' '[Servant.Optional, Strict] "Z-Host" ZHostValue -instance HasSwagger api => HasSwagger (ZHostOpt :> api) where - toSwagger _ = toSwagger (Proxy @api) +instance HasOpenApi api => HasOpenApi (ZHostOpt :> api) where + toOpenApi _ = toOpenApi (Proxy @api) type instance SpecialiseToVersion v (ZHostOpt :> api) = ZHostOpt :> SpecialiseToVersion v api -addZAuthSwagger :: Swagger -> Swagger +addZAuthSwagger :: OpenApi -> OpenApi addZAuthSwagger s = s - & securityDefinitions <>~ SecurityDefinitions (InsOrdHashMap.singleton "ZAuth" secScheme) + & S.components . S.securitySchemes <>~ SecurityDefinitions (InsOrdHashMap.singleton "ZAuth" secScheme) & security <>~ [SecurityRequirement $ InsOrdHashMap.singleton "ZAuth" []] where secScheme = @@ -244,11 +245,11 @@ type instance SpecialiseToVersion v (ZAuthServant t opts :> api) = ZAuthServant t opts :> SpecialiseToVersion v api -instance HasSwagger api => HasSwagger (ZAuthServant 'ZAuthUser _opts :> api) where - toSwagger _ = addZAuthSwagger (toSwagger (Proxy @api)) +instance HasOpenApi api => HasOpenApi (ZAuthServant 'ZAuthUser _opts :> api) where + toOpenApi _ = addZAuthSwagger (toOpenApi (Proxy @api)) -instance HasSwagger api => HasSwagger (ZAuthServant 'ZLocalAuthUser opts :> api) where - toSwagger _ = addZAuthSwagger (toSwagger (Proxy @api)) +instance HasOpenApi api => HasOpenApi (ZAuthServant 'ZLocalAuthUser opts :> api) where + toOpenApi _ = addZAuthSwagger (toOpenApi (Proxy @api)) instance HasLink endpoint => HasLink (ZAuthServant usr opts :> endpoint) where type MkLink (ZAuthServant _ _ :> endpoint) a = MkLink endpoint a @@ -256,10 +257,10 @@ instance HasLink endpoint => HasLink (ZAuthServant usr opts :> endpoint) where instance {-# OVERLAPPABLE #-} - HasSwagger api => - HasSwagger (ZAuthServant ztype _opts :> api) + HasOpenApi api => + HasOpenApi (ZAuthServant ztype _opts :> api) where - toSwagger _ = toSwagger (Proxy @api) + toOpenApi _ = toOpenApi (Proxy @api) instance ( HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, @@ -301,8 +302,8 @@ instance checkType :: Maybe ByteString -> Wai.Request -> DelayedIO () checkType token req = case (token, lookup "Z-Type" (Wai.requestHeaders req)) of - (Just t, value) - | value /= Just t -> + (Just t, v) + | v /= Just t -> delayedFail ServerError { errHTTPCode = 403, @@ -321,7 +322,7 @@ instance RoutesToPaths api => RoutesToPaths (ZHostOpt :> api) where getRoutes = getRoutes @api -- FUTUREWORK: Make a PR to the servant-swagger package with this instance -instance ToSchema a => ToSchema (Headers ls a) where +instance (Typeable ls, ToSchema a) => ToSchema (Headers ls a) where declareNamedSchema _ = declareNamedSchema (Proxy @a) data DescriptionOAuthScope (scope :: OAuth.OAuthScope) @@ -331,12 +332,12 @@ type instance DescriptionOAuthScope scope :> SpecialiseToVersion v api instance - (HasSwagger api, OAuth.IsOAuthScope scope) => - HasSwagger (DescriptionOAuthScope scope :> api) + (HasOpenApi api, OAuth.IsOAuthScope scope) => + HasOpenApi (DescriptionOAuthScope scope :> api) where - toSwagger _ = addScopeDescription @scope (toSwagger (Proxy @api)) + toOpenApi _ = addScopeDescription @scope (toOpenApi (Proxy @api)) -addScopeDescription :: forall scope. OAuth.IsOAuthScope scope => Swagger -> Swagger +addScopeDescription :: forall scope. OAuth.IsOAuthScope scope => OpenApi -> OpenApi addScopeDescription = allOperations . description %~ Just . (<> "\nOAuth scope: `" <> cs (toByteString (OAuth.toOAuthScope @scope)) <> "`") . fold instance (HasServer api ctx) => HasServer (DescriptionOAuthScope scope :> api) ctx where 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 88fd1189a4b..cdb94f58e3b 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -19,6 +19,7 @@ module Wire.API.Routes.Public.Brig where +import Control.Lens ((?~)) import Data.Aeson qualified as A (FromJSON, ToJSON, Value) import Data.ByteString.Conversion import Data.Code (Timeout) @@ -28,20 +29,21 @@ import Data.Handle import Data.Id as Id import Data.Misc (IpAddr) import Data.Nonce (Nonce) +import Data.OpenApi hiding (Contact, Header, Schema, ToSchema) +import Data.OpenApi qualified as S import Data.Qualified (Qualified (..)) import Data.Range import Data.SOP import Data.Schema as Schema -import Data.Swagger hiding (Contact, Header, Schema, ToSchema) -import Data.Swagger qualified as S import Generics.SOP qualified as GSOP import Imports hiding (head) import Network.Wai.Utilities import Servant (JSON) import Servant hiding (Handler, JSON, addHeader, respond) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Call.Config (RTCConfiguration) import Wire.API.Connection hiding (MissingLegalholdConsent) +import Wire.API.Deprecated import Wire.API.Error import Wire.API.Error.Brig import Wire.API.Error.Empty @@ -568,6 +570,7 @@ type AccountAPI = :<|> Named "post-password-reset-key-deprecated" ( Summary "Complete a password reset." + :> Deprecated :> CanThrow 'PasswordResetInProgress :> CanThrow 'InvalidPasswordResetKey :> CanThrow 'InvalidPasswordResetCode @@ -581,6 +584,7 @@ type AccountAPI = :<|> Named "onboarding" ( Summary "Upload contacts and invoke matching." + :> Deprecated :> Description "DEPRECATED: the feature has been turned off, the end-point does \ \nothing and always returns '{\"results\":[],\"auto-connects\":[]}'." @@ -602,8 +606,9 @@ data DeprecatedMatchingResult = DeprecatedMatchingResult instance ToSchema DeprecatedMatchingResult where schema = - object + objectWithDocModifier "DeprecatedMatchingResult" + (S.deprecated ?~ True) $ DeprecatedMatchingResult <$ const [] .= field "results" (array (null_ @SwaggerDoc)) @@ -1412,8 +1417,9 @@ type CallingAPI = Named "get-calls-config" ( Summary - "[deprecated] Retrieve TURN server addresses and credentials for \ - \ IP addresses, scheme `turn` and transport `udp` only" + "Retrieve TURN server addresses and credentials for \ + \ IP addresses, scheme `turn` and transport `udp` only (deprecated)" + :> Deprecated :> ZUser :> ZConn :> "calls" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs index 285202fb993..7e3259d8fca 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs @@ -22,7 +22,7 @@ import Data.Id as Id import Imports import Servant (JSON) import Servant hiding (Handler, JSON, Tagged, addHeader, respond) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Conversation.Bot import Wire.API.Error (CanThrow, ErrorResponse) import Wire.API.Error.Brig (BrigError (..)) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs index 0a4adf52401..a096c78d975 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs @@ -22,7 +22,7 @@ import Data.SOP import Imports hiding (exp, head) import Servant (JSON) import Servant hiding (Handler, JSON, Tagged, addHeader, respond) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.OAuth import Wire.API.Routes.API diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs b/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs index 1ce9dd600cc..a1dc8001504 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs @@ -24,7 +24,7 @@ import Data.Qualified import Data.SOP import Imports import Servant -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import URI.ByteString import Wire.API.Asset import Wire.API.Error diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs index d24c473738e..52ec0ee5022 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs @@ -21,7 +21,7 @@ module Wire.API.Routes.Public.Galley where import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.Bot import Wire.API.Routes.Public.Galley.Conversation diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs index 2c4752fda43..3eb711a96c8 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs @@ -18,7 +18,7 @@ module Wire.API.Routes.Public.Galley.Bot where import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Galley import Wire.API.MakesFederatedCall diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index e37ef51f6b6..3f0456d7014 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -24,12 +24,13 @@ import Data.Range import Data.SOP (I (..), NS (..)) import Imports hiding (head) import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Conversation import Wire.API.Conversation.Code import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Conversation.Typing +import Wire.API.Deprecated import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Event.Conversation @@ -923,6 +924,7 @@ type ConversationAPI = :<|> Named "update-other-member-unqualified" ( Summary "Update membership of the specified user (deprecated)" + :> Deprecated :> Description "Use `PUT /conversations/:cnv_domain/:cnv/members/:usr_domain/:usr` instead" :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" @@ -975,6 +977,7 @@ type ConversationAPI = :<|> Named "update-conversation-name-deprecated" ( Summary "Update conversation name (deprecated)" + :> Deprecated :> Description "Use `/conversations/:domain/:conv/name` instead." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" @@ -996,6 +999,7 @@ type ConversationAPI = :<|> Named "update-conversation-name-unqualified" ( Summary "Update conversation name (deprecated)" + :> Deprecated :> Description "Use `/conversations/:domain/:conv/name` instead." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" @@ -1041,6 +1045,7 @@ type ConversationAPI = :<|> Named "update-conversation-message-timer-unqualified" ( Summary "Update the message timer for a conversation (deprecated)" + :> Deprecated :> Description "Use `/conversations/:domain/:cnv/message-timer` instead." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" @@ -1088,6 +1093,7 @@ type ConversationAPI = :<|> Named "update-conversation-receipt-mode-unqualified" ( Summary "Update receipt mode for a conversation (deprecated)" + :> Deprecated :> Description "Use `PUT /conversations/:domain/:cnv/receipt-mode` instead." :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-mls-message-sent" @@ -1214,6 +1220,7 @@ type ConversationAPI = :<|> Named "get-conversation-self-unqualified" ( Summary "Get self membership properties (deprecated)" + :> Deprecated :> ZLocalUser :> "conversations" :> Capture' '[Description "Conversation ID"] "cnv" ConvId @@ -1223,6 +1230,7 @@ type ConversationAPI = :<|> Named "update-conversation-self-unqualified" ( Summary "Update self membership properties (deprecated)" + :> Deprecated :> Description "Use `/conversations/:domain/:conv/self` instead." :> CanThrow 'ConvNotFound :> ZLocalUser diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/CustomBackend.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/CustomBackend.hs index 079858baa0e..607a6e62573 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/CustomBackend.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/CustomBackend.hs @@ -19,7 +19,7 @@ module Wire.API.Routes.Public.Galley.CustomBackend where import Data.Domain import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.CustomBackend import Wire.API.Error import Wire.API.Error.Galley diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs index f1e406a20c3..a59144b926d 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs @@ -20,7 +20,7 @@ module Wire.API.Routes.Public.Galley.Feature where import Data.Id import GHC.TypeLits import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.ApplyMods import Wire.API.Conversation.Role import Wire.API.Error 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 2cbe78027fd..f04ad6c3e70 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 @@ -21,7 +21,7 @@ import Data.Id import GHC.Generics import Generics.SOP qualified as GSOP import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley 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 d5ee97aefe4..6266979e8c3 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 @@ -18,7 +18,7 @@ module Wire.API.Routes.Public.Galley.MLS where import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Galley import Wire.API.MLS.CommitBundle diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs index b5f07834e7f..72aa70f4125 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs @@ -22,7 +22,7 @@ import Data.SOP import Generics.SOP qualified as GSOP import Imports import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Brig qualified as BrigError import Wire.API.Error.Galley 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 3d3571dd23c..fd3fd392a4a 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 @@ -20,7 +20,7 @@ module Wire.API.Routes.Public.Galley.Team where import Data.Id import Imports import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Routes.MultiVerb diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs index 9eae7ea3ac1..0f45c2ac92c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs @@ -19,7 +19,7 @@ module Wire.API.Routes.Public.Galley.TeamConversation where import Data.Id import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs index 6d14ebc1483..4c71df03e49 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs @@ -23,7 +23,7 @@ import Data.Range import GHC.Generics import Generics.SOP qualified as GSOP import Servant hiding (WithStatus) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Routes.CSV diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs index 59b98ddc9eb..107ed1de9a5 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs @@ -25,11 +25,12 @@ import SAML2.WebSSO qualified as SAML import Servant import Servant.API.Extended import Servant.Multipart -import Servant.Swagger +import Servant.OpenApi import URI.ByteString qualified as URI import Web.Scim.Capabilities.MetaSchema as Scim.Meta import Web.Scim.Class.Auth as Scim.Auth import Web.Scim.Class.User as Scim.User +import Wire.API.Deprecated (Deprecated) import Wire.API.Error import Wire.API.Error.Brig import Wire.API.Routes.API @@ -57,7 +58,7 @@ type DeprecateSSOAPIV1 = \Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams" type APISSO = - DeprecateSSOAPIV1 :> "metadata" :> SAML.APIMeta + DeprecateSSOAPIV1 :> Deprecated :> "metadata" :> SAML.APIMeta :<|> "metadata" :> Capture "team" TeamId :> SAML.APIMeta :<|> "initiate-login" :> APIAuthReqPrecheck :<|> "initiate-login" :> APIAuthReq @@ -82,6 +83,7 @@ type APIAuthReq = type APIAuthRespLegacy = DeprecateSSOAPIV1 + :> Deprecated :> "finalize-login" -- (SAML.APIAuthResp from here on, except for response) :> MultipartForm Mem SAML.AuthnResponseBody @@ -191,4 +193,4 @@ data SparAPITag instance ServiceAPI SparAPITag v where type ServiceAPIRoutes SparAPITag = SparAPI type SpecialisedAPIRoutes v SparAPITag = SparAPI - serviceSwagger = toSwagger (Proxy @SparAPI) + serviceSwagger = toOpenApi (Proxy @SparAPI) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Util.hs b/libs/wire-api/src/Wire/API/Routes/Public/Util.hs index 694230f7574..ab34186bb12 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Util.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Util.hs @@ -23,7 +23,7 @@ module Wire.API.Routes.Public.Util where import Control.Comonad import Data.SOP (I (..), NS (..)) import Servant -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Routes.MultiVerb instance diff --git a/libs/wire-api/src/Wire/API/Routes/QualifiedCapture.hs b/libs/wire-api/src/Wire/API/Routes/QualifiedCapture.hs index f54cccf4f36..9147e008fda 100644 --- a/libs/wire-api/src/Wire/API/Routes/QualifiedCapture.hs +++ b/libs/wire-api/src/Wire/API/Routes/QualifiedCapture.hs @@ -24,16 +24,16 @@ where import Data.Domain import Data.Kind import Data.Metrics.Servant +import Data.OpenApi hiding (HasServer, value) import Data.Qualified -import Data.Swagger import GHC.TypeLits import Imports import Servant import Servant.API.Description import Servant.API.Modifiers import Servant.Client.Core.HasClient +import Servant.OpenApi import Servant.Server.Internal.ErrorFormatter -import Servant.Swagger import Wire.API.Routes.Version -- | Capture a value qualified by a domain, with modifiers. @@ -56,16 +56,15 @@ type instance QualifiedCapture' mods capture a :> SpecialiseToVersion v api instance - ( Typeable a, - ToParamSchema a, - HasSwagger api, + ( ToParamSchema a, + HasOpenApi api, KnownSymbol capture, KnownSymbol (AppendSymbol capture "_domain"), KnownSymbol (FoldDescription mods) ) => - HasSwagger (QualifiedCapture' mods capture a :> api) + HasOpenApi (QualifiedCapture' mods capture a :> api) where - toSwagger _ = toSwagger (Proxy @(WithDomain mods capture a api)) + toOpenApi _ = toOpenApi (Proxy @(WithDomain mods capture a api)) instance ( KnownSymbol capture, diff --git a/libs/wire-api/src/Wire/API/Routes/Version.hs b/libs/wire-api/src/Wire/API/Routes/Version.hs index ea8bfc26f8b..a2a337b61df 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version.hs @@ -52,15 +52,16 @@ import Data.Binary.Builder qualified as Builder import Data.ByteString.Conversion (ToByteString (builder), toByteString') import Data.ByteString.Lazy qualified as LBS import Data.Domain +import Data.OpenApi qualified as S import Data.Schema import Data.Singletons.Base.TH -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Encoding as Text import GHC.TypeLits import Imports import Servant import Servant.API.Extended.RawM +import Wire.API.Deprecated import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.VersionInfo @@ -230,6 +231,10 @@ type instance SpecialiseToVersion v (Summary s :> api) = Summary s :> SpecialiseToVersion v api +type instance + SpecialiseToVersion v (Deprecated :> api) = + Deprecated :> SpecialiseToVersion v api + type instance SpecialiseToVersion v (Verb m s t r) = Verb m s t r diff --git a/libs/wire-api/src/Wire/API/Routes/Versioned.hs b/libs/wire-api/src/Wire/API/Routes/Versioned.hs index 1ca7bac0587..7707e3441e6 100644 --- a/libs/wire-api/src/Wire/API/Routes/Versioned.hs +++ b/libs/wire-api/src/Wire/API/Routes/Versioned.hs @@ -20,15 +20,15 @@ module Wire.API.Routes.Versioned where import Data.Aeson (FromJSON, ToJSON) import Data.Kind import Data.Metrics.Servant +import Data.OpenApi qualified as S import Data.Schema import Data.Singletons -import Data.Swagger qualified as S import GHC.TypeLits import Imports import Servant import Servant.API.ContentTypes -import Servant.Swagger -import Servant.Swagger.Internal +import Servant.OpenApi +import Servant.OpenApi.Internal import Wire.API.Routes.MultiVerb import Wire.API.Routes.Version @@ -63,12 +63,12 @@ type instance instance ( S.ToSchema (Versioned v a), - HasSwagger api, + HasOpenApi api, AllAccept cts ) => - HasSwagger (VersionedReqBody v cts a :> api) + HasOpenApi (VersionedReqBody v cts a :> api) where - toSwagger _ = toSwagger (Proxy @(ReqBody cts (Versioned v a) :> api)) + toOpenApi _ = toOpenApi (Proxy @(ReqBody cts (Versioned v a) :> api)) -------------------------------------------------------------------------------- -- Versioned responses @@ -92,7 +92,7 @@ instance (KnownSymbol desc, S.ToSchema a) => IsSwaggerResponse (VersionedRespond v s desc a) where - responseSwagger = simpleResponseSwagger @a @desc + responseSwagger = simpleResponseSwagger @a @'[JSON] @desc ------------------------------------------------------------------------------- -- Versioned newtype wrapper @@ -111,7 +111,7 @@ deriving via Schema (Versioned v a) instance ToSchema (Versioned v a) => FromJSO deriving via Schema (Versioned v a) instance ToSchema (Versioned v a) => ToJSON (Versioned v a) -- add version suffix to swagger schema to prevent collisions -instance (SingI v, ToSchema (Versioned v a)) => S.ToSchema (Versioned v a) where +instance (SingI v, ToSchema (Versioned v a), Typeable a, Typeable v) => S.ToSchema (Versioned v a) where declareNamedSchema _ = do S.NamedSchema n s <- schemaToSwagger (Proxy @(Versioned v a)) pure $ S.NamedSchema (fmap (<> toUrlPiece (demote @v)) n) s diff --git a/libs/wire-api/src/Wire/API/Routes/WebSocket.hs b/libs/wire-api/src/Wire/API/Routes/WebSocket.hs index 72354d95bc1..0405b58d094 100644 --- a/libs/wire-api/src/Wire/API/Routes/WebSocket.hs +++ b/libs/wire-api/src/Wire/API/Routes/WebSocket.hs @@ -21,16 +21,16 @@ import Control.Lens import Control.Monad.Trans.Resource import Data.HashMap.Strict.InsOrd import Data.Metrics.Servant +import Data.OpenApi hiding (HasServer) import Data.Proxy -import Data.Swagger import Imports import Network.Wai.Handler.WebSockets import Network.WebSockets +import Servant.OpenApi import Servant.Server hiding (respond) import Servant.Server.Internal.Delayed import Servant.Server.Internal.RouteResult import Servant.Server.Internal.Router -import Servant.Swagger import Wire.API.Routes.Version -- | A websocket that relates to a 'PendingConnection' @@ -65,8 +65,8 @@ instance HasServer WebSocketPending ctx where type instance SpecialiseToVersion v WebSocketPending = WebSocketPending -instance HasSwagger WebSocketPending where - toSwagger _ = +instance HasOpenApi WebSocketPending where + toOpenApi _ = mempty & paths . at "/" @@ -82,7 +82,7 @@ instance HasSwagger WebSocketPending where ) ) where - resps :: InsOrdHashMap HttpStatusCode (Referenced Data.Swagger.Response) + resps :: InsOrdHashMap HttpStatusCode (Referenced Data.OpenApi.Response) resps = mempty & at 101 ?~ Inline (mempty & description .~ "Connection upgraded.") diff --git a/libs/wire-api/src/Wire/API/ServantProto.hs b/libs/wire-api/src/Wire/API/ServantProto.hs index 3eb06458fab..6e2dbd6140b 100644 --- a/libs/wire-api/src/Wire/API/ServantProto.hs +++ b/libs/wire-api/src/Wire/API/ServantProto.hs @@ -19,7 +19,7 @@ module Wire.API.ServantProto where import Data.ByteString.Lazy qualified as LBS import Data.List.NonEmpty (NonEmpty (..)) -import Data.Swagger +import Data.OpenApi import Imports import Network.HTTP.Media ((//)) import Servant diff --git a/libs/wire-api/src/Wire/API/SwaggerHelper.hs b/libs/wire-api/src/Wire/API/SwaggerHelper.hs index 9e1927156c1..3e882b8ab5c 100644 --- a/libs/wire-api/src/Wire/API/SwaggerHelper.hs +++ b/libs/wire-api/src/Wire/API/SwaggerHelper.hs @@ -19,23 +19,33 @@ module Wire.API.SwaggerHelper where import Control.Lens import Data.Containers.ListUtils (nubOrd) -import Data.Swagger hiding (Contact, Header, Schema, ToSchema) -import Data.Swagger qualified as S +import Data.HashMap.Strict.InsOrd +import Data.OpenApi hiding (Contact, Header, Schema, ToSchema) +import Data.OpenApi qualified as S +import Data.Text qualified as T import Imports hiding (head) -cleanupSwagger :: Swagger -> Swagger +cleanupSwagger :: OpenApi -> OpenApi cleanupSwagger = (S.security %~ nub) -- sanitise definitions - . (S.definitions . traverse %~ sanitise) + . (S.components . S.schemas . traverse %~ sanitise) + -- strip the default errors + . ( S.allOperations + . S.responses + . S.responses + %~ foldrWithKey stripDefaultErrors mempty + ) -- sanitise general responses - . (S.responses . traverse . S.schema . _Just . S._Inline %~ sanitise) + . (S.components . S.responses . traverse . S.content . traverse . S.schema . _Just . S._Inline %~ sanitise) -- sanitise all responses of all paths . ( S.allOperations . S.responses . S.responses . traverse . S._Inline + . S.content + . traverse . S.schema . _Just . S._Inline @@ -47,3 +57,49 @@ cleanupSwagger = (S.properties . traverse . S._Inline %~ sanitise) . (S.required %~ nubOrd) . (S.enum_ . _Just %~ nub) + -- servant-openapi and servant-swagger both insert default responses with codes 404 and 400. + -- They have a simple structure that we can match against, and remove from the final structure. + stripDefaultErrors :: HttpStatusCode -> Referenced Response -> Responses' -> Responses' + stripDefaultErrors code resp resps = + case code of + 400 -> case resp ^? _Inline . S.description of + (Just desc) -> + if "Invalid " + `T.isPrefixOf` desc + && resp + ^? _Inline + . links + == pure mempty + && resp + ^? _Inline + . content + == pure mempty + && resp + ^? _Inline + . headers + == pure mempty + then resps + else insert code resp resps + Nothing -> insert code resp resps + 404 -> case resp ^? _Inline . S.description of + (Just desc) -> + if " not found" + `T.isSuffixOf` desc + && resp + ^? _Inline + . links + == pure mempty + && resp + ^? _Inline + . content + == pure mempty + && resp + ^? _Inline + . headers + == pure mempty + then resps + else insert code resp resps + Nothing -> insert code resp resps + _ -> insert code resp resps + +type Responses' = InsOrdHashMap HttpStatusCode (Referenced Response) diff --git a/libs/wire-api/src/Wire/API/SwaggerServant.hs b/libs/wire-api/src/Wire/API/SwaggerServant.hs index 89973fb59ae..5c3918cf39c 100644 --- a/libs/wire-api/src/Wire/API/SwaggerServant.hs +++ b/libs/wire-api/src/Wire/API/SwaggerServant.hs @@ -25,7 +25,7 @@ import Data.Metrics.Servant import Data.Proxy import Imports hiding (head) import Servant -import Servant.Swagger (HasSwagger (toSwagger)) +import Servant.OpenApi (HasOpenApi (toOpenApi)) -- | A type-level tag that lets us omit any branch from Swagger docs. -- @@ -34,8 +34,8 @@ import Servant.Swagger (HasSwagger (toSwagger)) -- it's only justification is laziness. data OmitDocs -instance HasSwagger (OmitDocs :> a) where - toSwagger _ = mempty +instance HasOpenApi (OmitDocs :> a) where + toOpenApi _ = mempty instance HasServer api ctx => HasServer (OmitDocs :> api) ctx where type ServerT (OmitDocs :> api) m = ServerT api m diff --git a/libs/wire-api/src/Wire/API/SystemSettings.hs b/libs/wire-api/src/Wire/API/SystemSettings.hs index d6098ac4ec5..d07d7152a44 100644 --- a/libs/wire-api/src/Wire/API/SystemSettings.hs +++ b/libs/wire-api/src/Wire/API/SystemSettings.hs @@ -19,10 +19,10 @@ module Wire.API.SystemSettings where import Control.Lens hiding ((.=)) import Data.Aeson qualified as A +import Data.OpenApi qualified as S import Data.Schema as Schema -import Data.Swagger qualified as S import Imports -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Test.QuickCheck import Wire.Arbitrary diff --git a/libs/wire-api/src/Wire/API/Team.hs b/libs/wire-api/src/Wire/API/Team.hs index fe3ed5e596c..13c09ab567b 100644 --- a/libs/wire-api/src/Wire/API/Team.hs +++ b/libs/wire-api/src/Wire/API/Team.hs @@ -67,7 +67,7 @@ module Wire.API.Team ) where -import Control.Lens (makeLenses, (?~)) +import Control.Lens (makeLenses, over, (?~)) import Data.Aeson (FromJSON, ToJSON, Value (..)) import Data.Aeson.Types (Parser) import Data.Attoparsec.ByteString qualified as Atto (Parser, string) @@ -76,9 +76,10 @@ import Data.ByteString.Conversion import Data.Code qualified as Code import Data.Id (TeamId, UserId) import Data.Misc (PlainTextPassword6) +import Data.OpenApi (HasDeprecated (deprecated)) +import Data.OpenApi qualified as S import Data.Range import Data.Schema -import Data.Swagger qualified as S import Data.Text.Encoding qualified as T import Imports import Test.QuickCheck.Gen (suchThat) @@ -118,7 +119,10 @@ instance ToSchema Team where <*> _teamSplashScreen .= (fromMaybe DefaultIcon <$> optField "splash_screen" schema) where desc = description ?~ "`binding` is deprecated, and should be ignored. The non-binding teams API is not used (and will not be supported from API version V4 onwards), and `binding` will always be `true`." - bindingDesc = description ?~ "Deprecated, please ignore." + bindingDesc v = + v + & description ?~ "Deprecated, please ignore." + & deprecated ?~ True -- | How a team "binds" its members (users) -- @@ -145,8 +149,9 @@ data TeamBinding instance ToSchema TeamBinding where schema = - enum @Bool "TeamBinding" $ - mconcat [element True Binding, element False NonBinding] + over doc (deprecated ?~ True) $ + enum @Bool "TeamBinding" $ + mconcat [element True Binding, element False NonBinding] -------------------------------------------------------------------------------- -- TeamList diff --git a/libs/wire-api/src/Wire/API/Team/Conversation.hs b/libs/wire-api/src/Wire/API/Team/Conversation.hs index ae020086104..3822a614923 100644 --- a/libs/wire-api/src/Wire/API/Team/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Team/Conversation.hs @@ -35,8 +35,8 @@ where import Control.Lens (makeLenses, (?~)) import Data.Aeson qualified as A import Data.Id (ConvId) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.Arbitrary (Arbitrary, GenericUniform (..)) diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index ee21a46af6c..c8b1e00c205 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -100,10 +100,10 @@ import Data.Id import Data.Json.Util import Data.Kind import Data.Misc (HttpsUrl) +import Data.OpenApi qualified as S import Data.Proxy import Data.Schema import Data.Scientific (toBoundedInteger) -import Data.Swagger qualified as S import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.Text.Lazy qualified as TL @@ -274,7 +274,7 @@ deriving via (Schema (WithStatus cfg)) instance (ToSchema (WithStatus cfg)) => T deriving via (Schema (WithStatus cfg)) instance (ToSchema (WithStatus cfg)) => FromJSON (WithStatus cfg) -deriving via (Schema (WithStatus cfg)) instance (ToSchema (WithStatus cfg)) => S.ToSchema (WithStatus cfg) +deriving via (Schema (WithStatus cfg)) instance (ToSchema (WithStatus cfg), Typeable cfg) => S.ToSchema (WithStatus cfg) instance (ToSchema cfg, IsFeatureConfig cfg) => ToSchema (WithStatus cfg) where schema = @@ -304,7 +304,7 @@ deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch cfg)) => FromJSON (WithStatusPatch cfg) -deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch cfg)) => S.ToSchema (WithStatusPatch cfg) +deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch cfg), Typeable cfg) => S.ToSchema (WithStatusPatch cfg) wsPatch :: Maybe FeatureStatus -> Maybe LockStatus -> Maybe cfg -> Maybe FeatureTTL -> WithStatusPatch cfg wsPatch = WithStatusBase @@ -1096,8 +1096,8 @@ data FeatureStatus instance S.ToParamSchema FeatureStatus where toParamSchema _ = mempty - { S._paramSchemaType = Just S.SwaggerString, - S._paramSchemaEnum = Just (A.String . toQueryParam <$> [(minBound :: FeatureStatus) ..]) + { S._schemaType = Just S.OpenApiString, + S._schemaEnum = Just (A.String . toQueryParam <$> [(minBound :: FeatureStatus) ..]) } instance FromHttpApiData FeatureStatus where diff --git a/libs/wire-api/src/Wire/API/Team/Invitation.hs b/libs/wire-api/src/Wire/API/Team/Invitation.hs index 8593c67ce97..44cc508ab69 100644 --- a/libs/wire-api/src/Wire/API/Team/Invitation.hs +++ b/libs/wire-api/src/Wire/API/Team/Invitation.hs @@ -32,9 +32,9 @@ import Data.Aeson qualified as A import Data.ByteString.Conversion import Data.Id import Data.Json.Util +import Data.OpenApi qualified as S import Data.SOP import Data.Schema -import Data.Swagger qualified as S import Data.Text.Encoding qualified as TE import Imports import Servant (FromHttpApiData (..), ToHttpApiData (..)) @@ -130,7 +130,7 @@ newtype InvitationLocation = InvitationLocation instance S.ToParamSchema InvitationLocation where toParamSchema _ = mempty - & S.type_ ?~ S.SwaggerString + & S.type_ ?~ S.OpenApiString & S.format ?~ "url" instance FromHttpApiData InvitationLocation where diff --git a/libs/wire-api/src/Wire/API/Team/LegalHold.hs b/libs/wire-api/src/Wire/API/Team/LegalHold.hs index d72dafb5da8..40fbb9a7af0 100644 --- a/libs/wire-api/src/Wire/API/Team/LegalHold.hs +++ b/libs/wire-api/src/Wire/API/Team/LegalHold.hs @@ -35,9 +35,9 @@ import Data.Aeson.Types qualified as A import Data.Id import Data.LegalHold import Data.Misc +import Data.OpenApi qualified as S hiding (info) import Data.Proxy import Data.Schema -import Data.Swagger qualified as S hiding (info) import Deriving.Aeson import Imports import Wire.API.Provider @@ -240,11 +240,11 @@ instance ToSchema LegalholdProtectee where pure $ S.NamedSchema (Just "LegalholdProtectee") $ mempty - & S.type_ ?~ S.SwaggerObject + & S.type_ ?~ S.OpenApiObject & S.properties . at "tag" ?~ S.Inline ( mempty - & S.type_ ?~ S.SwaggerString + & S.type_ ?~ S.OpenApiString & S.enum_ ?~ [ A.toJSON ("ProtectedUser" :: String), A.toJSON ("UnprotectedBot" :: String), diff --git a/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs b/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs index ea892087bfe..8dc5fd14366 100644 --- a/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs +++ b/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs @@ -34,7 +34,7 @@ where import Data.Aeson hiding (fieldLabelModifier) import Data.Id import Data.Json.Util ((#)) -import Data.Swagger +import Data.OpenApi import Imports import Wire.API.User.Client.Prekey import Wire.Arbitrary (Arbitrary, GenericUniform (..)) diff --git a/libs/wire-api/src/Wire/API/Team/LegalHold/Internal.hs b/libs/wire-api/src/Wire/API/Team/LegalHold/Internal.hs index cb3915f4a38..e706f472fc6 100644 --- a/libs/wire-api/src/Wire/API/Team/LegalHold/Internal.hs +++ b/libs/wire-api/src/Wire/API/Team/LegalHold/Internal.hs @@ -29,8 +29,8 @@ import Data.Aeson import Data.Id import Data.Json.Util import Data.Misc +import Data.OpenApi qualified as Swagger import Data.Schema qualified as Schema -import Data.Swagger qualified as Swagger import Imports import Wire.API.Provider import Wire.API.Provider.Service diff --git a/libs/wire-api/src/Wire/API/Team/Member.hs b/libs/wire-api/src/Wire/API/Team/Member.hs index d47061389b5..91e790aa66e 100644 --- a/libs/wire-api/src/Wire/API/Team/Member.hs +++ b/libs/wire-api/src/Wire/API/Team/Member.hs @@ -74,10 +74,10 @@ import Data.Json.Util import Data.Kind import Data.LegalHold (UserLegalHoldStatus (..), defUserLegalHoldStatus) import Data.Misc (PlainTextPassword6) +import Data.OpenApi (ToParamSchema (..)) +import Data.OpenApi.Schema qualified as S import Data.Proxy import Data.Schema -import Data.Swagger (ToParamSchema (..)) -import Data.Swagger.Schema qualified as S import GHC.TypeLits import Imports import Wire.API.Routes.MultiTablePaging (MultiTablePage (..)) @@ -132,7 +132,7 @@ deriving via deriving via (Schema (TeamMember' tag)) instance - (ToSchema (TeamMember' tag)) => + (ToSchema (TeamMember' tag), Typeable tag) => S.ToSchema (TeamMember' tag) mkTeamMember :: @@ -256,7 +256,7 @@ deriving via deriving via (Schema (TeamMemberList' tag)) instance - ToSchema (TeamMemberList' tag) => + (ToSchema (TeamMemberList' tag), Typeable tag) => S.ToSchema (TeamMemberList' tag) newTeamMemberList :: [TeamMember] -> ListType -> TeamMemberList @@ -348,7 +348,7 @@ deriving via deriving via (Schema (NewTeamMember' tag)) instance - (ToSchema (NewTeamMember' tag)) => + (ToSchema (NewTeamMember' tag), Typeable tag) => S.ToSchema (NewTeamMember' tag) deriving via (GenericUniform NewTeamMember) instance Arbitrary NewTeamMember diff --git a/libs/wire-api/src/Wire/API/Team/Permission.hs b/libs/wire-api/src/Wire/API/Team/Permission.hs index be29d6b46d1..49a9893b370 100644 --- a/libs/wire-api/src/Wire/API/Team/Permission.hs +++ b/libs/wire-api/src/Wire/API/Team/Permission.hs @@ -48,10 +48,10 @@ import Control.Error.Util qualified as Err import Control.Lens (makeLenses, (?~), (^.)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Bits (testBit, (.|.)) +import Data.OpenApi qualified as S import Data.Schema import Data.Set qualified as Set import Data.Singletons.Base.TH -import Data.Swagger qualified as S import Imports import Wire.API.Util.Aeson (CustomEncoded (..)) import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) diff --git a/libs/wire-api/src/Wire/API/Team/Role.hs b/libs/wire-api/src/Wire/API/Team/Role.hs index 424065e66c0..d4602394750 100644 --- a/libs/wire-api/src/Wire/API/Team/Role.hs +++ b/libs/wire-api/src/Wire/API/Team/Role.hs @@ -29,8 +29,8 @@ import Control.Lens ((?~)) import Data.Aeson import Data.Attoparsec.ByteString.Char8 (string) import Data.ByteString.Conversion (FromByteString (..), ToByteString (..)) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as T import Imports import Servant.API (FromHttpApiData, parseQueryParam) @@ -93,7 +93,7 @@ instance ToSchema Role where instance S.ToParamSchema Role where toParamSchema _ = mempty - & S.type_ ?~ S.SwaggerString + & S.type_ ?~ S.OpenApiString & S.enum_ ?~ fmap roleName [minBound .. maxBound] instance FromHttpApiData Role where diff --git a/libs/wire-api/src/Wire/API/Team/SearchVisibility.hs b/libs/wire-api/src/Wire/API/Team/SearchVisibility.hs index b41300a8b7a..76d530f6f15 100644 --- a/libs/wire-api/src/Wire/API/Team/SearchVisibility.hs +++ b/libs/wire-api/src/Wire/API/Team/SearchVisibility.hs @@ -24,8 +24,8 @@ module Wire.API.Team.SearchVisibility where import Control.Lens ((?~)) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Deriving.Aeson import Imports import Wire.Arbitrary (Arbitrary, GenericUniform (..)) diff --git a/libs/wire-api/src/Wire/API/Team/Size.hs b/libs/wire-api/src/Wire/API/Team/Size.hs index 811a7a094e6..ce0d8fe6468 100644 --- a/libs/wire-api/src/Wire/API/Team/Size.hs +++ b/libs/wire-api/src/Wire/API/Team/Size.hs @@ -22,8 +22,8 @@ where import Control.Lens ((?~)) import Data.Aeson qualified as A +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Numeric.Natural diff --git a/libs/wire-api/src/Wire/API/Unreachable.hs b/libs/wire-api/src/Wire/API/Unreachable.hs index baf37558eff..54055ae6359 100644 --- a/libs/wire-api/src/Wire/API/Unreachable.hs +++ b/libs/wire-api/src/Wire/API/Unreachable.hs @@ -28,9 +28,9 @@ import Data.Aeson qualified as A import Data.Id import Data.List.NonEmpty import Data.List.NonEmpty qualified as NE +import Data.OpenApi qualified as S import Data.Qualified import Data.Schema -import Data.Swagger qualified as S import Imports newtype UnreachableUsers = UnreachableUsers {unreachableUsers :: NonEmpty (Qualified UserId)} diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 443b74dd1bd..7b98eaf0acf 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -178,13 +178,13 @@ import Data.Json.Util (UTCTimeMillis, (#)) import Data.LegalHold (UserLegalHoldStatus) import Data.List.NonEmpty (NonEmpty (..)) import Data.Misc (PlainTextPassword6, PlainTextPassword8) +import Data.OpenApi qualified as S import Data.Qualified import Data.Range import Data.SOP import Data.Schema import Data.Schema qualified as Schema import Data.Set qualified as Set -import Data.Swagger qualified as S import Data.Text qualified as T import Data.Text.Ascii import Data.Text.Encoding qualified as T @@ -1821,7 +1821,7 @@ instance S.ToSchema ListUsersQuery where pure $ S.NamedSchema (Just "ListUsersQuery") $ mempty - & S.type_ ?~ S.SwaggerObject + & S.type_ ?~ S.OpenApiObject & S.description ?~ "exactly one of qualified_ids or qualified_handles must be provided." & S.properties .~ InsOrdHashMap.fromList [("qualified_ids", uids), ("qualified_handles", handles)] & S.example ?~ toJSON (ListUsersByIds [Qualified (Id UUID.nil) (Domain "example.com")]) @@ -1956,8 +1956,8 @@ instance FromByteString VerificationAction where instance S.ToParamSchema VerificationAction where toParamSchema _ = mempty - { S._paramSchemaType = Just S.SwaggerString, - S._paramSchemaEnum = Just (A.String . toQueryParam <$> [(minBound :: VerificationAction) ..]) + { S._schemaType = Just S.OpenApiString, + S._schemaEnum = Just (A.String . toQueryParam <$> [(minBound :: VerificationAction) ..]) } instance FromHttpApiData VerificationAction where diff --git a/libs/wire-api/src/Wire/API/User/Activation.hs b/libs/wire-api/src/Wire/API/User/Activation.hs index 7777b2c25b8..e14b30bc326 100644 --- a/libs/wire-api/src/Wire/API/User/Activation.hs +++ b/libs/wire-api/src/Wire/API/User/Activation.hs @@ -40,9 +40,9 @@ import Data.Aeson qualified as A import Data.Aeson.Types (Parser) import Data.ByteString.Conversion import Data.Data (Proxy (Proxy)) +import Data.OpenApi (ToParamSchema) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger (ToParamSchema) -import Data.Swagger qualified as S import Data.Text.Ascii import Data.Tuple.Extra (fst3, snd3, thd3) import Imports diff --git a/libs/wire-api/src/Wire/API/User/Auth.hs b/libs/wire-api/src/Wire/API/User/Auth.hs index 8670a4bc20e..df15827e2e2 100644 --- a/libs/wire-api/src/Wire/API/User/Auth.hs +++ b/libs/wire-api/src/Wire/API/User/Auth.hs @@ -71,9 +71,9 @@ import Data.Handle (Handle) import Data.Id import Data.Json.Util import Data.Misc (PlainTextPassword6) +import Data.OpenApi qualified as S import Data.SOP import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.Text.Lazy.Encoding qualified as LT @@ -554,7 +554,7 @@ utcToSetCookie c = } instance S.ToParamSchema UserTokenCookie where - toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + toParamSchema _ = mempty & S.type_ ?~ S.OpenApiString instance FromHttpApiData UserTokenCookie where parseHeader = utcFromSetCookie . parseSetCookie diff --git a/libs/wire-api/src/Wire/API/User/Auth/LegalHold.hs b/libs/wire-api/src/Wire/API/User/Auth/LegalHold.hs index 951b2c19ab2..b1f20c416a8 100644 --- a/libs/wire-api/src/Wire/API/User/Auth/LegalHold.hs +++ b/libs/wire-api/src/Wire/API/User/Auth/LegalHold.hs @@ -21,8 +21,8 @@ import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A import Data.Id import Data.Misc +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.API.User.Auth diff --git a/libs/wire-api/src/Wire/API/User/Auth/ReAuth.hs b/libs/wire-api/src/Wire/API/User/Auth/ReAuth.hs index 040698e848a..0892089a90d 100644 --- a/libs/wire-api/src/Wire/API/User/Auth/ReAuth.hs +++ b/libs/wire-api/src/Wire/API/User/Auth/ReAuth.hs @@ -25,8 +25,8 @@ import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A import Data.Code import Data.Misc +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.API.User diff --git a/libs/wire-api/src/Wire/API/User/Auth/Sso.hs b/libs/wire-api/src/Wire/API/User/Auth/Sso.hs index 6e061536e01..0c9daa86859 100644 --- a/libs/wire-api/src/Wire/API/User/Auth/Sso.hs +++ b/libs/wire-api/src/Wire/API/User/Auth/Sso.hs @@ -20,8 +20,8 @@ module Wire.API.User.Auth.Sso where import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A import Data.Id +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.API.User.Auth diff --git a/libs/wire-api/src/Wire/API/User/Client.hs b/libs/wire-api/src/Wire/API/User/Client.hs index 75513f3b761..b168d21633e 100644 --- a/libs/wire-api/src/Wire/API/User/Client.hs +++ b/libs/wire-api/src/Wire/API/User/Client.hs @@ -86,11 +86,11 @@ import Data.Id import Data.Json.Util import Data.Map.Strict qualified as Map import Data.Misc (Latitude (..), Location, Longitude (..), PlainTextPassword6, latitude, location, longitude) +import Data.OpenApi hiding (Schema, ToSchema, nullable, schema) +import Data.OpenApi qualified as Swagger hiding (nullable) import Data.Qualified import Data.Schema import Data.Set qualified as Set -import Data.Swagger hiding (Schema, ToSchema, schema) -import Data.Swagger qualified as Swagger import Data.Text.Encoding qualified as Text.E import Data.Time.Clock import Data.UUID (toASCIIBytes) @@ -371,7 +371,7 @@ instance Swagger.ToSchema UserClientsFull where pure $ NamedSchema (Just "UserClientsFull") $ mempty - & type_ ?~ SwaggerObject + & type_ ?~ OpenApiObject & description ?~ "Dictionary object of `Client` objects indexed by `UserId`." & example ?~ "{\"1355c55a-0ac8-11ee-97ee-db1a6351f093\": , ...}" diff --git a/libs/wire-api/src/Wire/API/User/Client/DPoPAccessToken.hs b/libs/wire-api/src/Wire/API/User/Client/DPoPAccessToken.hs index df719886f37..99ed6e13d92 100644 --- a/libs/wire-api/src/Wire/API/User/Client/DPoPAccessToken.hs +++ b/libs/wire-api/src/Wire/API/User/Client/DPoPAccessToken.hs @@ -22,10 +22,10 @@ module Wire.API.User.Client.DPoPAccessToken where import Data.Aeson (FromJSON, ToJSON) import Data.ByteString.Conversion (FromByteString (..), ToByteString (..), fromByteString', toByteString') +import Data.OpenApi qualified as S +import Data.OpenApi.ParamSchema (ToParamSchema (..)) import Data.SOP import Data.Schema -import Data.Swagger qualified as S -import Data.Swagger.ParamSchema (ToParamSchema (..)) import Data.Text as T import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Imports diff --git a/libs/wire-api/src/Wire/API/User/Client/Prekey.hs b/libs/wire-api/src/Wire/API/User/Client/Prekey.hs index 4f03328465a..f58eaa000ed 100644 --- a/libs/wire-api/src/Wire/API/User/Client/Prekey.hs +++ b/libs/wire-api/src/Wire/API/User/Client/Prekey.hs @@ -35,8 +35,8 @@ where import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Hashable (hash) import Data.Id +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) diff --git a/libs/wire-api/src/Wire/API/User/Handle.hs b/libs/wire-api/src/Wire/API/User/Handle.hs index 08242a6dfe9..3db27ef8c12 100644 --- a/libs/wire-api/src/Wire/API/User/Handle.hs +++ b/libs/wire-api/src/Wire/API/User/Handle.hs @@ -28,10 +28,10 @@ import Control.Applicative import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A import Data.Id (UserId) +import Data.OpenApi qualified as S import Data.Qualified (Qualified (..), deprecatedSchema) import Data.Range import Data.Schema -import Data.Swagger qualified as S import Imports import Wire.Arbitrary (Arbitrary, GenericUniform (..)) diff --git a/libs/wire-api/src/Wire/API/User/Identity.hs b/libs/wire-api/src/Wire/API/User/Identity.hs index c71bec44864..2b88c1d3bd8 100644 --- a/libs/wire-api/src/Wire/API/User/Identity.hs +++ b/libs/wire-api/src/Wire/API/User/Identity.hs @@ -61,9 +61,9 @@ import Data.Attoparsec.Text import Data.Bifunctor (first) import Data.ByteString.Conversion import Data.CaseInsensitive qualified as CI +import Data.OpenApi (ToParamSchema (..)) +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger (ToParamSchema (..)) -import Data.Swagger qualified as S import Data.Text qualified as Text import Data.Text.Encoding (decodeUtf8', encodeUtf8) import Data.Time.Clock @@ -326,7 +326,7 @@ instance S.ToSchema UserSSOId where pure $ S.NamedSchema (Just "UserSSOId") $ mempty - & S.type_ ?~ S.SwaggerObject + & S.type_ ?~ S.OpenApiObject & S.properties .~ [ ("tenant", tenantSchema), ("subject", subjectSchema), diff --git a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs index f45a6f991ea..e954f15c2e6 100644 --- a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs +++ b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs @@ -31,8 +31,8 @@ import Data.ByteString.Conversion qualified as BSC import Data.HashMap.Strict.InsOrd (InsOrdHashMap) import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap import Data.Id (TeamId) +import Data.OpenApi import Data.Proxy (Proxy (Proxy)) -import Data.Swagger import Imports import Network.HTTP.Media ((//)) import SAML2.WebSSO (IdPConfig) @@ -108,9 +108,9 @@ instance ToHttpApiData WireIdPAPIVersion where instance ToParamSchema WireIdPAPIVersion where toParamSchema Proxy = mempty - { _paramSchemaDefault = Just "v2", - _paramSchemaType = Just SwaggerString, - _paramSchemaEnum = Just (String . toQueryParam <$> [(minBound :: WireIdPAPIVersion) ..]) + { _schemaDefault = Just "v2", + _schemaType = Just OpenApiString, + _schemaEnum = Just (String . toQueryParam <$> [(minBound :: WireIdPAPIVersion) ..]) } instance Cql.Cql WireIdPAPIVersion where @@ -205,7 +205,7 @@ instance ToSchema IdPMetadataInfo where & properties .~ properties_ & minProperties ?~ 1 & maxProperties ?~ 1 - & type_ ?~ SwaggerObject + & type_ ?~ OpenApiObject where properties_ :: InsOrdHashMap Text (Referenced Schema) properties_ = diff --git a/libs/wire-api/src/Wire/API/User/Orphans.hs b/libs/wire-api/src/Wire/API/User/Orphans.hs index 05f49534e4f..10ec177a3fe 100644 --- a/libs/wire-api/src/Wire/API/User/Orphans.hs +++ b/libs/wire-api/src/Wire/API/User/Orphans.hs @@ -26,8 +26,8 @@ import Data.Char import Data.Currency qualified as Currency import Data.ISO3166_CountryCodes import Data.LanguageCodes +import Data.OpenApi import Data.Proxy -import Data.Swagger import Data.UUID import Data.X509 as X509 import Imports @@ -35,7 +35,7 @@ import SAML2.WebSSO qualified as SAML import SAML2.WebSSO.Types.TH (deriveJSONOptions) import Servant.API ((:>)) import Servant.Multipart qualified as SM -import Servant.Swagger +import Servant.OpenApi import URI.ByteString deriving instance Generic ISO639_1 @@ -94,7 +94,7 @@ instance ToSchema (SAML.FormRedirect SAML.AuthnRequest) where pure $ NamedSchema (Just "FormRedirect") $ mempty - & type_ ?~ SwaggerObject + & type_ ?~ OpenApiObject & properties . at "uri" ?~ Inline (toSchema (Proxy @Text)) & properties . at "xml" ?~ authnReqSchema @@ -110,8 +110,8 @@ instance ToSchema SAML.SPMetadata where instance ToSchema Void where declareNamedSchema _ = declareNamedSchema (Proxy @String) -instance HasSwagger route => HasSwagger (SM.MultipartForm SM.Mem resp :> route) where - toSwagger _proxy = toSwagger (Proxy @route) +instance HasOpenApi route => HasOpenApi (SM.MultipartForm SM.Mem resp :> route) where + toOpenApi _proxy = toOpenApi (Proxy @route) instance ToSchema SAML.IdPId where declareNamedSchema _ = declareNamedSchema (Proxy @UUID) diff --git a/libs/wire-api/src/Wire/API/User/Password.hs b/libs/wire-api/src/Wire/API/User/Password.hs index 2a6b9bf20ed..4f14e4ca7c6 100644 --- a/libs/wire-api/src/Wire/API/User/Password.hs +++ b/libs/wire-api/src/Wire/API/User/Password.hs @@ -36,11 +36,11 @@ import Data.Aeson qualified as A import Data.Aeson.Types (Parser) import Data.ByteString.Conversion import Data.Misc (PlainTextPassword8) +import Data.OpenApi qualified as S +import Data.OpenApi.ParamSchema import Data.Proxy (Proxy (Proxy)) import Data.Range (Ranged (..)) import Data.Schema as Schema -import Data.Swagger qualified as S -import Data.Swagger.ParamSchema import Data.Text.Ascii import Data.Tuple.Extra (fst3, snd3, thd3) import Imports diff --git a/libs/wire-api/src/Wire/API/User/Profile.hs b/libs/wire-api/src/Wire/API/User/Profile.hs index 8f03b39b375..ae018f20b75 100644 --- a/libs/wire-api/src/Wire/API/User/Profile.hs +++ b/libs/wire-api/src/Wire/API/User/Profile.hs @@ -58,9 +58,9 @@ import Data.Attoparsec.Text import Data.ByteString.Conversion import Data.ISO3166_CountryCodes import Data.LanguageCodes +import Data.OpenApi qualified as S import Data.Range import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Imports import Wire.API.Asset (AssetKey (..)) diff --git a/libs/wire-api/src/Wire/API/User/RichInfo.hs b/libs/wire-api/src/Wire/API/User/RichInfo.hs index ef0eac713e8..32a3db8fa19 100644 --- a/libs/wire-api/src/Wire/API/User/RichInfo.hs +++ b/libs/wire-api/src/Wire/API/User/RichInfo.hs @@ -52,8 +52,8 @@ import Data.CaseInsensitive (CI) import Data.CaseInsensitive qualified as CI import Data.List.Extra (nubOrdOn) import Data.Map qualified as Map +import Data.OpenApi qualified as S import Data.Schema -import Data.Swagger qualified as S import Data.Text qualified as Text import Imports import Test.QuickCheck qualified as QC diff --git a/libs/wire-api/src/Wire/API/User/Saml.hs b/libs/wire-api/src/Wire/API/User/Saml.hs index 8ff2e27c954..09ad0d24367 100644 --- a/libs/wire-api/src/Wire/API/User/Saml.hs +++ b/libs/wire-api/src/Wire/API/User/Saml.hs @@ -30,8 +30,8 @@ import Data.Aeson hiding (fieldLabelModifier) import Data.Aeson.TH hiding (fieldLabelModifier) import Data.ByteString.Builder qualified as Builder import Data.Id (UserId) +import Data.OpenApi import Data.Proxy (Proxy (Proxy)) -import Data.Swagger import Data.Text qualified as T import Data.Time import GHC.TypeLits (KnownSymbol, symbolVal) diff --git a/libs/wire-api/src/Wire/API/User/Scim.hs b/libs/wire-api/src/Wire/API/User/Scim.hs index f3440beea7b..752c608bd85 100644 --- a/libs/wire-api/src/Wire/API/User/Scim.hs +++ b/libs/wire-api/src/Wire/API/User/Scim.hs @@ -59,8 +59,8 @@ import Data.Id (ScimTokenId, TeamId, UserId) import Data.Json.Util ((#)) import Data.Map qualified as Map import Data.Misc (PlainTextPassword6) +import Data.OpenApi hiding (Operation) import Data.Proxy -import Data.Swagger hiding (Operation) import Data.Text.Encoding (encodeUtf8) import Data.Time.Clock (UTCTime) import Imports @@ -462,7 +462,7 @@ instance ToSchema ScimTokenInfo where pure $ NamedSchema (Just "ScimTokenInfo") $ mempty - & type_ ?~ SwaggerObject + & type_ ?~ OpenApiObject & properties .~ [ ("team", teamSchema), ("id", idSchema), @@ -478,7 +478,7 @@ instance ToSchema CreateScimToken where pure $ NamedSchema (Just "CreateScimToken") $ mempty - & type_ ?~ SwaggerObject + & type_ ?~ OpenApiObject & properties .~ [ ("description", textSchema), ("password", textSchema), @@ -493,7 +493,7 @@ instance ToSchema CreateScimTokenResponse where pure $ NamedSchema (Just "CreateScimTokenResponse") $ mempty - & type_ ?~ SwaggerObject + & type_ ?~ OpenApiObject & properties .~ [ ("token", tokenSchema), ("info", infoSchema) @@ -506,7 +506,7 @@ instance ToSchema ScimTokenList where pure $ NamedSchema (Just "ScimTokenList") $ mempty - & type_ ?~ SwaggerObject + & type_ ?~ OpenApiObject & properties .~ [ ("tokens", infoListSchema) ] diff --git a/libs/wire-api/src/Wire/API/User/Search.hs b/libs/wire-api/src/Wire/API/User/Search.hs index 819f0111ab0..deaf7c08f4d 100644 --- a/libs/wire-api/src/Wire/API/User/Search.hs +++ b/libs/wire-api/src/Wire/API/User/Search.hs @@ -43,11 +43,11 @@ import Data.ByteString.Conversion (FromByteString (..), ToByteString (..)) import Data.Either.Combinators (mapLeft) import Data.Id (TeamId, UserId) import Data.Json.Util (UTCTimeMillis) +import Data.OpenApi (ToParamSchema (..)) +import Data.OpenApi qualified as S import Data.Proxy import Data.Qualified import Data.Schema -import Data.Swagger (ToParamSchema (..)) -import Data.Swagger qualified as S import Data.Text qualified as T import Data.Text.Ascii (AsciiBase64Url, toText, validateBase64Url) import Imports @@ -228,7 +228,7 @@ data TeamUserSearchSortBy instance S.ToParamSchema TeamUserSearchSortBy where toParamSchema _ = mempty - & S.type_ ?~ S.SwaggerString + & S.type_ ?~ S.OpenApiString & S.enum_ ?~ fmap teamUserSearchSortByName [minBound .. maxBound] instance ToByteString TeamUserSearchSortBy where @@ -264,7 +264,7 @@ data TeamUserSearchSortOrder instance S.ToParamSchema TeamUserSearchSortOrder where toParamSchema _ = mempty - & S.type_ ?~ S.SwaggerString + & S.type_ ?~ S.OpenApiString & S.enum_ ?~ fmap teamUserSearchSortOrderName [minBound .. maxBound] instance ToByteString TeamUserSearchSortOrder where diff --git a/libs/wire-api/src/Wire/API/UserMap.hs b/libs/wire-api/src/Wire/API/UserMap.hs index bcf41da1559..31f81392195 100644 --- a/libs/wire-api/src/Wire/API/UserMap.hs +++ b/libs/wire-api/src/Wire/API/UserMap.hs @@ -24,9 +24,9 @@ import Data.Aeson (FromJSON, ToJSON (toJSON)) import Data.Domain (Domain) import Data.Id (UserId) import Data.Map qualified as Map +import Data.OpenApi (HasDescription (description), HasExample (example), NamedSchema (..), ToSchema (..), declareSchema, toSchema) import Data.Proxy (Proxy (..)) import Data.Set qualified as Set -import Data.Swagger (HasDescription (description), HasExample (example), NamedSchema (..), ToSchema (..), declareSchema, toSchema) import Data.Text qualified as Text import Data.Typeable (typeRep) import Imports @@ -56,7 +56,7 @@ instance Functor QualifiedUserMap where instance Arbitrary a => Arbitrary (QualifiedUserMap a) where arbitrary = QualifiedUserMap <$> mapOf' arbitrary arbitrary -instance (Typeable a, ToSchema a, ToJSON a, Arbitrary a) => ToSchema (UserMap (Set a)) where +instance (ToSchema a, ToJSON a, Arbitrary a) => ToSchema (UserMap (Set a)) where declareNamedSchema _ = do mapSch <- declareSchema (Proxy @(Map UserId (Set a))) let valueTypeName = Text.pack $ show $ typeRep $ Proxy @a diff --git a/libs/wire-api/src/Wire/API/Wrapped.hs b/libs/wire-api/src/Wire/API/Wrapped.hs index f6d71142a5f..44c41bbddc2 100644 --- a/libs/wire-api/src/Wire/API/Wrapped.hs +++ b/libs/wire-api/src/Wire/API/Wrapped.hs @@ -21,8 +21,8 @@ import Control.Lens ((.~), (?~)) import Data.Aeson import Data.Aeson.Key qualified as Key import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap +import Data.OpenApi import Data.Proxy (Proxy (..)) -import Data.Swagger import Data.Text qualified as Text import GHC.TypeLits (KnownSymbol, Symbol, symbolVal) import Imports @@ -48,7 +48,7 @@ instance (ToSchema a, KnownSymbol name) => ToSchema (Wrapped name a) where pure $ NamedSchema Nothing $ mempty - & type_ ?~ SwaggerObject + & type_ ?~ OpenApiObject & properties .~ InsOrdHashMap.singleton (Text.pack (symbolVal (Proxy @name))) wrappedSchema instance (Arbitrary a, KnownSymbol name) => Arbitrary (Wrapped name a) where 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 79433dbd2d7..aefaa6cb8cd 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 @@ -20,7 +20,7 @@ module Test.Wire.API.Roundtrip.Aeson (tests) where import Data.Aeson (FromJSON, ToJSON, parseJSON, toJSON) import Data.Aeson.Types (parseEither) import Data.Id (ConvId) -import Data.Swagger (ToSchema, validatePrettyToJSON) +import Data.OpenApi (ToSchema, validatePrettyToJSON) import Imports import Test.Tasty qualified as T import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty, (.&&.), (===)) @@ -355,7 +355,7 @@ testRoundTrip = testProperty msg trip testRoundTripWithSwagger :: forall a. - (Arbitrary a, Typeable a, ToJSON a, FromJSON a, ToSchema a, Eq a, Show a) => + (Arbitrary a, ToJSON a, FromJSON a, ToSchema a, Eq a, Show a) => T.TestTree testRoundTripWithSwagger = testProperty msg (trip .&&. scm) where diff --git a/libs/wire-api/test/unit/Test/Wire/API/Swagger.hs b/libs/wire-api/test/unit/Test/Wire/API/Swagger.hs index 8de2cb6ad16..bbb37e6e2a4 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Swagger.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Swagger.hs @@ -18,7 +18,7 @@ module Test.Wire.API.Swagger (tests) where import Data.Aeson (ToJSON) -import Data.Swagger (ToSchema, validatePrettyToJSON) +import Data.OpenApi (ToSchema, validatePrettyToJSON) import Imports import Test.Tasty qualified as T import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty) @@ -56,7 +56,7 @@ tests = testToJSON @(Wrapped.Wrapped "some_user" User.User) ] -testToJSON :: forall a. (Arbitrary a, Typeable a, ToJSON a, ToSchema a, Show a) => T.TestTree +testToJSON :: forall a. (Arbitrary a, ToJSON a, ToSchema a, Show a) => T.TestTree testToJSON = testProperty msg trip where msg = show (typeRep @a) diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 84188ce01b5..312c1c1480c 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -82,6 +82,7 @@ library Wire.API.Conversation.Role Wire.API.Conversation.Typing Wire.API.CustomBackend + Wire.API.Deprecated Wire.API.Error Wire.API.Error.Brig Wire.API.Error.Cannon @@ -285,6 +286,7 @@ library , metrics-wai , mime >=0.4 , mtl + , openapi3 , pem >=0.2 , polysemy , proto-lens @@ -303,13 +305,12 @@ library , servant-client-core , servant-conduit , servant-multipart + , servant-openapi3 , servant-server - , servant-swagger , singletons , singletons-base , singletons-th , sop-core - , swagger2 , tagged , text >=0.11 , time >=1.4 @@ -672,6 +673,7 @@ test-suite wire-api-tests , imports , memory , metrics-wai + , openapi3 , process , QuickCheck , random @@ -679,7 +681,6 @@ test-suite wire-api-tests , schema-profunctor , servant , servant-server - , swagger2 , tasty , tasty-hspec , tasty-hunit diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 291ea06526f..cd56258ac99 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -133,13 +133,6 @@ let sha256 = "sha256-g2lbKt3+hToVFQvaHOa9dg4HqAL7YgReo8fy7wQavmY="; }; }; - swagger2 = { - src = fetchgit { - url = "https://github.com/GetShopTV/swagger2"; - rev = "d79deca03b714cdd4531217831a8305068b2e8f9"; - sha256 = "sha256-R3p0L0TgM0Bspe5z6vauwdPq9TmEWpMC53DBkMtCEoE="; - }; - }; # MR: https://gitlab.com/twittner/cql-io/-/merge_requests/20 cql-io = { src = fetchgit { @@ -180,6 +173,15 @@ let sha256 = "sha256-SKEE9ZqhjBxHYUKQaoB4IpN4/Ui3tS4S98FgZqj7WlY="; }; }; + servant-openapi3 = { + src = fetchgit { + # This is a patched version of the library that sets the required flag for HTTP request bodies. + # A PR for these changes has been made for the upstream library. biocad/servant-openapi3#49 + url = "https://github.com/lepsa/servant-openapi3"; + rev = "5cdb2783f15058f753c41b800415d4ba1149a78b"; + sha256 = "sha256-8FM3IAA3ewCuv9Mar8aWmzbyfKK9eLXIJPMHzmYb1zE="; + }; + }; # This can be removed once postie 0.6.0.3 (or later) is in nixpkgs postie = { src = fetchgit { diff --git a/nix/manual-overrides.nix b/nix/manual-overrides.nix index 31b6b7ce91a..4e4f70ab116 100644 --- a/nix/manual-overrides.nix +++ b/nix/manual-overrides.nix @@ -5,6 +5,7 @@ hself: hsuper: { aeson = (hlib.doJailbreak hsuper.aeson_2_1_2_1); binary-parsers = hlib.markUnbroken (hlib.doJailbreak hsuper.binary-parsers); bytestring-arbitrary = hlib.markUnbroken (hlib.doJailbreak hsuper.bytestring-arbitrary); + openapi3 = hlib.markUnbroken (hlib.dontCheck hsuper.openapi3); cql = hlib.appendPatch (hlib.markUnbroken hsuper.cql) (fetchpatch { url = "https://gitlab.com/twittner/cql/-/merge_requests/11.patch"; sha256 = "sha256-qfcCRkKjSS1TEqPRVBU9Ox2DjsdGsYG/F3DrZ5JGoEI="; @@ -23,7 +24,6 @@ hself: hsuper: { servant-swagger-ui = hlib.doJailbreak hsuper.servant-swagger-ui; servant-swagger-ui-core = hlib.doJailbreak hsuper.servant-swagger-ui-core; sodium-crypto-sign = hlib.addPkgconfigDepend hsuper.sodium-crypto-sign libsodium.dev; - swagger2 = hlib.doJailbreak hsuper.swagger2; text-icu-translit = hlib.markUnbroken (hlib.dontCheck hsuper.text-icu-translit); text-short = hlib.dontCheck hsuper.text-short; type-errors = hlib.dontCheck hsuper.type-errors; diff --git a/services/background-worker/background-worker.cabal b/services/background-worker/background-worker.cabal index 2dc7c897f45..f0c7083d066 100644 --- a/services/background-worker/background-worker.cabal +++ b/services/background-worker/background-worker.cabal @@ -18,7 +18,6 @@ library Wire.BackgroundWorker.Health Wire.BackgroundWorker.Options Wire.BackgroundWorker.Util - Wire.Defederation hs-source-dirs: src default-language: GHC2021 @@ -32,17 +31,13 @@ library , amqp , async , base - , bilge - , bytestring-conversion , containers , exceptions , extended , HsOpenSSL , http-client - , http-types , http2-manager , imports - , lens , metrics-core , metrics-wai , monad-control @@ -175,7 +170,6 @@ test-suite background-worker-test other-modules: Main Test.Wire.BackendNotificationPusherSpec - Test.Wire.DefederationSpec Test.Wire.Util build-depends: @@ -191,7 +185,6 @@ test-suite background-worker-test , http-client , http-media , http-types - , HUnit , imports , prometheus-client , QuickCheck diff --git a/services/background-worker/default.nix b/services/background-worker/default.nix index de2217e45b6..ce67f35095a 100644 --- a/services/background-worker/default.nix +++ b/services/background-worker/default.nix @@ -7,9 +7,7 @@ , amqp , async , base -, bilge , bytestring -, bytestring-conversion , containers , exceptions , extended @@ -21,9 +19,7 @@ , http-media , http-types , http2-manager -, HUnit , imports -, lens , lib , metrics-core , metrics-wai @@ -57,17 +53,13 @@ mkDerivation { amqp async base - bilge - bytestring-conversion containers exceptions extended HsOpenSSL http-client - http-types http2-manager imports - lens metrics-core metrics-wai monad-control @@ -97,7 +89,6 @@ mkDerivation { http-client http-media http-types - HUnit imports prometheus-client QuickCheck diff --git a/services/background-worker/src/Wire/BackgroundWorker.hs b/services/background-worker/src/Wire/BackgroundWorker.hs index 117b135aa6b..7709cb8bd52 100644 --- a/services/background-worker/src/Wire/BackgroundWorker.hs +++ b/services/background-worker/src/Wire/BackgroundWorker.hs @@ -17,31 +17,16 @@ import Wire.BackendNotificationPusher qualified as BackendNotificationPusher import Wire.BackgroundWorker.Env import Wire.BackgroundWorker.Health qualified as Health import Wire.BackgroundWorker.Options -import Wire.Defederation as Defederation --- FUTUREWORK: Start an http service with status and metrics endpoints run :: Opts -> IO () run opts = do (env, syncThread) <- mkEnv opts - (defedChanRef, defedConsumerRef) <- runAppT env $ Defederation.startWorker opts.rabbitmq (notifChanRef, notifConsumersRef) <- runAppT env $ BackendNotificationPusher.startWorker opts.rabbitmq let -- cleanup will run in a new thread when the signal is caught, so we need to use IORefs and -- specific exception types to message threads to clean up l = logger env cleanup = do cancel syncThread - -- Cancel the consumers and wait for them to finish their processing step. - -- Defederation thread - Log.info (logger env) $ Log.msg (Log.val "Cancelling the defederation thread") - readIORef defedChanRef >>= traverse_ \chan -> do - Log.info (logger env) $ Log.msg (Log.val "Got channel") - readIORef defedConsumerRef >>= traverse_ \(consumer, runningFlag) -> do - Log.info l $ Log.msg (Log.val "Cancelling consumer") - Q.cancelConsumer chan consumer - Log.info l $ Log.msg $ Log.val "Taking MVar. Waiting for current operation to finish" - takeMVar runningFlag - Log.info l $ Log.msg $ Log.val "Closing RabbitMQ channel" - Q.closeChannel chan -- Notification pusher thread Log.info (logger env) $ Log.msg (Log.val "Cancelling the notification pusher thread") readIORef notifChanRef >>= traverse_ \chan -> do diff --git a/services/background-worker/src/Wire/BackgroundWorker/Env.hs b/services/background-worker/src/Wire/BackgroundWorker/Env.hs index 86a5b99ed57..37bbaffad01 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Env.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Env.hs @@ -12,7 +12,6 @@ import Data.Map.Strict qualified as Map import Data.Metrics qualified as Metrics import HTTP2.Client.Manager import Imports -import Network.AMQP (Channel) import Network.AMQP.Extended import Network.HTTP.Client import Network.RabbitMqAdmin qualified as RabbitMqAdmin @@ -33,7 +32,6 @@ type IsWorking = Bool -- | Eventually this will be a sum type of all the types of workers data Worker = BackendNotificationPusher - | DefederationWorker deriving (Show, Eq, Ord) data Env = Env @@ -50,10 +48,6 @@ data Env = Env remoteDomains :: IORef FederationDomainConfigs, remoteDomainsChan :: Chan FederationDomainConfigs, backendNotificationMetrics :: BackendNotificationMetrics, - -- This is needed so that the defederation worker can push - -- connection-removed notifications into the notifications channels. - -- This allows us to reuse existing code. This only pushes. - notificationChannel :: MVar Channel, backendNotificationsConfig :: BackendNotificationsConfig, statuses :: IORef (Map Worker IsWorking) } @@ -96,12 +90,10 @@ mkEnv opts = do statuses <- newIORef $ Map.fromList - [ (BackendNotificationPusher, False), - (DefederationWorker, False) + [ (BackendNotificationPusher, False) ] metrics <- Metrics.metrics backendNotificationMetrics <- mkBackendNotificationMetrics - notificationChannel <- mkRabbitMqChannelMVar logger $ demoteOpts opts.rabbitmq let backendNotificationsConfig = opts.backendNotificationPusher pure (Env {..}, syncThread) diff --git a/services/background-worker/src/Wire/BackgroundWorker/Health.hs b/services/background-worker/src/Wire/BackgroundWorker/Health.hs index 26c8374654b..dc0cc0a97d7 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Health.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Health.hs @@ -7,12 +7,13 @@ import Servant.Server.Generic import Wire.BackgroundWorker.Env data HealthAPI routes = HealthAPI - { status :: routes :- "i" :> "status" :> Get '[PlainText] NoContent + { status :: routes :- "i" :> "status" :> Get '[PlainText] NoContent, + statusWorkers :: routes :- "i" :> "status" :> "workers" :> Get '[PlainText] NoContent } deriving (Generic) -statusImpl :: AppT Handler NoContent -statusImpl = do +statusWorkersImpl :: AppT Handler NoContent +statusWorkersImpl = do notWorkingWorkers <- Map.keys . Map.filter not <$> (readIORef =<< asks statuses) if null notWorkingWorkers then pure NoContent @@ -22,4 +23,8 @@ api :: Env -> HealthAPI AsServer api env = fromServant $ hoistServer (Proxy @(ToServant HealthAPI AsApi)) (runAppT env) (toServant apiInAppT) where apiInAppT :: HealthAPI (AsServerT (AppT Handler)) - apiInAppT = HealthAPI {status = statusImpl} + apiInAppT = + HealthAPI + { status = pure NoContent, + statusWorkers = statusWorkersImpl + } diff --git a/services/background-worker/src/Wire/Defederation.hs b/services/background-worker/src/Wire/Defederation.hs deleted file mode 100644 index e8da0e9366b..00000000000 --- a/services/background-worker/src/Wire/Defederation.hs +++ /dev/null @@ -1,143 +0,0 @@ -module Wire.Defederation where - -import Bilge.Retry -import Control.Concurrent.Async -import Control.Lens (to, (^.)) -import Control.Monad.Catch -import Control.Retry -import Data.Aeson qualified as A -import Data.ByteString.Conversion -import Data.Domain -import Data.Text (unpack) -import Data.Text.Encoding -import Imports -import Network.AMQP qualified as Q -import Network.AMQP.Extended -import Network.AMQP.Lifted qualified as QL -import Network.HTTP.Client -import Network.HTTP.Types -import Servant.Client (BaseUrl (..), ClientEnv, Scheme (Http), mkClientEnv) -import System.Logger.Class qualified as Log -import Util.Options -import Util.Options qualified as O -import Wire.API.Federation.BackendNotifications -import Wire.API.Routes.FederationDomainConfig qualified as Fed -import Wire.BackgroundWorker.Env -import Wire.BackgroundWorker.Util - -deleteFederationDomain :: MVar () -> Q.Channel -> AppT IO Q.ConsumerTag -deleteFederationDomain runningFlag chan = do - lift $ ensureQueue chan defederationQueue - QL.consumeMsgs chan (routingKey defederationQueue) Q.Ack $ deleteFederationDomainInner runningFlag - -x3 :: RetryPolicy -x3 = limitRetries 3 <> exponentialBackoff 100000 - --- Exposed for testing purposes so we can decode without further processing the message. -deleteFederationDomainInner' :: (RabbitMQEnvelope e) => (e -> DefederationDomain -> AppT IO ()) -> (Q.Message, e) -> AppT IO () -deleteFederationDomainInner' go (msg, envelope) = do - either - ( \e -> do - void $ logErr e - -- ensure that the message is _NOT_ requeued - -- This means that we won't process this message again - -- as it is unparsable. - liftIO $ reject envelope False - ) - (go envelope) - $ A.eitherDecode @DefederationDomain (Q.msgBody msg) - where - logErr err = - Log.err $ - Log.msg (Log.val "Failed to delete federation domain") - . Log.field "error" err - -mkBrigEnv :: AppT IO ClientEnv -mkBrigEnv = do - Endpoint brigHost brigPort <- asks brig - mkClientEnv - <$> asks httpManager - <*> pure (BaseUrl Http (unpack brigHost) (fromIntegral brigPort) "") - -getRemoteDomains :: AppT IO [Domain] -getRemoteDomains = do - ref <- asks remoteDomains - fmap Fed.domain . Fed.remotes <$> readIORef ref - -callGalleyDelete :: - ( MonadReader Env m, - MonadMask m, - ToByteString a, - RabbitMQEnvelope e, - MonadIO m - ) => - MVar () -> - e -> - a -> - m () -callGalleyDelete runningFlag envelope domain = do - env <- ask - -- Jittered exponential backoff with 10ms as starting delay and 60s as max - -- delay. When 60 is reached, every retry will happen after 60s. - let policy = capDelay 60_000_000 $ fullJitterBackoff 10000 - manager = httpManager env - recovering policy httpHandlers $ \_ -> - bracket_ (takeMVar runningFlag) (putMVar runningFlag ()) $ do - -- Non 2xx responses will throw an exception - -- So we are relying on that to be caught by recovering - resp <- liftIO $ httpLbs (req env domain) manager - let code = statusCode $ responseStatus resp - if code >= 200 && code <= 299 - then do - liftIO $ ack envelope - else -- ensure that the message is requeued - -- This message was able to be parsed but something - -- else in our stack failed and we should try again. - liftIO $ reject envelope True - -req :: ToByteString a => Env -> a -> Request -req env dom = - defaultRequest - { method = methodDelete, - secure = False, - host = galley env ^. O.host . to encodeUtf8, - port = galley env ^. O.port . to fromIntegral, - path = "/i/federation/" <> toByteString' dom, - requestHeaders = ("Accept", "application/json") : requestHeaders defaultRequest, - responseTimeout = defederationTimeout env - } - --- What should we do with non-recoverable (unparsable) errors/messages? --- should we deadletter, or do something else? --- Deadlettering has a privacy implication -- FUTUREWORK. -deleteFederationDomainInner :: RabbitMQEnvelope e => MVar () -> (Q.Message, e) -> AppT IO () -deleteFederationDomainInner runningFlag (msg, envelope) = - deleteFederationDomainInner' (const $ callGalleyDelete runningFlag envelope) (msg, envelope) - -startDefederator :: IORef (Maybe (Q.ConsumerTag, MVar ())) -> Q.Channel -> AppT IO () -startDefederator consumerRef chan = do - markAsWorking DefederationWorker - lift $ Q.qos chan 0 1 False - runningFlag <- newMVar () - consumer <- deleteFederationDomain runningFlag chan - liftIO $ atomicWriteIORef consumerRef $ pure (consumer, runningFlag) - liftIO $ forever $ threadDelay maxBound - -startWorker :: RabbitMqAdminOpts -> AppT IO (IORef (Maybe Q.Channel), IORef (Maybe (Q.ConsumerTag, MVar ()))) -startWorker rabbitmqOpts = do - env <- ask - chanRef <- newIORef Nothing - consumerRef <- newIORef Nothing - let clearRefs = do - runAppT env $ markAsNotWorking DefederationWorker - atomicWriteIORef chanRef Nothing - atomicWriteIORef consumerRef Nothing - void . liftIO . async . openConnectionWithRetries env.logger (demoteOpts rabbitmqOpts) $ - RabbitMqHooks - { onNewChannel = \chan -> do - atomicWriteIORef chanRef $ pure chan - runAppT env $ startDefederator consumerRef chan, - onChannelException = const clearRefs, - onConnectionClose = clearRefs - } - pure (chanRef, consumerRef) diff --git a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs index f38680c1d43..bb37c87fce5 100644 --- a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs +++ b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs @@ -92,44 +92,6 @@ spec = do getVectorWith env.backendNotificationMetrics.pushedCounter getCounter `shouldReturn` [(domainText targetDomain, 1)] - it "should push on-connection-removed notifications" $ do - let returnSuccess _ = pure ("application/json", Aeson.encode EmptyResponse) - let origDomain = Domain "origin.example.com" - targetDomain = Domain "target.example.com" - defederatedDomain = Domain "defederated.example.com" - let notif = - BackendNotification - { targetComponent = Galley, - ownDomain = origDomain, - path = "/on-connection-removed", - body = RawJson $ Aeson.encode defederatedDomain - } - envelope <- newMockEnvelope - let msg = - Q.newMsg - { Q.msgBody = Aeson.encode notif, - Q.msgContentType = Just "application/json" - } - runningFlag <- newMVar () - (env, fedReqs) <- - withTempMockFederator [] returnSuccess . runTestAppT $ do - wait =<< pushNotification runningFlag targetDomain (msg, envelope) - ask - - readIORef envelope.acks `shouldReturn` 1 - readIORef envelope.rejections `shouldReturn` [] - fedReqs - `shouldBe` [ FederatedRequest - { frTargetDomain = targetDomain, - frOriginDomain = origDomain, - frComponent = Galley, - frRPC = "on-connection-removed", - frBody = Aeson.encode defederatedDomain - } - ] - getVectorWith env.backendNotificationMetrics.pushedCounter getCounter - `shouldReturn` [(domainText targetDomain, 1)] - it "should reject invalid notifications" $ do let returnSuccess _ = pure ("application/json", Aeson.encode EmptyResponse) envelope <- newMockEnvelope @@ -222,7 +184,6 @@ spec = do httpManager <- newManager defaultManagerSettings remoteDomains <- newIORef defFederationDomainConfigs remoteDomainsChan <- newChan - notificationChannel <- newEmptyMVar let federatorInternal = Endpoint "localhost" 8097 http2Manager = undefined statuses = undefined @@ -245,7 +206,6 @@ spec = do httpManager <- newManager defaultManagerSettings remoteDomains <- newIORef defFederationDomainConfigs remoteDomainsChan <- newChan - notificationChannel <- newEmptyMVar let federatorInternal = Endpoint "localhost" 8097 http2Manager = undefined statuses = undefined diff --git a/services/background-worker/test/Test/Wire/DefederationSpec.hs b/services/background-worker/test/Test/Wire/DefederationSpec.hs deleted file mode 100644 index 8707414d442..00000000000 --- a/services/background-worker/test/Test/Wire/DefederationSpec.hs +++ /dev/null @@ -1,51 +0,0 @@ -module Test.Wire.DefederationSpec where - -import Data.Aeson qualified as Aeson -import Data.Domain -import Federator.MockServer -import Imports -import Network.AMQP qualified as Q -import Test.HUnit.Lang -import Test.Hspec -import Test.Wire.Util -import Wire.API.Federation.API.Common -import Wire.API.Federation.BackendNotifications -import Wire.BackgroundWorker.Util -import Wire.Defederation - -spec :: Spec -spec = do - describe - "Wire.BackendNotificationPusher.deleteFederationDomain" - $ do - it "should fail on message decoding" $ do - envelope <- newFakeEnvelope - let msg = Q.newMsg {Q.msgBody = Aeson.encode @[()] [], Q.msgContentType = Just "application/json"} - respSuccess _ = pure ("application/json", Aeson.encode EmptyResponse) - resps <- - withTempMockFederator [] respSuccess - . runTestAppT - $ deleteFederationDomainInner' (\e _ -> liftIO $ ack e) (msg, envelope) - case resps of - ((), []) -> pure () - _ -> assertFailure "Expected call to federation" - readIORef envelope.acks `shouldReturn` 0 - -- Fail to decode should not be requeued - readIORef envelope.rejections `shouldReturn` [False] - it "should succeed on message decoding" $ do - envelope <- newFakeEnvelope - let msg = - Q.newMsg - { Q.msgBody = Aeson.encode @DefederationDomain (Domain "far-away.example.com"), - Q.msgContentType = Just "application/json" - } - respSuccess _ = pure ("application/json", Aeson.encode EmptyResponse) - resps <- - withTempMockFederator [] respSuccess - . runTestAppT - $ deleteFederationDomainInner' (\e _ -> liftIO $ ack e) (msg, envelope) - case resps of - ((), []) -> pure () - _ -> assertFailure "Expected call to federation" - readIORef envelope.acks `shouldReturn` 1 - readIORef envelope.rejections `shouldReturn` [] diff --git a/services/background-worker/test/Test/Wire/Util.hs b/services/background-worker/test/Test/Wire/Util.hs index d80121fdc69..58454c9582c 100644 --- a/services/background-worker/test/Test/Wire/Util.hs +++ b/services/background-worker/test/Test/Wire/Util.hs @@ -22,7 +22,6 @@ testEnv = do httpManager <- newManager defaultManagerSettings remoteDomains <- newIORef defFederationDomainConfigs remoteDomainsChan <- newChan - notificationChannel <- newEmptyMVar let federatorInternal = Endpoint "localhost" 0 rabbitmqAdminClient = undefined rabbitmqVHost = undefined diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 6c0834d755e..f89687a6dd8 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -270,6 +270,7 @@ library , mwc-random , network >=2.4 , network-conduit-tls + , openapi3 , optparse-applicative >=0.11 , polysemy , polysemy-plugin @@ -286,15 +287,14 @@ library , schema-profunctor , scientific >=0.3.4 , servant + , servant-openapi3 , servant-server - , servant-swagger , servant-swagger-ui , sodium-crypto-sign >=0.1 , split >=0.2 , ssl-util , statistics >=0.13 , stomp-queue >=0.3 - , swagger2 , template >=0.2 , template-haskell , text >=0.11 diff --git a/services/brig/default.nix b/services/brig/default.nix index ad99f818f63..9e06fdb15e6 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -85,6 +85,7 @@ , network , network-conduit-tls , network-uri +, openapi3 , optparse-applicative , pem , pipes @@ -110,8 +111,8 @@ , servant , servant-client , servant-client-core +, servant-openapi3 , servant-server -, servant-swagger , servant-swagger-ui , sodium-crypto-sign , spar @@ -120,7 +121,6 @@ , statistics , stomp-queue , streaming-commons -, swagger2 , tasty , tasty-cannon , tasty-hunit @@ -235,6 +235,7 @@ mkDerivation { mwc-random network network-conduit-tls + openapi3 optparse-applicative polysemy polysemy-plugin @@ -251,15 +252,14 @@ mkDerivation { schema-profunctor scientific servant + servant-openapi3 servant-server - servant-swagger servant-swagger-ui sodium-crypto-sign split ssl-util statistics stomp-queue - swagger2 template template-haskell text diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 306ea0858c9..35bdfd5c3a3 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -63,9 +63,8 @@ import Brig.User.EJPD qualified import Brig.User.Search.Index qualified as Index import Control.Error hiding (bool) import Control.Lens (view, (^.)) -import Data.Aeson hiding (json) import Data.CommaSeparatedList -import Data.Domain (Domain, domainText) +import Data.Domain (Domain) import Data.Handle import Data.Id as Id import Data.Map.Strict qualified as Map @@ -73,13 +72,11 @@ import Data.Qualified import Data.Set qualified as Set import Data.Time.Clock.System import Imports hiding (head) -import Network.AMQP qualified as Q import Network.Wai.Routing hiding (toList) import Network.Wai.Utilities as Utilities import Polysemy import Servant hiding (Handler, JSON, addHeader, respond) -import Servant.Swagger.Internal.Orphans () -import System.Logger qualified as Lg +import Servant.OpenApi.Internal.Orphans () import System.Logger.Class qualified as Log import System.Random (randomRIO) import UnliftIO.Async @@ -87,7 +84,6 @@ import Wire.API.Connection import Wire.API.Error import Wire.API.Error.Brig qualified as E import Wire.API.Federation.API -import Wire.API.Federation.BackendNotifications import Wire.API.Federation.Error (FederationError (..)) import Wire.API.MLS.CipherSuite import Wire.API.Routes.FederationDomainConfig @@ -211,8 +207,6 @@ federationRemotesAPI = Named @"add-federation-remotes" addFederationRemote :<|> Named @"get-federation-remotes" getFederationRemotes :<|> Named @"update-federation-remotes" updateFederationRemote - :<|> Named @"delete-federation-remotes" deleteFederationRemote - :<|> Named @"delete-federation-remote-from-galley" deleteFederationRemoteGalley addFederationRemote :: FederationDomainConfig -> ExceptT Brig.API.Error.Error (AppT r) () addFederationRemote fedDomConf = do @@ -318,59 +312,6 @@ assertNoDomainsFromConfigFiles dom = do "keeping track of remote domains in the brig config file is deprecated, but as long as we \ \do that, removing or updating items listed in the config file is not allowed." --- | Remove the entry from the database if present (or do nothing if not). This responds with --- 533 if the entry was also present in the config file, but only *after* it has removed the --- entry from cassandra. --- --- The ordering on this delete then check seems weird, but allows us to default all the --- way back to config file state for a federation domain. -deleteFederationRemote :: Domain -> ExceptT Brig.API.Error.Error (AppT r) () -deleteFederationRemote dom = do - lift . wrapClient . Data.deleteFederationRemote $ dom - assertNoDomainsFromConfigFiles dom - env <- ask - ownDomain <- viewFederationDomain - remoteDomains <- fmap domain . remotes <$> getFederationRemotes - for_ (env ^. rabbitmqChannel) $ \chan -> liftIO . withMVar chan $ \chan' -> do - -- ensureQueue uses routingKey internally - ensureQueue chan' defederationQueue - void $ - Q.publishMsg chan' "" queue $ - Q.newMsg - { -- Check that this message type is compatible with what - -- background worker is expecting - Q.msgBody = encode @DefederationDomain dom, - Q.msgDeliveryMode = pure Q.Persistent, - Q.msgContentType = pure "application/json" - } - -- Send a notification to remaining federation servers, telling them - -- that we are defederating from a given domain, and that they should - -- clean up their conversations and notify clients. - -- Just to be safe! - for_ (filter (/= dom) remoteDomains) $ \remoteDomain -> do - ensureQueue chan' $ domainText remoteDomain - liftIO - $ enqueue chan' ownDomain remoteDomain Q.Persistent - . void - $ fedQueueClient @'Galley @"on-connection-removed" dom - -- Drop the notification queue for the domain. - -- This will also drop all of the messages in the queue - -- as we will no longer be able to communicate with this - -- domain. - num <- Q.deleteQueue chan' . routingKey $ domainText dom - Lg.info (env ^. applog) $ Log.msg @String "Dropped Notifications" . Log.field "domain" (domainText dom) . Log.field "count" (show num) - where - -- Ensure that this is kept in sync with background worker - queue = routingKey defederationQueue - --- | Remove one-on-one conversations for the given remote domain. This is called from Galley as --- part of the defederation process, and should not be called during the initial domain removal --- call to brig. This is so we can ensure that domains are correctly cleaned up if a service --- falls over for whatever reason. -deleteFederationRemoteGalley :: Domain -> ExceptT Brig.API.Error.Error (AppT r) () -deleteFederationRemoteGalley dom = do - lift . wrapClient . Data.deleteRemoteConnectionsDomain $ dom - -- | Responds with 'Nothing' if field is NULL in existing user or user does not exist. getAccountConferenceCallingConfig :: UserId -> (Handler r) (ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig) getAccountConferenceCallingConfig uid = diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 22a6e3daa44..169f17c4d66 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -88,9 +88,10 @@ import Data.List.NonEmpty (nonEmpty) import Data.Map.Strict qualified as Map import Data.Misc (IpAddr (..)) import Data.Nonce (Nonce, randomNonce) +import Data.OpenApi qualified as S import Data.Qualified import Data.Range -import Data.Swagger qualified as S +import Data.Schema () import Data.Text qualified as Text import Data.Text.Ascii qualified as Ascii import Data.Text.Lazy (pack) @@ -104,7 +105,7 @@ import Network.Wai.Utilities as Utilities import Polysemy import Servant hiding (Handler, JSON, addHeader, respond) import Servant qualified -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Servant.Swagger.UI import System.Logger.Class qualified as Log import Util.Logging (logFunction, logHandle, logTeam, logUser) @@ -221,7 +222,7 @@ versionedSwaggerDocsAPI Nothing = allroutes (throwError listAllVersionsResp) internalEndpointsSwaggerDocsAPI :: String -> PortNumber -> - S.Swagger -> + S.OpenApi -> Servant.Server (VersionedSwaggerDocsAPIBase service) internalEndpointsSwaggerDocsAPI service examplePort swagger (Just (VersionNumber V5)) = swaggerSchemaUIServer $ diff --git a/services/brig/src/Brig/API/Public/Swagger.hs b/services/brig/src/Brig/API/Public/Swagger.hs index 0f81a74a4d4..e17607cf8f7 100644 --- a/services/brig/src/Brig/API/Public/Swagger.hs +++ b/services/brig/src/Brig/API/Public/Swagger.hs @@ -18,8 +18,8 @@ import Data.Aeson qualified as A import Data.FileEmbed import Data.HashMap.Strict.InsOrd qualified as HM import Data.HashSet.InsOrd qualified as InsOrdSet -import Data.Swagger qualified as S -import Data.Swagger.Declare qualified as S +import Data.OpenApi qualified as S +import Data.OpenApi.Declare qualified as S import Data.Text qualified as T import FileEmbedLzma import GHC.TypeLits @@ -27,7 +27,7 @@ import Imports hiding (head) import Language.Haskell.TH import Network.Socket import Servant -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Servant.Swagger.UI import Wire.API.Event.Conversation qualified import Wire.API.Event.FeatureConfig qualified @@ -68,16 +68,14 @@ swaggerPregenUIServer = . fromMaybe A.Null . A.decode -adjustSwaggerForInternalEndpoint :: String -> PortNumber -> S.Swagger -> S.Swagger +adjustSwaggerForInternalEndpoint :: String -> PortNumber -> S.OpenApi -> S.OpenApi adjustSwaggerForInternalEndpoint service examplePort swagger = swagger & S.info . S.title .~ T.pack ("Wire-Server internal API (" ++ service ++ ")") & S.info . S.description ?~ renderedDescription - & S.host ?~ S.Host "localhost" (Just examplePort) & S.allOperations . S.tags <>~ tag -- Enforce HTTP as the services themselves don't understand HTTPS - & S.schemes ?~ [S.Http] - & S.allOperations . S.schemes ?~ [S.Http] + & S.servers .~ [S.Server ("http://localhost:" <> T.pack (show examplePort)) Nothing mempty] where tag :: InsOrdSet.InsOrdHashSet S.TagName tag = InsOrdSet.singleton @S.TagName (T.pack service) @@ -102,7 +100,7 @@ adjustSwaggerForInternalEndpoint service examplePort swagger = emptySwagger :: Servant.Server (ServiceSwaggerDocsAPIBase a) emptySwagger = swaggerSchemaUIServer $ - mempty @S.Swagger + mempty @S.OpenApi & S.info . S.description ?~ "There is no Swagger documentation for this version. Please refer to v3 or later." diff --git a/services/brig/src/Brig/Federation/Client.hs b/services/brig/src/Brig/Federation/Client.hs index 9e9c9a6b8fc..f0068f64320 100644 --- a/services/brig/src/Brig/Federation/Client.hs +++ b/services/brig/src/Brig/Federation/Client.hs @@ -150,7 +150,9 @@ notifyUserDeleted self remotes = do remoteDomain = tDomain remotes view rabbitmqChannel >>= \case Just chanVar -> do - enqueueNotification (tDomain self) remoteDomain Q.Persistent chanVar $ void $ fedQueueClient @'Brig @"on-user-deleted-connections" notif + enqueueNotification (tDomain self) remoteDomain Q.Persistent chanVar $ + void $ + fedQueueClient @'Brig @"on-user-deleted-connections" notif Nothing -> Log.err $ Log.msg ("Federation error while notifying remote backends of a user deletion." :: ByteString) @@ -159,7 +161,7 @@ notifyUserDeleted self remotes = do . Log.field "error" (show FederationNotConfigured) -- | Enqueues notifications in RabbitMQ. Retries 3 times with a delay of 1s. -enqueueNotification :: (MonadReader Env m, MonadIO m, MonadMask m, Log.MonadLogger m) => Domain -> Domain -> Q.DeliveryMode -> MVar Q.Channel -> FedQueueClient c () -> m () +enqueueNotification :: (MonadIO m, MonadMask m, Log.MonadLogger m) => Domain -> Domain -> Q.DeliveryMode -> MVar Q.Channel -> FedQueueClient c () -> m () enqueueNotification ownDomain remoteDomain deliveryMode chanVar action = do let policy = limitRetries 3 <> constantDelay 1_000_000 recovering policy [logRetries (const $ pure True) logError] (const go) diff --git a/services/brig/src/Brig/User/EJPD.hs b/services/brig/src/Brig/User/EJPD.hs index 07116e81207..fea8e51a37a 100644 --- a/services/brig/src/Brig/User/EJPD.hs +++ b/services/brig/src/Brig/User/EJPD.hs @@ -36,7 +36,7 @@ import Data.Id (UserId) import Data.Set qualified as Set import Imports hiding (head) import Polysemy (Member) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi.Internal.Orphans () import Wire.API.Connection (Relation, RelationWithHistory (..), relationDropHistory) import Wire.API.Push.Token qualified as PushTok import Wire.API.Routes.Internal.Brig.EJPD (EJPDRequestBody (EJPDRequestBody), EJPDResponseBody (EJPDResponseBody), EJPDResponseItem (EJPDResponseItem)) diff --git a/services/brig/test/integration/API/Internal.hs b/services/brig/test/integration/API/Internal.hs index 3d22a98eb57..d69b7c55617 100644 --- a/services/brig/test/integration/API/Internal.hs +++ b/services/brig/test/integration/API/Internal.hs @@ -24,16 +24,14 @@ module API.Internal where import API.Internal.Util -import API.MLS (createClient) +import API.MLS hiding (tests) import API.MLS.Util import Bilge import Bilge.Assert -import Brig.Data.Connection import Brig.Data.User (lookupFeatureConferenceCalling, lookupStatus, userExists) import Brig.Options qualified as Opt import Cassandra qualified as C import Cassandra qualified as Cass -import Cassandra.Exec (x1) import Cassandra.Util import Control.Exception (ErrorCall (ErrorCall), throwIO) import Control.Lens ((^.), (^?!)) @@ -41,17 +39,14 @@ import Data.Aeson.Lens qualified as Aeson import Data.Aeson.Types qualified as Aeson import Data.ByteString.Conversion (toByteString') import Data.Default -import Data.Domain import Data.Id -import Data.Json.Util (toUTCTimeMillis) import Data.Qualified import Data.Set qualified as Set -import Data.Time import GHC.TypeLits (KnownSymbol) import Imports +import System.IO.Temp import Test.Tasty import Test.Tasty.HUnit -import UnliftIO (withSystemTempDirectory) import Util import Util.Options (Endpoint) import Wire.API.Connection qualified as Conn @@ -73,74 +68,9 @@ tests opts mgr db brig brigep gundeck galley = do test mgr "suspend non existing user and verify no db entry" $ testSuspendNonExistingUser db brig, test mgr "mls/clients" $ testGetMlsClients brig, - test mgr "writetimeToInt64" $ testWritetimeRepresentation opts mgr db brig brigep galley, - test mgr "delete-federation-remote-galley" $ testDeleteFederationRemoteGalley db brig + test mgr "writetimeToInt64" $ testWritetimeRepresentation opts mgr db brig brigep galley ] -testDeleteFederationRemoteGalley :: forall m. (TestConstraints m) => Cass.ClientState -> Brig -> m () -testDeleteFederationRemoteGalley db brig = do - let remoteDomain1 = Domain "far-away.example.com" - remoteDomain2 = Domain "far-away-two.example.com" - isRemote1 = (== remoteDomain1) - isRemote2 = (== remoteDomain2) - localUser <- randomUser brig - let localUserId = userId localUser - remoteUserId1 <- randomId - remoteUserId2 <- randomId - now <- liftIO $ getCurrentTime - convId <- randomId - - -- Write the local and remote users into 'connection_remote' - let params1 = (localUserId, remoteDomain1, remoteUserId1, Conn.AcceptedWithHistory, toUTCTimeMillis now, remoteDomain1, convId) - liftIO $ - Cass.runClient db $ - Cass.retry x1 $ - Cass.write remoteConnectionInsert (Cass.params Cass.LocalQuorum params1) - let params2 = (localUserId, remoteDomain2, remoteUserId2, Conn.AcceptedWithHistory, toUTCTimeMillis now, remoteDomain1, convId) - liftIO $ - Cass.runClient db $ - Cass.retry x1 $ - Cass.write remoteConnectionInsert (Cass.params Cass.LocalQuorum params2) - - -- Check that the value exists in the DB as expected. - -- Remote 1 - liftIO - ( Cass.runClient db $ - Cass.retry x1 $ - Cass.query remoteConnectionsSelectUsers (Cass.params Cass.LocalQuorum $ pure localUserId) - ) - >>= liftIO . assertBool "connection_remote entry should exist for the user" . any (isRemote1 . fst) - -- Remote 2 - liftIO - ( Cass.runClient db $ - Cass.retry x1 $ - Cass.query remoteConnectionsSelectUsers (Cass.params Cass.LocalQuorum $ pure localUserId) - ) - >>= liftIO . assertBool "connection_remote entry should exist for the user" . any (isRemote2 . fst) - - -- Make the API call to delete remote domain 1 - delete - ( brig - . paths ["i", "federation", "remote", toByteString' $ domainText remoteDomain1, "galley"] - ) - !!! const 200 === statusCode - - -- Check 'connection_remote' for the local user and ensure - -- that there are no conversations for the remote domain. - liftIO - ( Cass.runClient db $ - Cass.retry x1 $ - Cass.query remoteConnectionsSelectUsers (Cass.params Cass.LocalQuorum $ pure localUserId) - ) - >>= liftIO . assertBool "connection_remote entry should NOT exist for the user" . not . any (isRemote1 . fst) - -- But remote domain 2 is still listed - liftIO - ( Cass.runClient db $ - Cass.retry x1 $ - Cass.query remoteConnectionsSelectUsers (Cass.params Cass.LocalQuorum $ pure localUserId) - ) - >>= liftIO . assertBool "connection_remote entry should exist for the user" . any (isRemote2 . fst) - testSuspendUser :: forall m. (TestConstraints m) => Cass.ClientState -> Brig -> m () testSuspendUser db brig = do user <- randomUser brig diff --git a/services/brig/test/integration/API/MLS.hs b/services/brig/test/integration/API/MLS.hs index a4c4b8b32c6..6c93560b0a9 100644 --- a/services/brig/test/integration/API/MLS.hs +++ b/services/brig/test/integration/API/MLS.hs @@ -31,9 +31,9 @@ import Data.Timeout import Debug.Trace (traceM) import Federation.Util import Imports +import System.IO.Temp import Test.Tasty import Test.Tasty.HUnit -import UnliftIO.Temporary import Util import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage diff --git a/services/brig/test/integration/Federation/End2end.hs b/services/brig/test/integration/Federation/End2end.hs index d2842747700..fceeb6c94c8 100644 --- a/services/brig/test/integration/Federation/End2end.hs +++ b/services/brig/test/integration/Federation/End2end.hs @@ -27,44 +27,35 @@ import Brig.Options qualified as BrigOpts import Control.Arrow ((&&&)) import Control.Lens hiding ((#)) import Data.Aeson qualified as Aeson -import Data.ByteString qualified as BS import Data.ByteString.Conversion (toByteString') import Data.Default import Data.Domain import Data.Handle import Data.Id import Data.Json.Util (toBase64Text) -import Data.List.NonEmpty (NonEmpty ((:|))) import Data.List1 as List1 import Data.Map qualified as Map import Data.ProtoLens qualified as Protolens import Data.Qualified import Data.Range (checked) import Data.Set qualified as Set -import Data.Text qualified as T import Federation.Util import Imports hiding (cs) -import System.FilePath +import System.IO.Temp import System.Logger qualified as Log -import System.Process import Test.Tasty import Test.Tasty.Cannon (TimeoutUnit (..), (#)) import Test.Tasty.Cannon qualified as WS import Test.Tasty.HUnit -import UnliftIO.Temporary import Util import Util.Options (Endpoint) import Wire.API.Asset import Wire.API.Conversation -import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Conversation.Typing import Wire.API.Event.Conversation import Wire.API.Internal.Notification -import Wire.API.MLS.CipherSuite import Wire.API.MLS.KeyPackage -import Wire.API.MLS.Serialisation -import Wire.API.MLS.SubConversation import Wire.API.Message import Wire.API.Routes.MultiTablePaging import Wire.API.User hiding (assetKey) @@ -110,7 +101,6 @@ spec _brigOpts mg brig galley cargohold cannon _federator brigTwo galleyTwo carg test mg "claim multi-prekey bundle" $ testClaimMultiPrekeyBundleSuccess brig brigTwo, test mg "list user clients" $ testListUserClients brig brigTwo, test mg "list own conversations" $ testListConversations brig brigTwo galley galleyTwo, - test mg "add remote users to local conversation" $ testAddRemoteUsersToLocalConv brig galley brigTwo galleyTwo, test mg "remove remote user from a local conversation" $ testRemoveRemoteUserFromLocalConv brig galley brigTwo galleyTwo, test mg "leave a remote conversation" $ leaveRemoteConversation brig galley brigTwo galleyTwo, test mg "include remote users to new conversation" $ testRemoteUsersInNewConv brig galley brigTwo galleyTwo, @@ -119,10 +109,6 @@ spec _brigOpts mg brig galley cargohold cannon _federator brigTwo galleyTwo carg test mg "delete user connected to remotes and in conversation with remotes" $ testDeleteUser brig brigTwo galley galleyTwo cannon, test mg "download remote asset" $ testRemoteAsset brig brigTwo cargohold cargoholdTwo, test mg "claim remote key packages" $ claimRemoteKeyPackages brig brigTwo, - test mg "send an MLS message to a remote user" $ - testSendMLSMessage brig brigTwo galley galleyTwo cannon cannonTwo, - test mg "send an MLS subconversation message to a federated user" $ - testSendMLSMessageToSubConversation brig brigTwo galley galleyTwo cannon cannonTwo, test mg "remote typing indicator" $ testRemoteTypingIndicator brig brigTwo galley galleyTwo cannon cannonTwo ] @@ -265,63 +251,6 @@ testClaimMultiPrekeyBundleSuccess brig1 brig2 = do const 200 === statusCode const (Just ucm) === responseJsonMaybe -testAddRemoteUsersToLocalConv :: Brig -> Galley -> Brig -> Galley -> Http () -testAddRemoteUsersToLocalConv brig1 galley1 brig2 galley2 = do - alice <- randomUser brig1 - bob <- randomUser brig2 - - let newConv = - NewConv - [] - [] - (checked "gossip") - mempty - Nothing - Nothing - Nothing - Nothing - roleNameWireAdmin - BaseProtocolProteusTag - convId <- - fmap cnvQualifiedId . responseJsonError - =<< post - ( galley1 - . path "/conversations" - . zUser (userId alice) - . zConn "conn" - . header "Z-Type" "access" - . json newConv - ) - - connectUsersEnd2End brig1 brig2 (userQualifiedId alice) (userQualifiedId bob) - - let invite = InviteQualified (userQualifiedId bob :| []) roleNameWireAdmin - post - ( apiVersion "v1" - . galley1 - . paths ["conversations", (toByteString' . qUnqualified) convId, "members", "v2"] - . zUser (userId alice) - . zConn "conn" - . header "Z-Type" "access" - . json invite - ) - !!! (const 200 === statusCode) - - -- test GET /conversations/:domain/:cnv -- Alice's domain is used here - liftIO $ putStrLn "search for conversation on backend 1..." - res <- getConvQualified galley1 (userId alice) convId Galley -> Brig -> Galley -> Http () testRemoveRemoteUserFromLocalConv brig1 galley1 brig2 galley2 = do alice <- randomUser brig1 @@ -699,585 +628,6 @@ claimRemoteKeyPackages brig1 brig2 = do Set.map (\e -> (e.user, e.client)) bundle.entries @?= Set.fromList [(bob, c) | c <- bobClients] --- bob creates an MLS conversation on domain 2 with alice on domain 1, then sends a --- message to alice -testSendMLSMessage :: Brig -> Brig -> Galley -> Galley -> Cannon -> Cannon -> Http () -testSendMLSMessage brig1 brig2 galley1 galley2 cannon1 cannon2 = do - let cli :: String -> FilePath -> [String] -> CreateProcess - cli store tmp args = - proc "mls-test-cli" $ - ["--store", tmp (store <> ".db")] <> args - - -- create alice user and client on domain 1 - alice <- randomUser brig1 - aliceClient <- - clientId . responseJsonUnsafe - <$> addClient - brig1 - (userId alice) - (defNewClient PermanentClientType [] (Imports.head someLastPrekeys)) - let aliceClientId = - show (userId alice) - <> ":" - <> T.unpack aliceClient.client - <> "@" - <> T.unpack (domainText (qDomain (userQualifiedId alice))) - - withSystemTempDirectory "mls" $ \tmp -> do - -- create alice's key package - void . liftIO $ spawn (cli aliceClientId tmp ["init", aliceClientId]) Nothing - kpMLS <- liftIO $ spawn (cli aliceClientId tmp ["key-package", "create"]) Nothing - aliceKP <- liftIO $ case decodeMLS' kpMLS of - Right kp -> pure kp - Left e -> assertFailure $ "Could not decode alice Key Package: " <> T.unpack e - - -- set public key - let update = - defUpdateClient - { updateClientMLSPublicKeys = - Map.singleton - Ed25519 - aliceKP.value.leafNode.signatureKey - } - put - ( brig1 - . paths ["clients", toByteString' aliceClient] - . zUser (qUnqualified (userQualifiedId alice)) - . zClient aliceClient - . json update - ) - !!! const 200 === statusCode - - -- upload key package - post - ( brig1 - . paths ["mls", "key-packages", "self", toByteString' aliceClient] - . zUser (qUnqualified (userQualifiedId alice)) - . zClient aliceClient - . json (KeyPackageUpload [aliceKP]) - ) - !!! const 201 === statusCode - - -- create bob user and client on domain 2 - bob <- randomUser brig2 - bobClient <- - clientId . responseJsonUnsafe - <$> addClient - brig2 - (userId bob) - (defNewClient PermanentClientType [] (someLastPrekeys !! 1)) - let bobClientId = - show (userId bob) - <> ":" - <> T.unpack bobClient.client - <> "@" - <> T.unpack (domainText (qDomain (userQualifiedId bob))) - void . liftIO $ spawn (cli bobClientId tmp ["init", bobClientId]) Nothing - - connectUsersEnd2End brig1 brig2 (userQualifiedId alice) (userQualifiedId bob) - - -- bob claims alice's key package - void $ - post - ( brig2 - . paths - [ "mls", - "key-packages", - "claim", - toByteString' (qDomain (userQualifiedId alice)), - toByteString' (qUnqualified (userQualifiedId alice)) - ] - . zUser (qUnqualified (userQualifiedId bob)) - . zClient bobClient - ) - pure (unGroupId (cnvmlsGroupId p)) - ProtocolProteus -> liftIO $ assertFailure "Expected MLS conversation" - ProtocolMixed _ -> liftIO $ assertFailure "Expected MLS conversation" - let qconvId = cnvQualifiedId conv - groupJSON <- - liftIO $ - spawn - ( cli - bobClientId - tmp - [ "group", - "create", - T.unpack (toBase64Text groupId) - ] - ) - Nothing - liftIO $ BS.writeFile (tmp "group.json") groupJSON - - -- invite alice - liftIO $ BS.writeFile (tmp aliceClientId) (raw aliceKP) - commit <- - liftIO $ - spawn - ( cli - bobClientId - tmp - [ "member", - "add", - "--in-place", - "--group", - tmp "group.json", - "--welcome-out", - tmp "welcome", - "--group-info-out", - tmp "groupinfo.mls", - tmp aliceClientId - ] - ) - Nothing - welcome <- liftIO $ BS.readFile (tmp "welcome") - - -- send a message to the group - dove <- - liftIO $ - spawn - (cli bobClientId tmp ["message", "--group", tmp "group.json", "dove"]) - Nothing - - -- alice creates the group and replies - void . liftIO $ - spawn - ( cli - aliceClientId - tmp - [ "group", - "from-welcome", - "--group-out", - tmp "groupA.json", - tmp "welcome" - ] - ) - Nothing - reply <- - liftIO $ - spawn - ( cli - aliceClientId - tmp - ["message", "--group", tmp "groupA.json", "raven"] - ) - Nothing - - -- send welcome, commit and dove - WS.bracketR cannon1 (userId alice) $ \wsAlice -> do - sendCommitBundle - tmp - "groupinfo.mls" - (Just "welcome") - galley2 - (userId bob) - bobClient - commit - - post - ( galley2 - . paths - ["mls", "messages"] - . zUser (userId bob) - . zClient bobClient - . zConn "conn" - . header "Z-Type" "access" - . content "message/mls" - . bytes dove - ) - !!! const 201 === statusCode - - -- verify that alice receives the welcome message - WS.assertMatch_ (5 # Second) wsAlice $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtType e @?= MLSWelcome - evtFrom e @?= userQualifiedId bob - evtData e @?= EdMLSWelcome welcome - - -- verify that alice receives a join event - WS.assertMatch_ (5 # Second) wsAlice $ \n -> do - let e = List1.head (WS.unpackPayload n) - evtConv e @?= qconvId - evtType e @?= MemberJoin - evtFrom e @?= userQualifiedId bob - fmap (sort . mMembers) (evtData e ^? _EdMembersJoin) - @?= Just [SimpleMember (userQualifiedId alice) roleNameWireMember] - - -- verify that alice receives the dove - WS.assertMatch_ (5 # Second) wsAlice $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconvId - evtType e @?= MLSMessageAdd - evtFrom e @?= userQualifiedId bob - evtData e @?= EdMLSMessage dove - - -- send the reply and assert reception - WS.bracketR cannon2 (userId bob) $ \wsBob -> do - post - ( galley1 - . paths - ["mls", "messages"] - . zUser (userId alice) - . zClient aliceClient - . zConn "conn" - . header "Z-Type" "access" - . content "message/mls" - . bytes reply - ) - !!! const 201 === statusCode - - -- verify that bob receives the reply - WS.assertMatch_ (5 # Second) wsBob $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconvId - evtType e @?= MLSMessageAdd - evtFrom e @?= userQualifiedId alice - evtData e @?= EdMLSMessage reply - --- bob creates an MLS conversation on domain 2 with alice on domain 1, then --- creates a subconversation, and finally sends a message to alice -testSendMLSMessageToSubConversation :: Brig -> Brig -> Galley -> Galley -> Cannon -> Cannon -> Http () -testSendMLSMessageToSubConversation brig1 brig2 galley1 galley2 cannon1 cannon2 = do - let cli :: String -> FilePath -> [String] -> CreateProcess - cli store tmp args = - proc "mls-test-cli" $ - ["--store", tmp (store <> ".db")] <> args - - -- create alice user and client on domain 1 - alice <- randomUser brig1 - aliceClient <- - clientId . responseJsonUnsafe - <$> addClient - brig1 - (userId alice) - (defNewClient PermanentClientType [] (Imports.head someLastPrekeys)) - let aliceClientId = - show (userId alice) - <> ":" - <> T.unpack aliceClient.client - <> "@" - <> T.unpack (domainText (qDomain (userQualifiedId alice))) - - -- create bob user and client on domain 2 - bob <- randomUser brig2 - bobClient <- - clientId . responseJsonUnsafe - <$> addClient - brig2 - (userId bob) - (defNewClient PermanentClientType [] (someLastPrekeys !! 1)) - let bobClientId = - show (userId bob) - <> ":" - <> T.unpack (bobClient.client) - <> "@" - <> T.unpack (domainText (qDomain (userQualifiedId bob))) - - withSystemTempDirectory "mls" $ \tmp -> do - -- create alice's key package - void . liftIO $ spawn (cli aliceClientId tmp ["init", aliceClientId]) Nothing - kpMLS <- liftIO $ spawn (cli aliceClientId tmp ["key-package", "create"]) Nothing - aliceKP <- liftIO $ case decodeMLS' kpMLS of - Right kp -> pure kp - Left e -> assertFailure $ "Could not decode alice Key Package: " <> T.unpack e - - -- set public key - let update = - defUpdateClient - { updateClientMLSPublicKeys = - Map.singleton - Ed25519 - aliceKP.value.leafNode.signatureKey - } - put - ( brig1 - . paths ["clients", toByteString' aliceClient] - . zUser (qUnqualified (userQualifiedId alice)) - . json update - ) - !!! const 200 === statusCode - - -- upload key package - post - ( brig1 - . paths ["mls", "key-packages", "self", toByteString' aliceClient] - . zUser (qUnqualified (userQualifiedId alice)) - . zClient aliceClient - . json (KeyPackageUpload [aliceKP]) - ) - !!! const 201 === statusCode - - -- create bob's client state - void . liftIO $ spawn (cli bobClientId tmp ["init", bobClientId]) Nothing - - connectUsersEnd2End brig1 brig2 (userQualifiedId alice) (userQualifiedId bob) - - -- bob claims alice's key package - void $ - post - ( brig2 - . paths - [ "mls", - "key-packages", - "claim", - toByteString' (qDomain (userQualifiedId alice)), - toByteString' (qUnqualified (userQualifiedId alice)) - ] - . zUser (qUnqualified (userQualifiedId bob)) - . zClient bobClient - ) - pure (unGroupId (cnvmlsGroupId p)) - ProtocolProteus -> liftIO $ assertFailure "Expected MLS conversation" - ProtocolMixed _ -> liftIO $ assertFailure "Expected MLS conversation" - let qconvId = cnvQualifiedId conv - groupJSON <- - liftIO $ - spawn - ( cli - bobClientId - tmp - [ "group", - "create", - T.unpack (toBase64Text groupId) - ] - ) - Nothing - liftIO $ BS.writeFile (tmp "group.json") groupJSON - - -- invite alice - liftIO $ BS.writeFile (tmp aliceClientId) (raw aliceKP) - commit <- - liftIO $ - spawn - ( cli - bobClientId - tmp - [ "member", - "add", - "--in-place", - "--group", - tmp "group.json", - "--welcome-out", - tmp "welcome", - "--group-info-out", - tmp "groupinfo.mls", - tmp aliceClientId - ] - ) - Nothing - welcome <- liftIO $ BS.readFile (tmp "welcome") - - -- send welcome and commit - WS.bracketR cannon1 (userId alice) $ \wsAlice -> do - sendCommitBundle - tmp - "groupinfo.mls" - (Just "welcome") - galley2 - (userId bob) - bobClient - commit - - -- verify that alice receives the welcome message - WS.assertMatch_ (5 # Second) wsAlice $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtType e @?= MLSWelcome - evtFrom e @?= userQualifiedId bob - evtData e @?= EdMLSWelcome welcome - - -- verify that alice receives a join event - WS.assertMatch_ (5 # Second) wsAlice $ \n -> do - let e = List1.head (WS.unpackPayload n) - evtConv e @?= qconvId - evtType e @?= MemberJoin - evtFrom e @?= userQualifiedId bob - fmap (sort . mMembers) (evtData e ^? _EdMembersJoin) - @?= Just [SimpleMember (userQualifiedId alice) roleNameWireMember] - - -- alice creates the group - void . liftIO $ - spawn - ( cli - aliceClientId - tmp - [ "group", - "from-welcome", - "--group-out", - tmp "groupA.json", - tmp "welcome" - ] - ) - Nothing - - -- SUBCONVERSATION - -- create subconversation on domain 2 - subConv <- - responseJsonError - =<< createMLSSubConversation galley2 (userId bob) qconvId (SubConvId "sub") - "subgroup.json") subGroupJSON - - -- bob sends commit bundle for subconversation - do - subCommitRaw <- - liftIO $ - spawn - ( cli - bobClientId - tmp - [ "commit", - "--in-place", - "--group", - tmp "subgroup.json", - "--group-info-out", - tmp "subgroupstate.mls" - ] - ) - Nothing - sendCommitBundle - tmp - "subgroupstate.mls" - Nothing - galley2 - (userId bob) - bobClient - subCommitRaw - - -- alice sends an external commit to add herself to the subconveration - do - subCommitRaw <- - liftIO $ - spawn - ( cli - aliceClientId - tmp - [ "external-commit", - "--group-out", - tmp "subgroupA.json", - "--group-info-in", - tmp "subgroupstate.mls", - "--group-info-out", - tmp "subgroupstateA.mls" - ] - ) - Nothing - sendCommitBundle - tmp - "subgroupstateA.mls" - Nothing - galley1 - (userId alice) - aliceClient - subCommitRaw - - -- prepare bob's message to the subconversation - dove <- - liftIO $ - spawn - ( cli - bobClientId - tmp - ["message", "--group", tmp "subgroup.json", "dove"] - ) - Nothing - - -- prepare alice's reply to the subconversation - reply <- - liftIO $ - spawn - ( cli - aliceClientId - tmp - ["message", "--group", tmp "subgroupA.json", "raven"] - ) - Nothing - - -- send bob's message - WS.bracketR cannon1 (userId alice) $ \wsAlice -> do - post - ( galley2 - . paths - ["mls", "messages"] - . zUser (userId bob) - . zClient bobClient - . zConn "conn" - . header "Z-Type" "access" - . content "message/mls" - . bytes dove - ) - !!! const 201 === statusCode - - -- verify that alice receives bob's message in the subconversation - WS.assertMatch_ (5 # Second) wsAlice $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconvId - evtType e @?= MLSMessageAdd - evtFrom e @?= userQualifiedId bob - evtData e @?= EdMLSMessage dove - - -- send alice's message - WS.bracketR cannon2 (userId bob) $ \wsBob -> do - post - ( galley1 - . paths - ["mls", "messages"] - . zUser (userId alice) - . zClient aliceClient - . zConn "conn" - . header "Z-Type" "access" - . content "message/mls" - . bytes reply - ) - !!! const 201 === statusCode - - -- verify that bob receives alice's message in the subconversation - WS.assertMatch_ (5 # Second) wsBob $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconvId - evtType e @?= MLSMessageAdd - evtFrom e @?= userQualifiedId alice - evtData e @?= EdMLSMessage reply - testRemoteTypingIndicator :: Brig -> Brig -> Galley -> Galley -> Cannon -> Cannon -> Http () testRemoteTypingIndicator brig1 brig2 galley1 galley2 cannon1 cannon2 = do alice <- randomUser brig1 diff --git a/services/federator/test/integration/Test/Federator/IngressSpec.hs b/services/federator/test/integration/Test/Federator/IngressSpec.hs index a93a4fa78bf..0d322238276 100644 --- a/services/federator/test/integration/Test/Federator/IngressSpec.hs +++ b/services/federator/test/integration/Test/Federator/IngressSpec.hs @@ -23,7 +23,6 @@ import Control.Monad.Codensity import Data.Aeson qualified as Aeson import Data.Binary.Builder import Data.Domain -import Data.Handle import Data.LegalHold (UserLegalHoldStatus (UserLegalHoldNoConsent)) import Data.Text.Encoding qualified as Text import Federator.Discovery @@ -57,22 +56,20 @@ spec env = do runTestFederator env $ do brig <- view teBrig <$> ask user <- randomUser brig - hdl <- randomHandle - _ <- putHandle brig (userId user) hdl - let expectedProfile = (publicProfile user UserLegalHoldNoConsent) {profileHandle = Just (Handle hdl)} + let expectedProfile = publicProfile user UserLegalHoldNoConsent runTestSem $ do resp <- liftToCodensity . assertNoError @RemoteError $ inwardBrigCallViaIngress - "get-user-by-handle" - (Aeson.fromEncoding (Aeson.toEncoding hdl)) + "get-users-by-ids" + (Aeson.fromEncoding (Aeson.toEncoding [userId user])) embed . lift @Codensity $ do bdy <- streamingResponseStrictBody resp let actualProfile = Aeson.decode (toLazyByteString bdy) responseStatusCode resp `shouldBe` HTTP.status200 - actualProfile `shouldBe` Just expectedProfile + actualProfile `shouldBe` Just [expectedProfile] -- @SF.Federation @TSFI.RESTfulAPI @S2 @S3 @S7 -- diff --git a/services/federator/test/integration/Test/Federator/InwardSpec.hs b/services/federator/test/integration/Test/Federator/InwardSpec.hs index ae267dd67e8..3b4cc55bd9b 100644 --- a/services/federator/test/integration/Test/Federator/InwardSpec.hs +++ b/services/federator/test/integration/Test/Federator/InwardSpec.hs @@ -28,7 +28,6 @@ import Data.Aeson.Types qualified as Aeson import Data.ByteString qualified as BS import Data.ByteString.Conversion (toByteString') import Data.ByteString.Lazy qualified as LBS -import Data.Handle import Data.LegalHold (UserLegalHoldStatus (UserLegalHoldNoConsent)) import Data.Text.Encoding import Federator.Options hiding (federatorExternal) @@ -48,7 +47,7 @@ import Wire.API.User -- they don't spread out over the different sevices. -- | This module contains tests for the interface between federator and brig. The tests call --- federator directly, circumnventing ingress: +-- federator directly, circumventing ingress: -- -- +----------+ -- |federator-| +------+--+ @@ -71,15 +70,15 @@ spec env = runTestFederator env $ do brig <- view teBrig <$> ask user <- randomUser brig - hdl <- randomHandle - _ <- putHandle brig (userId user) hdl - let expectedProfile = (publicProfile user UserLegalHoldNoConsent) {profileHandle = Just (Handle hdl)} + let expectedProfile = publicProfile user UserLegalHoldNoConsent bdy <- responseJsonError - =<< inwardCall "/federation/brig/get-user-by-handle" (encode hdl) - Sing tag -> @@ -494,7 +498,8 @@ performAction tag origUser lconv action = do performConversationJoin :: forall r. - ( HasConversationActionEffects 'ConversationJoinTag r + ( HasConversationActionEffects 'ConversationJoinTag r, + Member BackendNotificationQueueAccess r ) => Qualified UserId -> Local Conversation -> @@ -518,17 +523,17 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do Local UserId -> Sem r () checkRemoteBackendsConnected lusr = do - let invitedDomains = tDomain <$> snd (partitionQualified lusr $ NE.toList invited) - existingDomains = tDomain . rmId <$> convRemoteMembers (tUnqualified lconv) - - -- Note: - -- - -- In some cases, this federation status check might be redundant (for - -- example if there are only local users in the conversation). However, - -- it is important that we attempt to connect to the backends of the new - -- users here, because that results in the correct error when those - -- backends are not reachable. - checkFederationStatus (RemoteDomains . Set.fromList $ invitedDomains <> existingDomains) + let invitedRemoteUsers = filter ((/= tDomain lconv) . tDomain) $ snd (partitionQualified lusr $ NE.toList invited) + invitedRemoteDomains = Set.fromList $ tDomain <$> invitedRemoteUsers + existingRemoteDomains = Set.fromList $ tDomain . rmId <$> convRemoteMembers (tUnqualified lconv) + allInvitedAlreadyInConversation = null $ invitedRemoteDomains \\ existingRemoteDomains + + if not allInvitedAlreadyInConversation + then checkFederationStatus (RemoteDomains (invitedRemoteDomains <> existingRemoteDomains)) + else -- even if there are no new remotes, we still need to check they are reachable + void . (ensureNoUnreachableBackends =<<) $ + E.runFederatedConcurrentlyEither @_ @'Brig invitedRemoteUsers $ \_ -> + pure () conv :: Data.Conversation conv = tUnqualified lconv @@ -604,7 +609,8 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do performConversationAccessData :: ( HasConversationActionEffects 'ConversationAccessDataTag r, - Member (Error FederationError) r + Member (Error FederationError) r, + Member BackendNotificationQueueAccess r ) => Qualified UserId -> Local Conversation -> @@ -690,13 +696,13 @@ data LocalConversationUpdate = LocalConversationUpdate updateLocalConversation :: forall tag r. - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS ('ActionDenied (ConversationActionPermission tag))) r, Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'ConvNotFound) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Log.Msg -> Log.Msg)) r, @@ -731,12 +737,12 @@ updateLocalConversation lcnv qusr con action = do updateLocalConversationUnchecked :: forall tag r. ( SingI tag, + Member BackendNotificationQueueAccess r, Member (Error FederationError) r, Member (ErrorS ('ActionDenied (ConversationActionPermission tag))) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Log.Msg -> Log.Msg)) r, @@ -779,6 +785,7 @@ updateLocalConversationUserUnchecked :: forall tag r. ( SingI tag, HasConversationActionEffects tag r, + Member BackendNotificationQueueAccess r, Member (Error FederationError) r ) => Local Conversation -> @@ -837,7 +844,7 @@ addMembersToLocalConversation lcnv users role = do notifyConversationAction :: forall tag r. - ( Member FederatorAccess r, + ( Member BackendNotificationQueueAccess r, Member ExternalAccess r, Member GundeckAccess r, Member (Input UTCTime) r, @@ -865,24 +872,23 @@ notifyConversationAction tag quid notifyOrigDomain con lconv targets action = do (SomeConversationAction tag action) update <- do + let remoteTargets = toList (bmRemotes targets) updates <- - E.runFederatedConcurrentlyEither (toList (bmRemotes targets)) $ - \ruids -> do - let update = mkUpdate (tUnqualified ruids) - -- if notifyOrigDomain is false, filter out user from quid's domain, - -- because quid's backend will update local state and notify its users - -- itself using the ConversationUpdate returned by this function - if notifyOrigDomain || tDomain ruids /= qDomain quid - then fedClient @'Galley @"on-conversation-updated" update $> Nothing - else pure (Just update) - let f = fromMaybe (mkUpdate []) . asum . map tUnqualified . rights - update = f updates - failedUpdates = lefts updates - for_ failedUpdates $ - logError - "on-conversation-updated" - "An error occurred while communicating with federated server: " - pure update + enqueueNotificationsConcurrently Q.Persistent remoteTargets $ \ruids -> do + let update = mkUpdate (tUnqualified ruids) + -- if notifyOrigDomain is false, filter out user from quid's domain, + -- because quid's backend will update local state and notify its users + -- itself using the ConversationUpdate returned by this function + if notifyOrigDomain || tDomain ruids /= qDomain quid + then fedQueueClient @'Galley @"on-conversation-updated" update $> Nothing + else pure (Just update) + case partitionEithers updates of + (ls :: [Remote ([UserId], FederationError)], rs) -> do + for_ ls $ + logError + "on-conversation-updated" + "An error occurred while communicating with federated server: " + pure $ fromMaybe (mkUpdate []) . asum . map tUnqualified $ rs -- notify local participants and bots pushConversationEvent con e (qualifyAs lcnv (bmLocals targets)) (bmBots targets) @@ -891,10 +897,12 @@ notifyConversationAction tag quid notifyOrigDomain con lconv targets action = do -- to the originating domain (if it is remote) pure $ LocalConversationUpdate e update where - logError :: (Show a) => String -> String -> (a, FederationError) -> Sem r () + logError :: String -> String -> Remote (a, FederationError) -> Sem r () logError field msg e = P.warn $ - Log.field "federation call" field . Log.msg (msg <> show e) + Log.field "federation call" field + . Log.field "domain" (_domainText (tDomain e)) + . Log.msg (msg <> displayException (snd (tUnqualified e))) -- | Update the local database with information on conversation members joining -- or leaving. Finally, push out notifications to local users. @@ -1022,7 +1030,8 @@ addLocalUsersToRemoteConv remoteConvId qAdder localUsers = do -- leave, but then sends notifications as if the user was removed by someone -- else. kickMember :: - ( Member (Error FederationError) r, + ( Member BackendNotificationQueueAccess r, + Member (Error FederationError) r, Member (Error InternalError) r, Member ExternalAccess r, Member FederatorAccess r, diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index e0fc5889e34..01ca2c1d7e5 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -20,27 +20,20 @@ module Galley.API.Federation where -import Bilge.Retry (httpHandlers) -import Cassandra (ClientState, Consistency (LocalQuorum), Page (hasMore, nextPage, result), PrepQuery, QueryParams, R, Tuple, paginate, paramsP) import Control.Error hiding (note) -import Control.Exception (throwIO) import Control.Lens -import Control.Retry (capDelay, fullJitterBackoff, recovering) import Data.Bifunctor import Data.ByteString.Conversion (toByteString') import Data.Domain (Domain) import Data.Id import Data.Json.Util -import Data.List.NonEmpty qualified as N import Data.Map qualified as Map import Data.Map.Lens (toMapOf) -import Data.Proxy (Proxy (Proxy)) import Data.Qualified -import Data.Range (Range (fromRange), toRange) +import Data.Range (Range (fromRange)) import Data.Set qualified as Set import Data.Singletons (SingI (..), demote, sing) import Data.Tagged -import Data.Text qualified as T import Data.Text.Lazy qualified as LT import Data.Time.Clock import Galley.API.Action @@ -59,16 +52,11 @@ import Galley.API.Message import Galley.API.Push import Galley.API.Util import Galley.App -import Galley.Cassandra.Queries qualified as Q -import Galley.Cassandra.Store import Galley.Data.Conversation qualified as Data import Galley.Effects -import Galley.Effects.BackendNotificationQueueAccess import Galley.Effects.ConversationStore qualified as E -import Galley.Effects.DefederationNotifications import Galley.Effects.FireAndForget qualified as E import Galley.Effects.MemberStore qualified as E -import Galley.Env import Galley.Options import Galley.Types.Conversations.Members import Galley.Types.Conversations.One2One @@ -82,10 +70,8 @@ import Polysemy.Resource import Polysemy.TinyLog import Polysemy.TinyLog qualified as P import Servant (ServerT) -import Servant.API hiding (QueryParams) -import Servant.Client (BaseUrl (BaseUrl), Scheme (Http), mkClientEnv) +import Servant.API import System.Logger.Class qualified as Log -import Util.Options (Endpoint (..)) import Wire.API.Conversation hiding (Member) import Wire.API.Conversation qualified as Public import Wire.API.Conversation.Action @@ -97,13 +83,11 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Common (EmptyResponse (..)) import Wire.API.Federation.API.Galley hiding (id) import Wire.API.Federation.Error -import Wire.API.FederationUpdate (fetch) import Wire.API.MLS.Credential import Wire.API.MLS.GroupInfo import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.API.Message -import Wire.API.Routes.FederationDomainConfig (domain, remotes) import Wire.API.Routes.Named import Wire.API.ServantProto import Wire.API.User (BaseProtocolTag (..)) @@ -134,7 +118,6 @@ federationSitemap = :<|> Named @"delete-sub-conversation" (callsFed deleteSubConversationForRemoteUser) :<|> Named @"leave-sub-conversation" (callsFed leaveSubConversation) :<|> Named @"get-one2one-conversation" getOne2OneConversation - :<|> Named @"on-connection-removed" (onFederationConnectionRemoved (toRange (Proxy @500))) onClientRemoved :: ( Member ConversationStore r, @@ -241,7 +224,8 @@ onConversationUpdated requestingDomain cu = do -- as of now this will not generate the necessary events on the leaver's domain leaveConversation :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error InternalError) r, Member ExternalAccess r, Member FederatorAccess r, @@ -388,7 +372,8 @@ sendMessage originDomain msr = do throwErr = throw . InvalidPayload . LT.pack onUserDeleted :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member FederatorAccess r, Member FireAndForget r, Member ExternalAccess r, @@ -445,7 +430,8 @@ onUserDeleted origDomain udcn = do updateConversation :: forall r. - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member CodeStore r, Member BotAccess r, Member FireAndForget r, @@ -566,7 +552,8 @@ handleMLSMessageErrors = . mapToGalleyError @MLSBundleStaticErrors sendMLSCommitBundle :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member ExternalAccess r, Member (Error FederationError) r, @@ -610,7 +597,8 @@ sendMLSCommitBundle remoteDomain msr = handleMLSMessageErrors $ do ibundle sendMLSMessage :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member ExternalAccess r, Member (Error FederationError) r, @@ -649,132 +637,6 @@ sendMLSMessage remoteDomain msr = handleMLSMessageErrors $ do Nothing msg -mlsSendWelcome :: - ( Member (Error InternalError) r, - Member GundeckAccess r, - Member ExternalAccess r, - Member P.TinyLog r, - Member (Input Env) r, - Member (Input (Local ())) r, - Member (Input UTCTime) r - ) => - Domain -> - MLSWelcomeRequest -> - Sem r MLSWelcomeResponse -mlsSendWelcome origDomain req = do - fmap (either (const MLSWelcomeMLSNotEnabled) (const MLSWelcomeSent)) - . runError @(Tagged 'MLSNotEnabled ()) - $ do - assertMLSEnabled - loc <- qualifyLocal () - now <- input - welcome <- - either (throw . InternalErrorWithDescription . LT.fromStrict) pure $ - decodeMLS' (fromBase64ByteString req.welcomeMessage) - sendLocalWelcomes req.qualifiedConvId (Qualified req.originatingUser origDomain) Nothing now welcome (qualifyAs loc req.recipients) - -onMLSMessageSent :: - ( Member ExternalAccess r, - Member GundeckAccess r, - Member (Input (Local ())) r, - Member (Input Env) r, - Member MemberStore r, - Member P.TinyLog r - ) => - Domain -> - RemoteMLSMessage -> - Sem r RemoteMLSMessageResponse -onMLSMessageSent domain rmm = - fmap (either (const RemoteMLSMessageMLSNotEnabled) (const RemoteMLSMessageOk)) - . runError @(Tagged 'MLSNotEnabled ()) - $ do - assertMLSEnabled - loc <- qualifyLocal () - let rcnv = toRemoteUnsafe domain rmm.conversation - let users = Set.fromList (map fst rmm.recipients) - (members, allMembers) <- - first Set.fromList - <$> E.selectRemoteMembers (toList users) rcnv - unless allMembers $ - P.warn $ - Log.field "conversation" (toByteString' (tUnqualified rcnv)) - Log.~~ Log.field "domain" (toByteString' (tDomain rcnv)) - Log.~~ Log.msg - ( "Attempt to send remote message to local\ - \ users not in the conversation" :: - ByteString - ) - let recipients = filter (\(u, _) -> Set.member u members) rmm.recipients - -- FUTUREWORK: support local bots - let e = - Event (tUntagged rcnv) rmm.subConversation rmm.sender rmm.time $ - EdMLSMessage (fromBase64ByteString rmm.message) - - runMessagePush loc (Just (tUntagged rcnv)) $ - newMessagePush mempty Nothing rmm.metadata recipients e - -queryGroupInfo :: - ( Member ConversationStore r, - Member (Input (Local ())) r, - Member (Input Env) r, - Member SubConversationStore r, - Member MemberStore r - ) => - Domain -> - GetGroupInfoRequest -> - Sem r GetGroupInfoResponse -queryGroupInfo origDomain req = - fmap (either GetGroupInfoResponseError GetGroupInfoResponseState) - . runError @GalleyError - . mapToGalleyError @MLSGroupInfoStaticErrors - $ do - assertMLSEnabled - let sender = toRemoteUnsafe origDomain . (.sender) $ req - state <- case req.conv of - Conv convId -> do - lconvId <- qualifyLocal convId - getGroupInfoFromLocalConv (tUntagged sender) lconvId - SubConv convId subConvId -> do - lconvId <- qualifyLocal convId - getSubConversationGroupInfoFromLocalConv (tUntagged sender) subConvId lconvId - pure - . Base64ByteString - . unGroupInfoData - $ state - -updateTypingIndicator :: - ( Member GundeckAccess r, - Member FederatorAccess r, - Member ConversationStore r, - Member (Input UTCTime) r, - Member (Input (Local ())) r - ) => - Domain -> - TypingDataUpdateRequest -> - Sem r TypingDataUpdateResponse -updateTypingIndicator origDomain TypingDataUpdateRequest {..} = do - let qusr = Qualified userId origDomain - lcnv <- qualifyLocal convId - - ret <- runError - . mapToRuntimeError @'ConvNotFound ConvNotFound - $ do - (conv, _) <- getConversationAndMemberWithError @'ConvNotFound qusr lcnv - notifyTypingIndicator conv qusr Nothing typingStatus - - pure (either TypingDataUpdateError TypingDataUpdateSuccess ret) - -onTypingIndicatorUpdated :: - ( Member GundeckAccess r - ) => - Domain -> - TypingDataUpdated -> - Sem r EmptyResponse -onTypingIndicatorUpdated origDomain TypingDataUpdated {..} = do - let qcnv = Qualified convId origDomain - pushTypingIndicatorEvents origUserId time usersInConv Nothing qcnv typingStatus - pure EmptyResponse - getSubConversationForRemoteUser :: Members '[ SubConversationStore, @@ -904,77 +766,131 @@ instance Left _ -> throw (demote @err) Right res -> pure res --- Since we already have the origin domain where the defederation event started, --- all it needs to carry in addition is the domain it is defederating from. This --- is all the information that we need to cleanup the database and notify clients. -onFederationConnectionRemoved :: - forall r. - ( Member (Input Env) r, - Member (Embed IO) r, - Member (Input ClientState) r, +onMLSMessageSent :: + ( Member ExternalAccess r, + Member GundeckAccess r, + Member (Input (Local ())) r, + Member (Input Env) r, Member MemberStore r, - Member DefederationNotifications r + Member P.TinyLog r ) => - Range 1 1000 Int32 -> Domain -> - Domain -> - Sem r EmptyResponse -onFederationConnectionRemoved range originDomain targetDomain = do - fedDomains <- getFederationDomains - let federatedWithBoth = all (`elem` fedDomains) [originDomain, targetDomain] - when federatedWithBoth $ do - sendOnConnectionRemovedNotifications originDomain targetDomain - cleanupRemovedConnections originDomain targetDomain range - sendOnConnectionRemovedNotifications originDomain targetDomain - pure EmptyResponse + RemoteMLSMessage -> + Sem r RemoteMLSMessageResponse +onMLSMessageSent domain rmm = + fmap (either (const RemoteMLSMessageMLSNotEnabled) (const RemoteMLSMessageOk)) + . runError @(Tagged 'MLSNotEnabled ()) + $ do + assertMLSEnabled + loc <- qualifyLocal () + let rcnv = toRemoteUnsafe domain rmm.conversation + let users = Set.fromList (map fst rmm.recipients) + (members, allMembers) <- + first Set.fromList + <$> E.selectRemoteMembers (toList users) rcnv + unless allMembers $ + P.warn $ + Log.field "conversation" (toByteString' (tUnqualified rcnv)) + Log.~~ Log.field "domain" (toByteString' (tDomain rcnv)) + Log.~~ Log.msg + ( "Attempt to send remote message to local\ + \ users not in the conversation" :: + ByteString + ) + let recipients = filter (\(u, _) -> Set.member u members) rmm.recipients + -- FUTUREWORK: support local bots + let e = + Event (tUntagged rcnv) rmm.subConversation rmm.sender rmm.time $ + EdMLSMessage (fromBase64ByteString rmm.message) -getFederationDomains :: - ( Member (Input Env) r, - Member (Embed IO) r + runMessagePush loc (Just (tUntagged rcnv)) $ + newMessagePush mempty Nothing rmm.metadata recipients e + +mlsSendWelcome :: + ( Member (Error InternalError) r, + Member GundeckAccess r, + Member ExternalAccess r, + Member P.TinyLog r, + Member (Input Env) r, + Member (Input (Local ())) r, + Member (Input UTCTime) r ) => - Sem r [Domain] -getFederationDomains = do - Endpoint (T.unpack -> h) (fromIntegral -> p) <- inputs _brig - mgr <- inputs _manager - liftIO $ recovering policy httpHandlers $ \_ -> do - resp <- fetch $ mkClientEnv mgr $ BaseUrl Http h p "" - either throwIO (pure . fmap domain . remotes) resp - where - policy = capDelay 60_000_000 $ fullJitterBackoff 200_000 - --- for all conversations owned by backend C, only if there are users from both A and B, --- remove users from A and B from those conversations --- This is similar to Galley.API.Internal.deleteFederationDomain --- However it has some important differences, such as we only remove from our conversations --- where users for both domains are in the same conversation. -cleanupRemovedConnections :: - forall r. - ( Member (Embed IO) r, - Member (Input ClientState) r, + Domain -> + MLSWelcomeRequest -> + Sem r MLSWelcomeResponse +mlsSendWelcome origDomain req = do + fmap (either (const MLSWelcomeMLSNotEnabled) (const MLSWelcomeSent)) + . runError @(Tagged 'MLSNotEnabled ()) + $ do + assertMLSEnabled + loc <- qualifyLocal () + now <- input + welcome <- + either (throw . InternalErrorWithDescription . LT.fromStrict) pure $ + decodeMLS' (fromBase64ByteString req.welcomeMessage) + sendLocalWelcomes req.qualifiedConvId (Qualified req.originatingUser origDomain) Nothing now welcome (qualifyAs loc req.recipients) + +queryGroupInfo :: + ( Member ConversationStore r, + Member (Input (Local ())) r, + Member (Input Env) r, + Member SubConversationStore r, Member MemberStore r ) => Domain -> + GetGroupInfoRequest -> + Sem r GetGroupInfoResponse +queryGroupInfo origDomain req = + fmap (either GetGroupInfoResponseError GetGroupInfoResponseState) + . runError @GalleyError + . mapToGalleyError @MLSGroupInfoStaticErrors + $ do + assertMLSEnabled + let sender = toRemoteUnsafe origDomain . (.sender) $ req + state <- case req.conv of + Conv convId -> do + lconvId <- qualifyLocal convId + getGroupInfoFromLocalConv (tUntagged sender) lconvId + SubConv convId subConvId -> do + lconvId <- qualifyLocal convId + getSubConversationGroupInfoFromLocalConv (tUntagged sender) subConvId lconvId + pure + . Base64ByteString + . unGroupInfoData + $ state + +updateTypingIndicator :: + ( Member GundeckAccess r, + Member FederatorAccess r, + Member ConversationStore r, + Member (Input UTCTime) r, + Member (Input (Local ())) r + ) => + Domain -> + TypingDataUpdateRequest -> + Sem r TypingDataUpdateResponse +updateTypingIndicator origDomain TypingDataUpdateRequest {..} = do + let qusr = Qualified userId origDomain + lcnv <- qualifyLocal convId + + ret <- runError + . mapToRuntimeError @'ConvNotFound ConvNotFound + $ do + (conv, _) <- getConversationAndMemberWithError @'ConvNotFound qusr lcnv + notifyTypingIndicator conv qusr Nothing typingStatus + + pure (either TypingDataUpdateError TypingDataUpdateSuccess ret) + +onTypingIndicatorUpdated :: + ( Member GundeckAccess r + ) => Domain -> - Range 1 1000 Int32 -> - Sem r () -cleanupRemovedConnections domainA domainB (fromRange -> maxPage) = do - runPaginated Q.selectConvIdsByRemoteDomain (paramsP LocalQuorum (Identity domainA) maxPage) $ \convIds -> - -- `nub $ sort` is a small performance boost, it will drop duplicate convIds from the page results. - -- However we can certainly still process a conversation more than once if it is in multiple pages. - for_ (nub $ sort convIds) $ \(runIdentity -> convId) -> do - -- Check if users from domain B are in the conversation - b <- isJust <$> E.checkConvForRemoteDomain convId domainB - when b $ do - -- Users from both domains exist, delete all of them from the conversation. - E.removeRemoteDomain convId domainA - E.removeRemoteDomain convId domainB - where - runPaginated :: (Tuple p, Tuple a) => PrepQuery R p a -> QueryParams p -> ([a] -> Sem r b) -> Sem r b - runPaginated q ps f = go f <=< embedClient $ paginate q ps - go :: ([a] -> Sem r b) -> Page a -> Sem r b - go f page - | hasMore page = f (result page) >> embedClient (nextPage page) >>= go f - | otherwise = f $ result page + TypingDataUpdated -> + Sem r EmptyResponse +onTypingIndicatorUpdated origDomain TypingDataUpdated {..} = do + let qcnv = Qualified convId origDomain + pushTypingIndicatorEvents origUserId time usersInConv Nothing qcnv typingStatus + pure EmptyResponse -------------------------------------------------------------------------------- -- Utilities @@ -996,7 +912,3 @@ logFederationError lc e = \ a user from a local conversation: " <> displayException e ) - --- Build the map, keyed by conversations to the list of members -insertIntoMap :: (ConvId, a) -> Map ConvId (N.NonEmpty a) -> Map ConvId (N.NonEmpty a) -insertIntoMap (cnvId, user) m = Map.alter (pure . maybe (pure user) (N.cons user)) cnvId m diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 6fe891249ca..8c0586f5daa 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -21,32 +21,23 @@ module Galley.API.Internal InternalAPI, deleteLoop, safeForever, - -- Exported for tests - deleteFederationDomain, ) where -import Bilge.Retry -import Cassandra (ClientState, Consistency (LocalQuorum), Page (hasMore, nextPage, result), paginate, paramsP) import Control.Exception.Safe (catchAny) import Control.Lens hiding (Getter, Setter, (.=)) -import Control.Retry -import Data.Domain import Data.Id as Id -import Data.List.NonEmpty qualified as N import Data.List1 (maybeList1) import Data.Map qualified as Map import Data.Qualified import Data.Range import Data.Singletons -import Data.Text (unpack) import Data.Time import Galley.API.Action import Galley.API.Clients qualified as Clients import Galley.API.Create qualified as Create import Galley.API.CustomBackend qualified as CustomBackend import Galley.API.Error -import Galley.API.Federation (insertIntoMap) import Galley.API.LegalHold (unsetTeamLegalholdWhitelistedH) import Galley.API.LegalHold.Conflicts import Galley.API.MLS.Removal @@ -60,15 +51,11 @@ import Galley.API.Teams.Features import Galley.API.Update qualified as Update import Galley.API.Util import Galley.App -import Galley.Cassandra.Queries qualified as Q -import Galley.Cassandra.Store (embedClient) import Galley.Data.Conversation qualified as Data -import Galley.Data.Conversation.Types import Galley.Effects import Galley.Effects.BackendNotificationQueueAccess import Galley.Effects.ClientStore import Galley.Effects.ConversationStore -import Galley.Effects.DefederationNotifications (DefederationNotifications, sendDefederationNotifications) import Galley.Effects.FederatorAccess import Galley.Effects.GundeckAccess import Galley.Effects.LegalHoldStore as LegalHoldStore @@ -80,12 +67,10 @@ import Galley.Options hiding (brig) import Galley.Queue qualified as Q import Galley.Types.Bot (AddBot, RemoveBot) import Galley.Types.Bot.Service -import Galley.Types.Conversations.Members (RemoteMember (RemoteMember, rmId)) +import Galley.Types.Conversations.Members (RemoteMember (rmId)) import Galley.Types.UserList import Imports hiding (head) import Network.AMQP qualified as Q -import Network.HTTP.Types -import Network.Wai import Network.Wai.Predicate hiding (Error, err, result, setStatus) import Network.Wai.Predicate qualified as Predicate hiding (result) import Network.Wai.Routing hiding (App, route, toList) @@ -96,10 +81,8 @@ import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog qualified as P import Servant hiding (JSON, WithStatus) -import Servant.Client (BaseUrl (BaseUrl), ClientEnv (ClientEnv), Scheme (Http), defaultMakeClientRequest) import System.Logger.Class hiding (Path, name) import System.Logger.Class qualified as Log -import Util.Options import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Action import Wire.API.CustomBackend @@ -109,7 +92,6 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error -import Wire.API.FederationUpdate import Wire.API.Provider.Service hiding (Service) import Wire.API.Routes.API import Wire.API.Routes.Internal.Galley @@ -118,7 +100,7 @@ import Wire.API.Routes.MultiTablePaging (mtpHasMore, mtpPagingState, mtpResults) import Wire.API.Team.Feature hiding (setStatus) import Wire.API.Team.Member import Wire.API.User.Client -import Wire.Sem.Paging (Paging (pageItems, pageState)) +import Wire.Sem.Paging import Wire.Sem.Paging.Cassandra internalAPI :: API InternalAPI GalleyEffects @@ -329,10 +311,6 @@ internalSitemap = unsafeCallsFed @'Galley @"on-client-removed" $ unsafeCallsFed capture "domain" .&. accept "application" "json" - delete "/i/federation/:domain" (continue . internalDeleteFederationDomainH $ toRange (Proxy @500)) $ - capture "domain" - .&. accept "application" "json" - rmUser :: forall p1 p2 r. ( p1 ~ CassandraPaging, @@ -515,193 +493,3 @@ iGetMLSClientListForConv :: iGetMLSClientListForConv gid = do cm <- E.lookupMLSClients gid pure $ ClientList (concatMap (Map.keys . snd) (Map.assocs cm)) - --- Bundle all of the deletes together for easy calling --- Errors & exceptions are thrown to IO to stop the message being ACKed, eventually timing it --- out so that it can be redelivered. -deleteFederationDomain :: - ( Member (Input Env) r, - Member (P.Logger (Msg -> Msg)) r, - Member (Error FederationError) r, - Member (Error InternalError) r, - Member MemberStore r, - Member (Input ClientState) r, - Member ConversationStore r, - Member (Embed IO) r, - Member CodeStore r, - Member TeamStore r, - Member BrigAccess r, - Member GundeckAccess r, - Member ExternalAccess r, - Member (Input UTCTime) r, - Member SubConversationStore r, - Member ProposalStore r, - Member FederatorAccess r - ) => - Range 1 1000 Int32 -> - Domain -> - Sem r () -deleteFederationDomain range d = do - deleteFederationDomainRemoteUserFromLocalConversations range d - deleteFederationDomainLocalUserFromRemoteConversation range d - deleteFederationDomainOneOnOne d - -internalDeleteFederationDomainH :: - ( Member (Input Env) r, - Member (P.Logger (Msg -> Msg)) r, - Member (Error FederationError) r, - Member (Error InternalError) r, - Member MemberStore r, - Member ConversationStore r, - Member (Embed IO) r, - Member (Input ClientState) r, - Member CodeStore r, - Member TeamStore r, - Member BrigAccess r, - Member GundeckAccess r, - Member ExternalAccess r, - Member DefederationNotifications r, - Member (Input UTCTime) r, - Member SubConversationStore r, - Member ProposalStore r, - Member FederatorAccess r - ) => - Range 1 1000 Int32 -> - Domain ::: JSON -> - Sem r Response -internalDeleteFederationDomainH range (domain ::: _) = do - -- We have to send the same event twice. - -- Once before and once after defederation work. - -- https://wearezeta.atlassian.net/wiki/spaces/ENGINEERIN/pages/809238539/Use+case+Stopping+to+federate+with+a+domain - sendDefederationNotifications domain - deleteFederationDomain range domain - sendDefederationNotifications domain - pure (empty & setStatus status200) - --- Remove remote members from local conversations -deleteFederationDomainRemoteUserFromLocalConversations :: - forall r. - ( Member (Input Env) r, - Member (P.Logger (Msg -> Msg)) r, - Member (Error FederationError) r, - Member (Error InternalError) r, - Member (Input ClientState) r, - Member (Embed IO) r, - Member MemberStore r, - Member ConversationStore r, - Member CodeStore r, - Member TeamStore r, - Member (Input UTCTime) r, - Member SubConversationStore r, - Member ProposalStore r, - Member ExternalAccess r, - Member FederatorAccess r, - Member GundeckAccess r, - Member BrigAccess r - ) => - Range 1 1000 Int32 -> - Domain -> - Sem r () -deleteFederationDomainRemoteUserFromLocalConversations (fromRange -> maxPage) dom = do - remoteUsers <- - mkConvMem <$$> do - page <- - embedClient $ - paginate Q.selectRemoteMembersByDomain $ - paramsP LocalQuorum (Identity dom) maxPage - getPaginatedData page - env <- input - let lCnvMap = foldr insertIntoMap mempty remoteUsers - localDomain = env ^. Galley.App.options . Galley.Options.settings . federationDomain - for_ (Map.toList lCnvMap) $ \(cnvId, rUsers) -> do - let mapAllErrors :: - Text -> - Sem (Error NoChanges ': ErrorS 'NotATeamMember ': r) () -> - Sem r () - mapAllErrors msgText = - -- This can be thrown in `updateLocalConversationUserUnchecked @'ConversationDeleteTag`. - P.logAndIgnoreErrors @(Tagged 'NotATeamMember ()) (const "Not a team member") msgText - -- This can be thrown in `updateLocalConversationUserUnchecked @'ConversationRemoveMembersTag` - . P.logAndIgnoreErrors @NoChanges (const "No changes") msgText - - mapAllErrors "Federation domain removal" $ do - getConversation cnvId - >>= maybe (pure () {- conv already gone, nothing to do -}) (delConv localDomain rUsers) - where - mkConvMem (convId, usr, role) = (convId, RemoteMember (toRemoteUnsafe dom usr) role) - delConv :: - Domain -> - N.NonEmpty RemoteMember -> - Galley.Data.Conversation.Types.Conversation -> - Sem (Error NoChanges : ErrorS 'NotATeamMember : r) () - delConv localDomain rUsers conv = - do - let lConv = toLocalUnsafe localDomain conv - updateLocalConversationUserUnchecked - @'ConversationRemoveMembersTag - lConv - undefined - $ tUntagged . rmId <$> rUsers -- This field can be undefined as the path for ConversationRemoveMembersTag doens't use it - -- Check if the conversation if type 2 or 3, one-on-one conversations. - -- If it is, then we need to remove the entire conversation as users - -- aren't able to delete those types of conversations themselves. - -- Check that we are in a type 2 or a type 3 conversation - when (cnvmType (convMetadata conv) `elem` [One2OneConv, ConnectConv]) $ - -- If we are, delete it. - updateLocalConversationUserUnchecked - @'ConversationDeleteTag - lConv - undefined - () - --- Remove local members from remote conversations -deleteFederationDomainLocalUserFromRemoteConversation :: - ( Member (Error InternalError) r, - Member (Input ClientState) r, - Member (Embed IO) r, - Member MemberStore r - ) => - Range 1 1000 Int32 -> - Domain -> - Sem r () -deleteFederationDomainLocalUserFromRemoteConversation (fromRange -> maxPage) dom = do - remoteConvs <- - foldr insertIntoMap mempty <$> do - page <- - embedClient $ - paginate Q.selectLocalMembersByDomain $ - paramsP LocalQuorum (Identity dom) maxPage - getPaginatedData page - for_ (Map.toList remoteConvs) $ \(cnv, lUsers) -> do - -- All errors, either exceptions or Either e, get thrown into IO - mapError @NoChanges (const (InternalErrorWithDescription "No Changes: Could not remove a local member from a remote conversation.")) $ do - E.deleteMembersInRemoteConversation (toRemoteUnsafe dom cnv) (N.toList lUsers) - --- These need to be recoverable? --- This is recoverable with the following flow conditions. --- 1) Deletion calls to the Brig endpoint `delete-federation-remote-from-galley` are idempotent for a given domain. --- 2) This call is made from a function that is backed by a RabbitMQ queue. --- The calling function needs to catch thrown exceptions and NACK the deletion --- message. This will allow Rabbit to redeliver the message and give us a second --- go at performing the deletion. -deleteFederationDomainOneOnOne :: (Member (Input Env) r, Member (Embed IO) r) => Domain -> Sem r () -deleteFederationDomainOneOnOne dom = do - env <- input - let c = mkClientEnv (env ^. manager) (env ^. brig) - -- This is the same policy as background-worker for retrying. - policy = capDelay 60_000_000 $ fullJitterBackoff 200_000 - void . liftIO . recovering policy httpHandlers $ \_ -> deleteFederationRemoteGalley dom c - where - mkClientEnv mgr (Endpoint h p) = ClientEnv mgr (BaseUrl Http (unpack h) (fromIntegral p) "") Nothing defaultMakeClientRequest - -getPaginatedData :: - ( Member (Input ClientState) r, - Member (Embed IO) r - ) => - Page a -> - Sem r [a] -getPaginatedData page - | hasMore page = - (result page <>) <$> do - getPaginatedData <=< embedClient $ nextPage page - | otherwise = pure $ result page diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index c2a52f5115d..5c23f29b89d 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -138,7 +138,8 @@ getSettings lzusr tid = do removeSettingsInternalPaging :: forall r. - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error AuthenticationError) r, Member (Error FederationError) r, @@ -178,6 +179,10 @@ removeSettings :: forall p r. ( Paging p, Bounded (PagingBounds p TeamMember), + Member TeamFeatureStore r, + Member (TeamMemberStore p) r, + Member TeamStore r, + Member BackendNotificationQueueAccess r, Member BrigAccess r, Member ConversationStore r, Member (Error AuthenticationError) r, @@ -203,10 +208,7 @@ removeSettings :: Member MemberStore r, Member ProposalStore r, Member P.TinyLog r, - Member SubConversationStore r, - Member TeamFeatureStore r, - Member (TeamMemberStore p) r, - Member TeamStore r + Member SubConversationStore r ) => UserId -> TeamId -> @@ -237,29 +239,30 @@ removeSettings' :: forall p r. ( Paging p, Bounded (PagingBounds p TeamMember), + Member BackendNotificationQueueAccess r, Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, - Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, Member (ErrorS 'LegalHoldServiceNotRegistered) r, Member (ErrorS 'UserLegalHoldIllegalOperation) r, + Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, Member ExternalAccess r, Member FederatorAccess r, Member FireAndForget r, Member GundeckAccess r, - Member (Input Env) r, - Member (Input (Local ())) r, Member (Input UTCTime) r, + Member (Input (Local ())) r, + Member (Input Env) r, Member LegalHoldStore r, Member (ListItems LegacyPaging ConvId) r, Member MemberStore r, + Member (TeamMemberStore p) r, + Member TeamStore r, Member ProposalStore r, Member P.TinyLog r, - Member SubConversationStore r, - Member (TeamMemberStore p) r, - Member TeamStore r + Member SubConversationStore r ) => TeamId -> Sem r () @@ -325,7 +328,8 @@ getUserStatus _lzusr tid uid = do -- @withdrawExplicitConsentH@ (lots of corner cases we'd have to implement for that to pan -- out). grantConsent :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -363,7 +367,8 @@ grantConsent lusr tid = do -- | Request to provision a device on the legal hold service for a user requestDevice :: forall r. - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -442,7 +447,8 @@ requestDevice lzusr tid uid = do -- since they are replaced if needed when registering new LH devices. approveDevice :: forall r. - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error AuthenticationError) r, Member (Error FederationError) r, @@ -521,7 +527,8 @@ approveDevice lzusr connId tid uid (Public.ApproveLegalHoldForUserRequest mPassw disableForUser :: forall r. - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error AuthenticationError) r, Member (Error FederationError) r, @@ -579,7 +586,8 @@ disableForUser lzusr tid uid (Public.DisableLegalHoldForUserRequest mPassword) = -- or disabled, make sure the affected connections are screened for policy conflict (anybody -- with no-consent), and put those connections in the appropriate blocked state. changeLegalholdStatus :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -697,7 +705,8 @@ unsetTeamLegalholdWhitelistedH tid = do -- contains the hypothetical new LH status of `uid`'s so it can be consulted instead of the -- one from the database. handleGroupConvPolicyConflicts :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, diff --git a/services/galley/src/Galley/API/MLS/Commit/Core.hs b/services/galley/src/Galley/API/MLS/Commit/Core.hs index 12f2a1cf73e..99cbd5106c6 100644 --- a/services/galley/src/Galley/API/MLS/Commit/Core.hs +++ b/services/galley/src/Galley/API/MLS/Commit/Core.hs @@ -61,7 +61,8 @@ import Wire.API.MLS.SubConversation import Wire.API.User.Client type HasProposalActionEffects r = - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error InternalError) r, Member (ErrorS 'ConvNotFound) r, diff --git a/services/galley/src/Galley/API/MLS/Proposal.hs b/services/galley/src/Galley/API/MLS/Proposal.hs index 0005765d191..90875ecf585 100644 --- a/services/galley/src/Galley/API/MLS/Proposal.hs +++ b/services/galley/src/Galley/API/MLS/Proposal.hs @@ -110,7 +110,8 @@ proposalProcessingStage (ExternalInitProposal _) = ProposalProcessingStageExtern proposalProcessingStage (GroupContextExtensionsProposal _) = ProposalProcessingStageExtensions type HasProposalEffects r = - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error InternalError) r, Member (Error MLSProposalFailure) r, diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index acb04b647bd..d4395976b05 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -1082,7 +1082,8 @@ getTeamConversation zusr tid cid = do >>= noteS @'ConvNotFound deleteTeamConversation :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member CodeStore r, Member ConversationStore r, Member (Error FederationError) r, @@ -1090,10 +1091,10 @@ deleteTeamConversation :: Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS ('ActionDenied 'DeleteConversation)) r, + Member FederatorAccess r, Member MemberStore r, Member ProposalStore r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member SubConversationStore r, diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index e574c3de759..1736b887bca 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -283,6 +283,7 @@ instance SetFeatureConfig LegalholdConfig where type SetConfigForTeamConstraints LegalholdConfig (r :: EffectRow) = ( Bounded (PagingBounds InternalPaging TeamMember), + Member BackendNotificationQueueAccess r, Member BotAccess r, Member BrigAccess r, Member CodeStore r, diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 2e9524d33a0..e39df03f31c 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -97,7 +97,6 @@ import Galley.Data.Conversation.Types qualified as Data import Galley.Data.Services as Data import Galley.Data.Types hiding (Conversation) import Galley.Effects -import Galley.Effects.BackendNotificationQueueAccess import Galley.Effects.ClientStore qualified as E import Galley.Effects.CodeStore qualified as E import Galley.Effects.ConversationStore qualified as E @@ -257,7 +256,8 @@ handleUpdateResult = \case Unchanged -> empty & setStatus status204 type UpdateConversationAccessEffects = - '[ BotAccess, + '[ BackendNotificationQueueAccess, + BotAccess, BrigAccess, CodeStore, ConversationStore, @@ -312,7 +312,8 @@ updateConversationAccessUnqualified lusr con cnv update = update updateConversationReceiptMode :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -385,7 +386,8 @@ updateRemoteConversation rcnv lusr conn action = getUpdateResult $ do updateLocalStateOfRemoteConv (qualifyAs rcnv convUpdate) (Just conn) >>= note NoChanges updateConversationReceiptModeUnqualified :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -408,13 +410,13 @@ updateConversationReceiptModeUnqualified :: updateConversationReceiptModeUnqualified lusr zcon cnv = updateConversationReceiptMode lusr zcon (tUntagged (qualifyAs lusr cnv)) updateConversationMessageTimer :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (ErrorS ('ActionDenied 'ModifyConversationMessageTimer)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member (Error FederationError) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Msg -> Msg)) r @@ -441,13 +443,13 @@ updateConversationMessageTimer lusr zcon qcnv update = qcnv updateConversationMessageTimerUnqualified :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (ErrorS ('ActionDenied 'ModifyConversationMessageTimer)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member (Error FederationError) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Msg -> Msg)) r @@ -461,6 +463,7 @@ updateConversationMessageTimerUnqualified lusr zcon cnv = updateConversationMess deleteLocalConversation :: ( Member BrigAccess r, + Member BackendNotificationQueueAccess r, Member CodeStore r, Member ConversationStore r, Member (Error FederationError) r, @@ -696,6 +699,7 @@ updateConversationProtocolWithLocalUser :: Member (Input Env) r, Member (Input (Local ())) r, Member (Input Opts) r, + Member BackendNotificationQueueAccess r, Member BrigAccess r, Member ConversationStore r, Member MemberStore r, @@ -732,7 +736,8 @@ updateConversationProtocolWithLocalUser lusr conn qcnv (P.ProtocolUpdate newProt joinConversationByReusableCode :: forall r. - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member CodeStore r, Member ConversationStore r, Member (ErrorS 'CodeNotFound) r, @@ -743,7 +748,6 @@ joinConversationByReusableCode :: Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'TooManyMembers) r, - Member FederatorAccess r, Member ExternalAccess r, Member GundeckAccess r, Member (Input Opts) r, @@ -765,8 +769,8 @@ joinConversationByReusableCode lusr zcon req = do joinConversationById :: forall r. - ( Member BrigAccess r, - Member FederatorAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'ConvNotFound) r, @@ -790,8 +794,9 @@ joinConversationById lusr zcon cnv = do joinConversation lusr zcon conv LinkAccess joinConversation :: - ( Member BrigAccess r, - Member FederatorAccess r, + forall r. + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'NotATeamMember) r, @@ -833,7 +838,8 @@ joinConversation lusr zcon conv access = do action addMembers :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'AddConversationMember)) r, @@ -873,7 +879,8 @@ addMembers lusr zcon qcnv (InviteQualified users role) = do ConversationJoin users role addMembersUnqualifiedV2 :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -913,7 +920,8 @@ addMembersUnqualifiedV2 lusr zcon cnv (InviteQualified users role) = do ConversationJoin users role addMembersUnqualified :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -1015,7 +1023,8 @@ updateUnqualifiedSelfMember lusr zcon cnv update = do updateSelfMember lusr zcon (tUntagged lcnv) update updateOtherMemberLocalConv :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r, Member (ErrorS 'InvalidTarget) r, @@ -1023,7 +1032,6 @@ updateOtherMemberLocalConv :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvMemberNotFound) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member MemberStore r, @@ -1042,7 +1050,8 @@ updateOtherMemberLocalConv lcnv lusr con qvictim update = void . getUpdateResult ConversationMemberUpdate qvictim update updateOtherMemberUnqualified :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r, Member (ErrorS 'InvalidTarget) r, @@ -1050,7 +1059,6 @@ updateOtherMemberUnqualified :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvMemberNotFound) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member MemberStore r, @@ -1068,7 +1076,8 @@ updateOtherMemberUnqualified lusr zcon cnv victim update = do updateOtherMemberLocalConv lcnv lusr zcon (tUntagged lvictim) update updateOtherMember :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r, Member (ErrorS 'InvalidTarget) r, @@ -1076,7 +1085,6 @@ updateOtherMember :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvMemberNotFound) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member MemberStore r, @@ -1103,7 +1111,8 @@ updateOtherMemberRemoteConv :: updateOtherMemberRemoteConv _ _ _ _ _ = throw FederationNotImplemented removeMemberUnqualified :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, @@ -1130,7 +1139,8 @@ removeMemberUnqualified lusr con cnv victim = do removeMemberQualified lusr con (tUntagged lcnv) (tUntagged lvictim) removeMemberQualified :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, @@ -1198,7 +1208,8 @@ removeMemberFromRemoteConv cnv lusr victim -- | Remove a member from a local conversation. removeMemberFromLocalConv :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'LeaveConversation)) r, @@ -1391,14 +1402,14 @@ postOtrMessageUnqualified sender zcon cnv = (runLocalInput sender . postQualifiedOtrMessage User (tUntagged sender) (Just zcon) lcnv) updateConversationName :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InvalidInput) r, Member (ErrorS ('ActionDenied 'ModifyConversationName)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Msg -> Msg)) r @@ -1417,14 +1428,14 @@ updateConversationName lusr zcon qcnv convRename = do convRename updateUnqualifiedConversationName :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InvalidInput) r, Member (ErrorS ('ActionDenied 'ModifyConversationName)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Msg -> Msg)) r @@ -1439,14 +1450,14 @@ updateUnqualifiedConversationName lusr zcon cnv rename = do updateLocalConversationName lusr zcon lcnv rename updateLocalConversationName :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InvalidInput) r, Member (ErrorS ('ActionDenied 'ModifyConversationName)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ExternalAccess r, - Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, Member (Logger (Msg -> Msg)) r diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 0d2c9484a6d..9978c77ec0f 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -145,7 +145,6 @@ validateOptions o = do (Nothing, Just _) -> error "RabbitMQ config is specified and federator is not, please specify both or none" (Just _, Nothing) -> error "Federator is specified and RabbitMQ config is not, please specify both or none" _ -> pure () - let mlsFlag = settings' ^. featureFlags . Teams.flagMLS . Teams.unDefaults . Teams.unImplicitLockStatus mlsConfig = wsConfig mlsFlag migrationStatus = wsStatus $ settings' ^. featureFlags . Teams.flagMlsMigration . Teams.unDefaults @@ -153,7 +152,6 @@ validateOptions o = do error "For starting MLS migration, MLS must be included in the supportedProtocol list" unless (mlsDefaultProtocol mlsConfig `elem` mlsSupportedProtocols mlsConfig) $ error "The list 'settings.featureFlags.mls.supportedProtocols' must include the value in the field 'settings.featureFlags.mls.defaultProtocol'" - let errMsg = "Either conversationCodeURI or multiIngress needs to be set." case (settings' ^. conversationCodeURI, settings' ^. multiIngress) of (Nothing, Nothing) -> error errMsg @@ -296,7 +294,6 @@ evalGalley e = . interpretFederatorAccess . interpretExternalAccess . interpretGundeckAccess - . interpretDefederationNotifications . interpretSparAccess . interpretBrigAccess where diff --git a/services/galley/src/Galley/Cassandra/Conversation/Members.hs b/services/galley/src/Galley/Cassandra/Conversation/Members.hs index 090c7be4e30..abd3a0139e6 100644 --- a/services/galley/src/Galley/Cassandra/Conversation/Members.hs +++ b/services/galley/src/Galley/Cassandra/Conversation/Members.hs @@ -209,32 +209,14 @@ lookupRemoteMembers conv = do lookupRemoteMembersByDomain :: Domain -> Client [(ConvId, RemoteMember)] lookupRemoteMembersByDomain dom = do - mkConvMem <$$$> retry x1 $ query Cql.selectRemoteMembersByDomain (params LocalQuorum (Identity dom)) + fmap (fmap mkConvMem) . retry x1 $ query Cql.selectRemoteMembersByDomain (params LocalQuorum (Identity dom)) where mkConvMem (convId, usr, role) = (convId, RemoteMember (toRemoteUnsafe dom usr) role) -lookupRemoteMembersByConvAndDomain :: ConvId -> Domain -> Client [RemoteMember] -lookupRemoteMembersByConvAndDomain conv dom = do - mkMem <$$$> retry x1 $ query Cql.selectRemoteMembersByConvAndDomain (params LocalQuorum (conv, dom)) - where - mkMem (usr, role) = RemoteMember (toRemoteUnsafe dom usr) role - lookupLocalMembersByDomain :: Domain -> Client [(ConvId, UserId)] lookupLocalMembersByDomain dom = do retry x1 $ query Cql.selectLocalMembersByDomain (params LocalQuorum (Identity dom)) -removeRemoteDomain :: ConvId -> Domain -> Client () -removeRemoteDomain convId dom = do - retry x1 $ write Cql.removeRemoteDomain $ params LocalQuorum (convId, dom) - -selectConvIdsByRemoteDomain :: Domain -> Client [ConvId] -selectConvIdsByRemoteDomain dom = do - runIdentity <$$$> retry x1 $ query Cql.selectConvIdsByRemoteDomain $ params LocalQuorum $ Identity dom - -checkConvForRemoteDomain :: ConvId -> Domain -> Client (Maybe ConvId) -checkConvForRemoteDomain convId dom = do - runIdentity <$$$> retry x1 $ query1 Cql.checkConvForRemoteDomain $ params LocalQuorum (convId, dom) - member :: ConvId -> UserId -> @@ -438,8 +420,4 @@ interpretMemberStoreToCassandra = interpret $ \case LookupMLSClients lcnv -> embedClient $ lookupMLSClients lcnv LookupMLSClientLeafIndices lcnv -> embedClient $ lookupMLSClientLeafIndices lcnv GetRemoteMembersByDomain dom -> embedClient $ lookupRemoteMembersByDomain dom - GetRemoteMembersByConvAndDomain conv dom -> embedClient $ lookupRemoteMembersByConvAndDomain conv dom GetLocalMembersByDomain dom -> embedClient $ lookupLocalMembersByDomain dom - RemoveRemoteDomain convId dom -> embedClient $ removeRemoteDomain convId dom - SelectConvIdsByRemoteDomain dom -> embedClient $ selectConvIdsByRemoteDomain dom - CheckConvForRemoteDomain convId dom -> embedClient $ checkConvForRemoteDomain convId dom diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index 25bd766c789..c5904013608 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -423,24 +423,11 @@ selectRemoteMembers = "select user_remote_domain, user_remote_id, conversation_r updateRemoteMemberConvRoleName :: PrepQuery W (RoleName, ConvId, Domain, UserId) () updateRemoteMemberConvRoleName = {- `IF EXISTS`, but that requires benchmarking -} "update member_remote_user set conversation_role = ? where conv = ? and user_remote_domain = ? and user_remote_id = ?" -removeRemoteDomain :: PrepQuery W (ConvId, Domain) () -removeRemoteDomain = "delete from member_remote_user where conv = ? and user_remote_domain = ?" - -- Used when removing a federation domain, so that we can quickly list all of the affected remote users and conversations -- This returns local conversation IDs and remote users selectRemoteMembersByDomain :: PrepQuery R (Identity Domain) (ConvId, UserId, RoleName) selectRemoteMembersByDomain = "select conv, user_remote_id, conversation_role from member_remote_user where user_remote_domain = ?" -selectRemoteMembersByConvAndDomain :: PrepQuery R (ConvId, Domain) (UserId, RoleName) -selectRemoteMembersByConvAndDomain = "select user_remote_id, conversation_role from member_remote_user where conv = ? and user_remote_domain = ?" - -selectConvIdsByRemoteDomain :: PrepQuery R (Identity Domain) (Identity ConvId) -selectConvIdsByRemoteDomain = "select conv from member_remote_user where user_remote_domain = ?" - --- Return a single element, as this is being used as a SQL exists analog -checkConvForRemoteDomain :: PrepQuery R (ConvId, Domain) (Identity ConvId) -checkConvForRemoteDomain = "select conv from member_remote_user where conv = ? and user_remote_domain = ? limit 1" - -- local user with remote conversations insertUserRemoteConv :: PrepQuery W (UserId, Domain, ConvId) () diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs index 02826870a1d..12c7c31df5f 100644 --- a/services/galley/src/Galley/Effects.hs +++ b/services/galley/src/Galley/Effects.hs @@ -58,6 +58,9 @@ module Galley.Effects -- * Polysemy re-exports Member, Members, + + -- * Queueing effects + BackendNotificationQueueAccess, ) where @@ -71,7 +74,6 @@ import Galley.Effects.ClientStore import Galley.Effects.CodeStore import Galley.Effects.ConversationStore import Galley.Effects.CustomBackendStore -import Galley.Effects.DefederationNotifications import Galley.Effects.ExternalAccess import Galley.Effects.FederatorAccess import Galley.Effects.FireAndForget @@ -104,7 +106,6 @@ import Wire.Sem.Random type GalleyEffects1 = '[ BrigAccess, SparAccess, - DefederationNotifications, GundeckAccess, ExternalAccess, FederatorAccess, diff --git a/services/galley/src/Galley/Effects/BackendNotificationQueueAccess.hs b/services/galley/src/Galley/Effects/BackendNotificationQueueAccess.hs index c7ecdfcb771..ac006ded4c5 100644 --- a/services/galley/src/Galley/Effects/BackendNotificationQueueAccess.hs +++ b/services/galley/src/Galley/Effects/BackendNotificationQueueAccess.hs @@ -15,7 +15,13 @@ data BackendNotificationQueueAccess m a where KnownComponent c => Remote x -> Q.DeliveryMode -> - FedQueueClient c () -> - BackendNotificationQueueAccess m (Either FederationError ()) + FedQueueClient c a -> + BackendNotificationQueueAccess m (Either FederationError a) + EnqueueNotificationsConcurrently :: + (KnownComponent c, Foldable f, Functor f) => + Q.DeliveryMode -> + f (Remote x) -> + (Remote [x] -> FedQueueClient c a) -> + BackendNotificationQueueAccess m [Either (Remote ([x], FederationError)) (Remote a)] makeSem ''BackendNotificationQueueAccess diff --git a/services/galley/src/Galley/Effects/DefederationNotifications.hs b/services/galley/src/Galley/Effects/DefederationNotifications.hs deleted file mode 100644 index aaef53bc794..00000000000 --- a/services/galley/src/Galley/Effects/DefederationNotifications.hs +++ /dev/null @@ -1,17 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} - -module Galley.Effects.DefederationNotifications - ( DefederationNotifications (..), - sendDefederationNotifications, - sendOnConnectionRemovedNotifications, - ) -where - -import Data.Domain (Domain) -import Polysemy - -data DefederationNotifications m a where - SendDefederationNotifications :: Domain -> DefederationNotifications m () - SendOnConnectionRemovedNotifications :: Domain -> Domain -> DefederationNotifications m () - -makeSem ''DefederationNotifications diff --git a/services/galley/src/Galley/Effects/MemberStore.hs b/services/galley/src/Galley/Effects/MemberStore.hs index 1857fe9f464..0513cc6570e 100644 --- a/services/galley/src/Galley/Effects/MemberStore.hs +++ b/services/galley/src/Galley/Effects/MemberStore.hs @@ -36,13 +36,8 @@ module Galley.Effects.MemberStore checkLocalMemberRemoteConv, selectRemoteMembers, getRemoteMembersByDomain, - getRemoteMembersByConvAndDomain, getLocalMembersByDomain, - -- * Conversation checks - selectConvIdsByRemoteDomain, - checkConvForRemoteDomain, - -- * Update members setSelfMember, setOtherMember, @@ -56,7 +51,6 @@ module Galley.Effects.MemberStore -- * Delete members deleteMembers, deleteMembersInRemoteConversation, - removeRemoteDomain, ) where @@ -98,11 +92,7 @@ data MemberStore m a where LookupMLSClients :: GroupId -> MemberStore m ClientMap LookupMLSClientLeafIndices :: GroupId -> MemberStore m (ClientMap, IndexMap) GetRemoteMembersByDomain :: Domain -> MemberStore m [(ConvId, RemoteMember)] - GetRemoteMembersByConvAndDomain :: ConvId -> Domain -> MemberStore m [RemoteMember] GetLocalMembersByDomain :: Domain -> MemberStore m [(ConvId, UserId)] - RemoveRemoteDomain :: ConvId -> Domain -> MemberStore m () - SelectConvIdsByRemoteDomain :: Domain -> MemberStore m [ConvId] - CheckConvForRemoteDomain :: ConvId -> Domain -> MemberStore m (Maybe ConvId) makeSem ''MemberStore diff --git a/services/galley/src/Galley/Intra/BackendNotificationQueue.hs b/services/galley/src/Galley/Intra/BackendNotificationQueue.hs index fb2e02605fc..b9affe48585 100644 --- a/services/galley/src/Galley/Intra/BackendNotificationQueue.hs +++ b/services/galley/src/Galley/Intra/BackendNotificationQueue.hs @@ -5,6 +5,7 @@ module Galley.Intra.BackendNotificationQueue (interpretBackendNotificationQueueA import Control.Lens (view) import Control.Monad.Catch import Control.Retry +import Data.Bifunctor import Data.Domain import Data.Qualified import Galley.Effects.BackendNotificationQueueAccess (BackendNotificationQueueAccess (..)) @@ -16,7 +17,7 @@ import Network.AMQP qualified as Q import Polysemy import Polysemy.Input import System.Logger.Class qualified as Log -import UnliftIO.Timeout (timeout) +import UnliftIO import Wire.API.Federation.BackendNotifications import Wire.API.Federation.Error @@ -29,8 +30,10 @@ interpretBackendNotificationQueueAccess :: interpretBackendNotificationQueueAccess = interpret $ \case EnqueueNotification remote deliveryMode action -> do embedApp $ enqueueNotification (tDomain remote) deliveryMode action + EnqueueNotificationsConcurrently m xs rpc -> do + embedApp $ enqueueNotificationsConcurrently m xs rpc -enqueueNotification :: Domain -> Q.DeliveryMode -> FedQueueClient c () -> App (Either FederationError ()) +enqueueNotification :: Domain -> Q.DeliveryMode -> FedQueueClient c a -> App (Either FederationError a) enqueueNotification remoteDomain deliveryMode action = do mChanVar <- view rabbitmqChannel ownDomain <- view (options . settings . federationDomain) @@ -56,6 +59,19 @@ enqueueNotification remoteDomain deliveryMode action = do Just chan -> do liftIO $ enqueue chan ownDomain remoteDomain deliveryMode action +enqueueNotificationsConcurrently :: + (Foldable f, Functor f) => + Q.DeliveryMode -> + f (Remote x) -> + (Remote [x] -> FedQueueClient c a) -> + App [(Either (Remote ([x], FederationError)) (Remote a))] +enqueueNotificationsConcurrently m xs f = + pooledForConcurrentlyN 8 (bucketRemote xs) $ \r -> + bimap + (qualifyAs r . (tUnqualified r,)) + (qualifyAs r) + <$> enqueueNotification (tDomain r) m (f r) + data NoRabbitMqChannel = NoRabbitMqChannel deriving (Show) diff --git a/services/galley/src/Galley/Intra/Effects.hs b/services/galley/src/Galley/Intra/Effects.hs index c27c321c8ad..51909889e85 100644 --- a/services/galley/src/Galley/Intra/Effects.hs +++ b/services/galley/src/Galley/Intra/Effects.hs @@ -20,45 +20,27 @@ module Galley.Intra.Effects interpretSparAccess, interpretBotAccess, interpretGundeckAccess, - interpretDefederationNotifications, ) where -import Cassandra (ClientState, Consistency (LocalQuorum), Page (hasMore, nextPage, result), paginate, paramsP) -import Control.Lens ((.~)) -import Data.Id (ProviderId, ServiceId, UserId) -import Data.Range (Range (fromRange)) -import Data.Set qualified as Set import Galley.API.Error -import Galley.API.Util (localBotsAndUsers) -import Galley.Cassandra.Conversation.Members (toMember) -import Galley.Cassandra.Queries (MemberStatus, selectAllMembers) -import Galley.Cassandra.Store (embedClient) import Galley.Effects.BotAccess (BotAccess (..)) import Galley.Effects.BrigAccess (BrigAccess (..)) -import Galley.Effects.DefederationNotifications (DefederationNotifications (..)) -import Galley.Effects.ExternalAccess (ExternalAccess, deliverAsync) -import Galley.Effects.GundeckAccess (GundeckAccess (..), push1) +import Galley.Effects.GundeckAccess (GundeckAccess (..)) import Galley.Effects.SparAccess (SparAccess (..)) import Galley.Env import Galley.Intra.Client -import Galley.Intra.Push qualified as Intra import Galley.Intra.Push.Internal qualified as G import Galley.Intra.Spar import Galley.Intra.Team import Galley.Intra.User import Galley.Monad -import Galley.Types.Conversations.Members (LocalMember) import Imports import Polysemy import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog qualified as P import UnliftIO qualified -import Wire.API.Conversation (MutedStatus) -import Wire.API.Conversation.Role (RoleName) -import Wire.API.Event.Federation qualified as Federation -import Wire.API.Team.Member (ListType (ListComplete)) interpretBrigAccess :: ( Member (Embed IO) r, @@ -130,80 +112,3 @@ interpretGundeckAccess :: interpretGundeckAccess = interpret $ \case Push ps -> embedApp $ G.push ps PushSlowly ps -> embedApp $ G.pushSlowly ps - --- FUTUREWORK: --- This functions uses an in-memory set for tracking UserIds that we have already --- sent notifications to. This set will only grow throughout the lifttime of this --- function, and may cause memory & performance problems with millions of users. --- How we are tracking which users have already been sent 0, 1, or 2 defederation --- messages should be rethought to be more fault tollerant, e.g. this method doesn't --- handle the server crashing and restarting. -interpretDefederationNotifications :: - forall r a. - ( Member (Embed IO) r, - Member (Input Env) r, - Member (Input ClientState) r, - Member GundeckAccess r, - Member ExternalAccess r - ) => - Sem (DefederationNotifications ': r) a -> - Sem r a -interpretDefederationNotifications = interpret $ \case - SendDefederationNotifications domain -> - getPage - >>= void . sendNotificationPage mempty (Federation.FederationDelete domain) - SendOnConnectionRemovedNotifications domainA domainB -> - getPage - >>= void . sendNotificationPage mempty (Federation.FederationConnectionRemoved (domainA, domainB)) - where - getPage :: Sem r (Page PageType) - getPage = do - maxPage <- inputs (fromRange . currentFanoutLimit . _options) -- This is based on the limits in removeIfLargeFanout - -- selectAllMembers will return duplicate members when they are in more than one chat - -- however we need the full row to build out the bot members to send notifications - -- to them. We have to do the duplicate filtering here. - embedClient $ paginate selectAllMembers (paramsP LocalQuorum () maxPage) - pushEvents :: Set UserId -> Federation.Event -> [LocalMember] -> Sem r (Set UserId) - pushEvents seenRecipients eventData results = do - let (bots, mems) = localBotsAndUsers results - recipients = Intra.recipient <$> mems - event = Intra.FederationEvent eventData - filteredRecipients = - -- Deduplicate by UserId the page of recipients that we are working on - nubBy (\a b -> a._recipientUserId == b._recipientUserId) - -- Sort the remaining recipients by their IDs - $ - sortBy (\a b -> a._recipientUserId `compare` b._recipientUserId) - -- Filter out any recipient that we have already seen in a previous page - $ - filter (\r -> r._recipientUserId `notElem` seenRecipients) recipients - for_ (Intra.newPush ListComplete Nothing event filteredRecipients) $ \p -> do - -- Futurework: Transient or not? - -- RouteAny is used as it will wake up mobile clients - -- and notify them of the changes to federation state. - push1 $ p & Intra.pushRoute .~ Intra.RouteAny - deliverAsync (bots `zip` repeat (G.pushEventJson event)) - -- Add the users to the set of users we've sent messages to. - pure $ seenRecipients <> Set.fromList ((._recipientUserId) <$> filteredRecipients) - sendNotificationPage :: Set UserId -> Federation.Event -> Page PageType -> Sem r () - sendNotificationPage seenRecipients eventData page = do - let res = result page - mems = mapMaybe toMember res - seenRecipients' <- pushEvents seenRecipients eventData mems - when (hasMore page) $ do - page' <- embedClient $ nextPage page - sendNotificationPage seenRecipients' eventData page' - -type PageType = - ( UserId, - Maybe ServiceId, - Maybe ProviderId, - Maybe MemberStatus, - Maybe MutedStatus, - Maybe Text, - Maybe Bool, - Maybe Text, - Maybe Bool, - Maybe Text, - Maybe RoleName - ) diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 591c3e32617..ffc437f2ad8 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -51,7 +51,6 @@ import Data.ByteString qualified as BS import Data.ByteString.Conversion import Data.Code qualified as Code import Data.Domain -import Data.Either.Extra (eitherToMaybe) import Data.Id import Data.Json.Util (toBase64Text, toUTCTimeMillis) import Data.List.NonEmpty (NonEmpty (..)) @@ -176,9 +175,6 @@ tests s = test s "generate guest link forbidden when no guest or non-team-member access role" generateGuestLinkFailIfNoNonTeamMemberOrNoGuestAccess, test s "fail to add members when not connected" postMembersFail, test s "fail to add too many members" postTooManyMembersFail, - test s "add remote members" testAddRemoteMember, - test s "delete conversation with remote members" testDeleteTeamConversationWithRemoteMembers, - test s "delete conversation with unavailable remote members" testDeleteTeamConversationWithUnavailableRemoteMembers, test s "get conversations/:domain/:cnv - local" testGetQualifiedLocalConv, test s "get conversations/:domain/:cnv - local, not found" testGetQualifiedLocalConvNotFound, test s "get conversations/:domain/:cnv - local, not participating" testGetQualifiedLocalConvNotParticipating, @@ -191,9 +187,6 @@ tests s = test s "delete conversations/:domain/:cnv/members/:domain/:usr - fail, self conv" deleteMembersQualifiedFailSelf, test s "delete conversations/:domain:/cnv/members/:domain/:usr - fail, 1:1 conv" deleteMembersQualifiedFailO2O, test s "delete conversations/:domain/:cnv/members/:domain/:usr - local conv with all locals" deleteMembersConvLocalQualifiedOk, - test s "delete conversations/:domain/:cnv/members/:domain/:usr - local conv with locals and remote, delete local" deleteLocalMemberConvLocalQualifiedOk, - test s "delete conversations/:domain/:cnv/members/:domain/:usr - local conv with locals and remote, delete remote" deleteRemoteMemberConvLocalQualifiedOk, - test s "delete conversations/:domain/:cnv/members/:domain/:usr - local conv with locals and remote, delete unavailable remote" deleteUnavailableRemoteMemberConvLocalQualifiedOk, test s "delete conversations/:domain/:cnv/members/:domain/:usr - remote conv, leave conv" leaveRemoteConvQualifiedOk, test s "delete conversations/:domain/:cnv/members/:domain/:usr - remote conv, leave conv, non-existent" leaveNonExistentRemoteConv, test s "delete conversations/:domain/:cnv/members/:domain/:usr - remote conv, leave conv, denied" leaveRemoteConvDenied, @@ -202,8 +195,6 @@ tests s = test s "rename conversation (deprecated endpoint)" putConvDeprecatedRenameOk, test s "rename conversation" putConvRenameOk, test s "rename qualified conversation" putQualifiedConvRenameOk, - test s "rename qualified conversation with remote members" putQualifiedConvRenameWithRemotesOk, - test s "rename qualified conversation with unavailable remote" putQualifiedConvRenameWithRemotesUnavailable, test s "rename qualified conversation failure" putQualifiedConvRenameFailure, test s "other member update role" putOtherMemberOk, test s "qualified other member update role" putQualifiedOtherMemberOk, @@ -216,8 +207,6 @@ tests s = test s "remote conversation member update (otr hidden)" putRemoteConvMemberHiddenOk, test s "remote conversation member update (everything)" putRemoteConvMemberAllOk, test s "conversation receipt mode update" putReceiptModeOk, - test s "conversation receipt mode update with remote members" putReceiptModeWithRemotesOk, - test s "conversation receipt mode update with unavailable remote members" putReceiptModeWithRemotesUnavailable, test s "remote conversation receipt mode update" putRemoteReceiptModeOk, test s "leave connect conversation" leaveConnectConversation, test s "post conversations/:cnv/otr/message: message delivery and missing clients" postCryptoMessageVerifyMsgSentAndRejectIfMissingClient, @@ -238,8 +227,6 @@ tests s = test s "join code-access conversation - password" postJoinCodeConvWithPassword, test s "convert invite to code-access conversation" postConvertCodeConv, test s "convert code to team-access conversation" postConvertTeamConv, - test s "local and remote guests are removed when access changes" testAccessUpdateGuestRemoved, - test s "local and remote guests are removed when access changes remotes unavailable" testAccessUpdateGuestRemovedRemotesUnavailable, test s "team member can't join via guest link if access role removed" testTeamMemberCantJoinViaGuestLinkIfAccessRoleRemoved, test s "cannot join private conversation" postJoinConvFail, test s "revoke guest links for team conversation" testJoinTeamConvGuestLinksDisabled, @@ -264,23 +251,7 @@ tests s = [ test s "send typing indicators" postTypingIndicators, test s "send typing indicators without domain" postTypingIndicatorsV2, test s "send typing indicators with invalid pyaload" postTypingIndicatorsHandlesNonsense - ], - -- NOTE: These federation notification tests need to run after all of the other tests are finished. - -- This is because they will send notifications to _ALL_ registered clients for the local domain. - -- As a lot of these tests are waiting on specific notifications to come through in a specified - -- order, these tests will cause them to fail. - -- See the Tasty docs on patterns. https://hackage.haskell.org/package/tasty-1.4.3#patterns - after AllFinish "$0 !~ /federation notifications/" $ - testGroup - "federation notifications" - -- Run these tests in order by having them wait on each other. - -- The names need to be distint enough so that there isn't a loop with the regexes - [ test s "delete federation notifications" testDefederationNotifications, - after AllFinish "$0 ~ /delete federation notifications/" $ test s "connection removed notifications normal" testConnectionRemovedNotifications, - after AllFinish "$0 ~ /connection removed notifications normal/" $ test s "connection removed notifications no-op" testConnectionRemovedNotificationsNoop, - after AllFinish "$0 ~ /connection removed notifications no-op/" $ test s "connection removed notifications domain A bias" testConnectionRemovedNotificationsNoopDomainA, - after AllFinish "$0 ~ /connection removed notifications domain A bias/" $ test s "connection removed notifications domain B bias" testConnectionRemovedNotificationsNoopDomainB - ] + ] ] rb1, rb2, rb3, rb4 :: Remote Backend rb1 = @@ -1642,183 +1613,6 @@ postConvertTeamConv = do -- team members (dave) can still join postJoinCodeConv dave j !!! const 200 === statusCode --- @SF.Federation @SF.Separation @TSFI.RESTfulAPI @S2 --- --- The test asserts that, among others, remote users are removed from a --- conversation when an access update occurs that disallows guests from --- accessing. -testAccessUpdateGuestRemoved :: TestM () -testAccessUpdateGuestRemoved = do - -- alice, bob are in a team - (tid, alice, [bob]) <- createBindingTeamWithQualifiedMembers 2 - - -- charlie is a local guest - charlie <- randomQualifiedUser - connectUsers (qUnqualified alice) (pure (qUnqualified charlie)) - - -- dee is a remote guest - let remoteDomain = Domain "far-away.example.com" - dee <- Qualified <$> randomId <*> pure remoteDomain - - connectWithRemoteUser (qUnqualified alice) dee - - -- they are all in a local conversation - conv <- - responseJsonError - =<< postConvWithRemoteUsers - (qUnqualified alice) - Nothing - defNewProteusConv - { newConvQualifiedUsers = [bob, charlie, dee], - newConvTeam = Just (ConvTeamInfo tid) - } - do - -- conversation access role changes to team only - (_, reqs) <- withTempMockFederator' (mockReply EmptyResponse) $ do - putQualifiedAccessUpdate - (qUnqualified alice) - (cnvQualifiedId conv) - (ConversationAccessData mempty (Set.fromList [TeamMemberAccessRole])) - !!! const 200 === statusCode - - -- charlie and dee are kicked out - -- - -- note that removing users happens asynchronously, so this check should - -- happen while the mock federator is still available - WS.assertMatchN_ (5 # Second) [wsA, wsB, wsC] $ - wsAssertMembersLeave (cnvQualifiedId conv) alice [charlie] - WS.assertMatchN_ (5 # Second) [wsA, wsB, wsC] $ - wsAssertMembersLeave (cnvQualifiedId conv) alice [dee] - - -- dee's remote receives a notification - let compareLists [] ys = [] @?= ys - compareLists (x : xs) ys = case break (== x) ys of - (ys1, _ : ys2) -> compareLists xs (ys1 <> ys2) - _ -> assertFailure $ "Could not find " <> show x <> " in " <> show ys - liftIO $ - compareLists - ( map - ( \fr -> do - cu <- eitherDecode @ConversationUpdate (frBody fr) - pure (cu.cuOrigUserId, cu.cuAction) - ) - ( filter - ( \fr -> - frComponent fr == Galley - && frRPC fr == "on-conversation-updated" - ) - reqs - ) - ) - [ Right (alice, SomeConversationAction (sing @'ConversationRemoveMembersTag) (pure charlie)), - Right (alice, SomeConversationAction (sing @'ConversationRemoveMembersTag) (pure dee)), - Right - ( alice, - SomeConversationAction - (sing @'ConversationAccessDataTag) - ConversationAccessData - { cupAccess = mempty, - cupAccessRoles = Set.fromList [TeamMemberAccessRole] - } - ) - ] - - -- only alice and bob remain - conv2 <- - responseJsonError - =<< getConvQualified (qUnqualified alice) (cnvQualifiedId conv) - randomId <*> pure remoteDomain - - connectWithRemoteUser (qUnqualified alice) dee - - -- they are all in a local conversation - conv <- - responseJsonError - =<< postConvWithRemoteUsers - (qUnqualified alice) - Nothing - defNewProteusConv - { newConvQualifiedUsers = [bob, charlie, dee], - newConvTeam = Just (ConvTeamInfo tid) - } - do - -- conversation access role changes to team only - (_, reqs) <- withTempMockFederator' (throw $ MockErrorResponse HTTP.status503 "Down for maintenance") $ do - -- This request should still succeed even with an unresponsive federation member. - putQualifiedAccessUpdate - (qUnqualified alice) - (cnvQualifiedId conv) - (ConversationAccessData mempty (Set.fromList [TeamMemberAccessRole])) - !!! const 200 === statusCode - -- charlie and dee are kicked out - -- - -- note that removing users happens asynchronously, so this check should - -- happen while the mock federator is still available - WS.assertMatchN_ (5 # Second) [wsA, wsB, wsC] $ - wsAssertMembersLeave (cnvQualifiedId conv) alice [charlie] - WS.assertMatchN_ (5 # Second) [wsA, wsB, wsC] $ - wsAssertMembersLeave (cnvQualifiedId conv) alice [dee] - - let compareLists [] ys = [] @?= ys - compareLists (x : xs) ys = case break (== x) ys of - (ys1, _ : ys2) -> compareLists xs (ys1 <> ys2) - _ -> assertFailure $ "Could not find " <> show x <> " in " <> show ys - liftIO $ - compareLists - ( map - ( \fr -> do - cu <- eitherDecode @ConversationUpdate (frBody fr) - pure (cu.cuOrigUserId, cu.cuAction) - ) - ( filter - ( \fr -> - frComponent fr == Galley - && frRPC fr == "on-conversation-updated" - ) - reqs - ) - ) - [ Right (alice, SomeConversationAction (sing @'ConversationRemoveMembersTag) (pure charlie)), - Right (alice, SomeConversationAction (sing @'ConversationRemoveMembersTag) (pure dee)), - Right - ( alice, - SomeConversationAction - (sing @'ConversationAccessDataTag) - ConversationAccessData - { cupAccess = mempty, - cupAccessRoles = Set.fromList [TeamMemberAccessRole] - } - ) - ] - -- only alice and bob remain - conv2 <- - responseJsonError - =<< getConvQualified (qUnqualified alice) (cnvQualifiedId conv) - viewFederationDomain deleteMemberQualified alice qalice qc !!! const 403 === statusCode -testAddRemoteMember :: TestM () -testAddRemoteMember = do - qalice <- randomQualifiedUser - let alice = qUnqualified qalice - let localDomain = qDomain qalice - bobId <- randomId - let remoteDomain = Domain "far-away.example.com" - remoteBob = Qualified bobId remoteDomain - convId <- decodeConvId <$> postConv alice [] (Just "remote gossip") [] Nothing Nothing - let qconvId = Qualified convId localDomain - - postQualifiedMembers alice (remoteBob :| []) qconvId !!! do - const 403 === statusCode - const (Right (Just "not-connected")) === fmap (view (at "label")) . responseJsonEither @Object - - connectWithRemoteUser alice remoteBob - - (resp, reqs) <- - withTempMockFederator' (respond remoteBob) $ - postQualifiedMembers alice (remoteBob :| []) qconvId - getConvQualified alice qconvId - liftIO $ do - let actual = cmOthers $ cnvMembers conv - let expected = [OtherMember remoteBob Nothing roleNameWireAdmin] - assertEqual "other members should include remoteBob" expected actual - where - respond :: Qualified UserId -> Mock LByteString - respond bob = - asum - [ getNotFullyConnectedBackendsMock, - guardComponent Brig *> mockReply [mkProfile bob (Name "bob")], - "on-conversation-updated" ~> () - ] - -testDeleteTeamConversationWithRemoteMembers :: TestM () -testDeleteTeamConversationWithRemoteMembers = do - (alice, tid) <- createBindingTeam - localDomain <- viewFederationDomain - let qalice = Qualified alice localDomain - - bobId <- randomId - let remoteDomain = Domain "far-away.example.com" - remoteBob = Qualified bobId remoteDomain - - convId <- decodeConvId <$> postTeamConv tid alice [] (Just "remote gossip") [] Nothing Nothing - let qconvId = Qualified convId localDomain - - connectWithRemoteUser alice remoteBob - - let mock = getNotFullyConnectedBackendsMock <|> "api-version" ~> EmptyResponse - (_, received) <- withTempMockFederator' mock $ do - postQualifiedMembers alice (remoteBob :| []) qconvId - !!! const 200 === statusCode - - deleteTeamConv tid convId alice - !!! const 200 === statusCode - - liftIO $ do - let convUpdates = mapMaybe (eitherToMaybe . parseFedRequest) received - convUpdate <- case filter ((== SomeConversationAction (sing @'ConversationDeleteTag) ()) . cuAction) convUpdates of - [] -> assertFailure "No ConversationUpdate requests received" - [convDelete] -> pure convDelete - _ -> assertFailure "Multiple ConversationUpdate requests received" - cuAlreadyPresentUsers convUpdate @?= [bobId] - cuOrigUserId convUpdate @?= qalice - -testDeleteTeamConversationWithUnavailableRemoteMembers :: TestM () -testDeleteTeamConversationWithUnavailableRemoteMembers = do - (alice, tid) <- createBindingTeam - localDomain <- viewFederationDomain - let qalice = Qualified alice localDomain - - bobId <- randomId - let remoteDomain = Domain "far-away.example.com" - remoteBob = Qualified bobId remoteDomain - - convId <- decodeConvId <$> postTeamConv tid alice [] (Just "remote gossip") [] Nothing Nothing - let qconvId = Qualified convId localDomain - - connectWithRemoteUser alice remoteBob - - let mock = - getNotFullyConnectedBackendsMock - <|> - -- Mock an unavailable federation server for the deletion call - (guardRPC "on-conversation-updated" *> throw (MockErrorResponse HTTP.status503 "Down for maintenance.")) - <|> (guardRPC "delete-team-conversation" *> throw (MockErrorResponse HTTP.status503 "Down for maintenance.")) - (_, received) <- withTempMockFederator' mock $ do - postQualifiedMembers alice (remoteBob :| []) qconvId - !!! const 200 === statusCode - - deleteTeamConv tid convId alice - !!! const 200 === statusCode - liftIO $ do - let convUpdates = mapMaybe (eitherToMaybe . parseFedRequest) received - convUpdate <- case filter ((== SomeConversationAction (sing @'ConversationDeleteTag) ()) . cuAction) convUpdates of - [] -> assertFailure "No ConversationUpdate requests received" - [convDelete] -> pure convDelete - _ -> assertFailure "Multiple ConversationUpdate requests received" - cuAlreadyPresentUsers convUpdate @?= [bobId] - cuOrigUserId convUpdate @?= qalice - testGetQualifiedLocalConv :: TestM () testGetQualifiedLocalConv = do alice <- randomUser @@ -3114,182 +2794,6 @@ deleteMembersConvLocalQualifiedOk = do deleteMemberQualified alice qAlice qconv !!! const 200 === statusCode deleteMemberQualified alice qAlice qconv !!! const 404 === statusCode --- Creates a conversation with three users. Alice and Bob are on the local --- domain, while Eve is on a remote domain. It uses a qualified endpoint for --- removing Bob from the conversation: --- --- DELETE /conversations/:domain/:cnv/members/:domain/:usr -deleteLocalMemberConvLocalQualifiedOk :: TestM () -deleteLocalMemberConvLocalQualifiedOk = do - localDomain <- viewFederationDomain - [alice, bob] <- randomUsers 2 - eve <- randomId - let [qAlice, qBob] = (`Qualified` localDomain) <$> [alice, bob] - remoteDomain = Domain "far-away.example.com" - qEve = Qualified eve remoteDomain - - connectUsers alice (singleton bob) - connectWithRemoteUser alice qEve - convId <- - decodeConvId - <$> postConvWithRemoteUsers - alice - Nothing - defNewProteusConv {newConvQualifiedUsers = [qBob, qEve]} - let qconvId = Qualified convId localDomain - - let mockReturnEve = - mockedFederatedBrigResponse [(qEve, "Eve")] - <|> mockReply EmptyResponse - (respDel, fedRequests) <- - withTempMockFederator' mockReturnEve $ - deleteMemberQualified alice qBob qconvId - let [galleyFederatedRequest] = fedRequestsForDomain remoteDomain Galley fedRequests - assertRemoveUpdate galleyFederatedRequest qconvId qAlice [qUnqualified qEve] qBob - - liftIO $ do - statusCode respDel @?= 200 - case responseJsonEither respDel of - Left err -> assertFailure err - Right e -> assertLeaveEvent qconvId qAlice [qBob] e - - -- Now that Bob is gone, try removing him once again - deleteMemberQualified alice qBob qconvId !!! do - const 204 === statusCode - const Nothing === responseBody - --- Creates a conversation with five users. Alice and Bob are on the local --- domain. Chad and Dee are on far-away-1.example.com. Eve is on --- far-away-2.example.com. It uses a qualified endpoint to remove Chad from the --- conversation: --- --- DELETE /conversations/:domain/:cnv/members/:domain/:usr -deleteRemoteMemberConvLocalQualifiedOk :: TestM () -deleteRemoteMemberConvLocalQualifiedOk = do - localDomain <- viewFederationDomain - [alice, bob] <- randomUsers 2 - let [qAlice, qBob] = (`Qualified` localDomain) <$> [alice, bob] - remoteDomain1 = Domain "far-away-1.example.com" - remoteDomain2 = Domain "far-away-2.example.com" - qChad <- (`Qualified` remoteDomain1) <$> randomId - qDee <- (`Qualified` remoteDomain1) <$> randomId - qEve <- (`Qualified` remoteDomain2) <$> randomId - connectUsers alice (singleton bob) - mapM_ (connectWithRemoteUser alice) [qChad, qDee, qEve] - - let mockedResponse = do - guardRPC "get-users-by-ids" - d <- frTargetDomain <$> getRequest - asum - [ guard (d == remoteDomain1) - *> mockReply [mkProfile qChad (Name "Chad"), mkProfile qDee (Name "Dee")], - guard (d == remoteDomain2) - *> mockReply [mkProfile qEve (Name "Eve")] - ] - (convId, _) <- - withTempMockFederator' (getNotFullyConnectedBackendsMock <|> mockedResponse <|> mockReply EmptyResponse) $ - fmap decodeConvId $ - postConvQualified - alice - Nothing - defNewProteusConv {newConvQualifiedUsers = [qBob, qChad, qDee, qEve]} - mockedResponse <|> mockReply EmptyResponse) $ - deleteMemberQualified alice qChad qconvId - liftIO $ do - statusCode respDel @?= 200 - case responseJsonEither respDel of - Left err -> assertFailure err - Right e -> assertLeaveEvent qconvId qAlice [qChad] e - - remote1GalleyFederatedRequest <- - assertOne (filter ((== "on-conversation-updated") . frRPC) (fedRequestsForDomain remoteDomain1 Galley federatedRequests)) - remote2GalleyFederatedRequest <- - assertOne (filter ((== "on-conversation-updated") . frRPC) (fedRequestsForDomain remoteDomain2 Galley federatedRequests)) - assertRemoveUpdate remote1GalleyFederatedRequest qconvId qAlice [qUnqualified qChad, qUnqualified qDee] qChad - assertRemoveUpdate remote2GalleyFederatedRequest qconvId qAlice [qUnqualified qEve] qChad - - -- Now that Chad is gone, try removing him once again - deleteMemberQualified alice qChad qconvId !!! do - const 204 === statusCode - const Nothing === responseBody - --- Creates a conversation with five users. Alice and Bob are on the local --- domain. Chad and Dee are on far-away-1.example.com. Eve is on --- far-away-2.example.com. It uses a qualified endpoint to remove Chad from the --- conversation. The federator for far-away-2.example.com isn't availabe: --- --- DELETE /conversations/:domain/:cnv/members/:domain/:usr -deleteUnavailableRemoteMemberConvLocalQualifiedOk :: TestM () -deleteUnavailableRemoteMemberConvLocalQualifiedOk = do - localDomain <- viewFederationDomain - [alice, bob] <- randomUsers 2 - let [qAlice, qBob] = (`Qualified` localDomain) <$> [alice, bob] - remoteDomain1 = Domain "far-away-1.example.com" - remoteDomain2 = Domain "far-away-2.example.com" - qChad <- (`Qualified` remoteDomain1) <$> randomId - qDee <- (`Qualified` remoteDomain1) <$> randomId - qEve <- (`Qualified` remoteDomain2) <$> randomId - connectUsers alice (singleton bob) - mapM_ (connectWithRemoteUser alice) [qChad, qDee, qEve] - - let mockedGetUsers remote2Response = do - guardRPC "get-users-by-ids" - d <- frTargetDomain <$> getRequest - asum - [ guard (d == remoteDomain1) - *> mockReply [mkProfile qChad (Name "Chad"), mkProfile qDee (Name "Dee")], - guard (d == remoteDomain2) - *> remote2Response - ] - mockedCreateConvGetUsers = - mockedGetUsers (mockReply [mkProfile qEve (Name "Eve")]) - mockedRemMemGetUsers = - mockedGetUsers (throw (MockErrorResponse HTTP.status503 "Down for maintenance.")) - mockedOther = do - d <- frTargetDomain <$> getRequest - asum - [ guard (d == remoteDomain1) - *> mockReply EmptyResponse, - guard (d == remoteDomain2) - *> asum - [ guardRPC "on-conversation-created" *> mockReply EmptyResponse, - guardRPC "on-conversation-updated" *> mockReply EmptyResponse, - throw $ MockErrorResponse HTTP.status503 "Down for maintenance." - ] - ] - convId <- - fmap decodeConvId $ - postConvWithRemoteUsersGeneric - (mockedCreateConvGetUsers <|> mockedOther) - alice - Nothing - defNewProteusConv {newConvQualifiedUsers = [qBob, qChad, qDee, qEve]} - mockedOther) $ - deleteMemberQualified alice qChad qconvId - liftIO $ do - statusCode respDel @?= 200 - case responseJsonEither respDel of - Left err -> assertFailure err - Right e -> assertLeaveEvent qconvId qAlice [qChad] e - - let [remote1GalleyFederatedRequest] = fedRequestsForDomain remoteDomain1 Galley federatedRequests - [remote2GalleyFederatedRequest] = fedRequestsForDomain remoteDomain2 Galley federatedRequests - assertRemoveUpdate remote1GalleyFederatedRequest qconvId qAlice [qUnqualified qChad, qUnqualified qDee] qChad - assertRemoveUpdate remote2GalleyFederatedRequest qconvId qAlice [qUnqualified qEve] qChad - - -- Now that Chad is gone, try removing him once again - deleteMemberQualified alice qChad qconvId !!! do - const 204 === statusCode - const Nothing === responseBody - -- Alice, a local user, leaves a remote conversation. Bob's domain is the same -- as that of the conversation. The test uses the following endpoint: -- @@ -3455,86 +2959,6 @@ putQualifiedConvRenameOk = do evtFrom e @?= qbob evtData e @?= EdConvRename (ConversationRename "gossip++") -putQualifiedConvRenameWithRemotesOk :: TestM () -putQualifiedConvRenameWithRemotesOk = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - let bob = qUnqualified qbob - - connectWithRemoteUser bob qalice - - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice]} - do - (_, requests) <- - withTempMockFederator' (mockReply EmptyResponse) $ - putQualifiedConversationName bob qconv "gossip++" !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode @ConversationUpdate . frBody $ req - cu.cuConvId @?= qUnqualified qconv - cu.cuAction @?= SomeConversationAction (sing @'ConversationRenameTag) (ConversationRename "gossip++") - - void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvRename - evtFrom e @?= qbob - evtData e @?= EdConvRename (ConversationRename "gossip++") - -putQualifiedConvRenameWithRemotesUnavailable :: TestM () -putQualifiedConvRenameWithRemotesUnavailable = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - let bob = qUnqualified qbob - - connectWithRemoteUser bob qalice - - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice]} - do - (_, requests) <- - withTempMockFederator' (throw $ MockErrorResponse HTTP.status503 "Down for maintenance") $ - putQualifiedConversationName bob qconv "gossip++" !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode @ConversationUpdate . frBody $ req - cu.cuConvId @?= qUnqualified qconv - cu.cuAction @?= SomeConversationAction (sing @'ConversationRenameTag) (ConversationRename "gossip++") - - void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvRename - evtFrom e @?= qbob - evtData e @?= EdConvRename (ConversationRename "gossip++") - putConvDeprecatedRenameOk :: TestM () putConvDeprecatedRenameOk = do c <- view tsCannon @@ -3981,90 +3405,6 @@ putRemoteReceiptModeOk = do WS.assertMatch_ (5 # Second) wsAdam $ \n -> do liftIO $ wsAssertConvReceiptModeUpdate qconv qalice newReceiptMode n -putReceiptModeWithRemotesOk :: TestM () -putReceiptModeWithRemotesOk = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - let bob = qUnqualified qbob - - connectWithRemoteUser bob qalice - - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR c bob $ \wsB -> do - (_, requests) <- - withTempMockFederator' (mockReply EmptyResponse) $ - putQualifiedReceiptMode bob qconv (ReceiptMode 43) !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode @ConversationUpdate . frBody $ req - cu.cuConvId @?= qUnqualified qconv - cu.cuAction - @?= SomeConversationAction (sing @'ConversationReceiptModeUpdateTag) (ConversationReceiptModeUpdate (ReceiptMode 43)) - - void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvReceiptModeUpdate - evtFrom e @?= qbob - evtData e - @?= EdConvReceiptModeUpdate - (ConversationReceiptModeUpdate (ReceiptMode 43)) - -putReceiptModeWithRemotesUnavailable :: TestM () -putReceiptModeWithRemotesUnavailable = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - let bob = qUnqualified qbob - - connectWithRemoteUser bob qalice - - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR c bob $ \wsB -> do - (_, requests) <- - withTempMockFederator' (throw $ MockErrorResponse HTTP.status503 "Down for maintenance") $ - putQualifiedReceiptMode bob qconv (ReceiptMode 43) !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode @ConversationUpdate . frBody $ req - cu.cuConvId @?= qUnqualified qconv - cu.cuAction - @?= SomeConversationAction (sing @'ConversationReceiptModeUpdateTag) (ConversationReceiptModeUpdate (ReceiptMode 43)) - - void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvReceiptModeUpdate - evtFrom e @?= qbob - evtData e - @?= EdConvReceiptModeUpdate - (ConversationReceiptModeUpdate (ReceiptMode 43)) - postTypingIndicatorsV2 :: TestM () postTypingIndicatorsV2 = do c <- view tsCannon @@ -4399,323 +3739,3 @@ testOne2OneConversationRequest shouldBeLocal actor desired = do pure $ statusCode resp == 200 liftIO $ found @?= ((actor, desired) == (LocalActor, Included)) ) - --- Testing defederation notifications. The important thing to note for all --- of this is that when defederating from a remote domain only _2_ notifications --- are sent, and both are identical. One notification is at the start of --- defederation, and one is sent at the end of defederation. No other --- notifications about users being removed from conversations, or conversations --- being deleted are sent. We are do not want to DOS either our local clients, --- nor our own services. -testDefederationNotifications :: TestM () -testDefederationNotifications = do - -- alice, bob are in a team - (tid, alice, [bob]) <- createBindingTeamWithQualifiedMembers 2 - - -- charlie is a local guest - charlie <- randomQualifiedUser - connectUsers (qUnqualified alice) (pure (qUnqualified charlie)) - - let remoteDomain = Domain "far-away.example.com" - -- This variable should be commented out if the below - -- section is used to insert users to the database. - users = [] - -- This section of code is useful to massively increase - -- the amount of users in the testing database. This is - -- useful for checking that notifications are being fanned - -- out correctly, and that all users are sent a - -- notification. If the database already has a large - -- amount of users then this can be left out and will also - -- allow this test to run faster. - -- count = 10000 - -- users <- replicateM count randomQualifiedUser - -- replicateM_ count $ do - -- connectWithRemoteUser (qUnqualified alice) =<< - -- Qualified <$> randomId <*> pure remoteDomain - - -- dee is a remote guest - dee <- Qualified <$> randomId <*> pure remoteDomain - - connectWithRemoteUser (qUnqualified alice) dee - - -- they are all in a local conversation - conv <- - responseJsonError - =<< postConvWithRemoteUsers - (qUnqualified alice) - Nothing - defNewProteusConv - { newConvQualifiedUsers = [bob, charlie, dee], - newConvTeam = Just (ConvTeamInfo tid) - } - users) $ \(wsA : wsB : wsC : wsD : wsUsers) -> do - -- conversation access role changes to team only - (_, reqs) <- withTempMockFederator' (mockReply ()) $ do - -- Delete the domain that Dee lives on - deleteFederation remoteDomain !!! const 200 === statusCode - -- First notification to local clients - WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC] <> wsUsers) $ - wsAssertFederationDeleted remoteDomain - -- Second notification to local clients - WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC] <> wsUsers) $ - wsAssertFederationDeleted remoteDomain - -- dee's remote doesn't receive a notification - WS.assertNoEvent (5 # Second) [wsD] - -- There should be not requests out to the federtaion domain - liftIO $ reqs @?= [] - - -- only alice, bob, and charlie remain - conv2 <- - responseJsonError - =<< getConvQualified (qUnqualified alice) (cnvQualifiedId conv) - cmOthers (cnvMembers conv2)) @?= sort [bob, charlie] - --- Testing defederation notifications. The important thing to note for all --- of this is that when defederating from a remote domain only _2_ notifications --- are sent, and both are identical. One notification is at the start of --- defederation, and one is sent at the end of defederation. No other --- notifications about users being removed from conversations, or conversations --- being deleted are sent. We are do not want to DOS either our local clients, --- nor our own services. --- There are four tests here. - --- * A normal run where we have users from both remote domains in a conversation. Both remote users should be removed. - --- * A no-op run where we have no remote users in the conversation. The conversation remains unchanged. - --- * A domain A biased run where we have a conversation with a remote member from domain A, but none from domain B. The conversation remains unchanged. - --- * A domain B biased run where we have a conversation with a remote member from domain B, but none from domain A. The conversation remains unchanged. - -testConnectionRemovedNotifications :: TestM () -testConnectionRemovedNotifications = do - -- alice, bob are in a team - (tid, alice, [bob]) <- createBindingTeamWithQualifiedMembers 2 - - -- charlie is a local guest - charlie <- randomQualifiedUser - connectUsers (qUnqualified alice) (pure (qUnqualified charlie)) - - let remoteDomain1 = Domain "far-away.example.com" - remoteDomain2 = Domain "far-away-2.example.com" - remoteDomain3 = Domain "far-away-3.example.com" - -- dee and erin are remote guests - dee <- Qualified <$> randomId <*> pure remoteDomain1 - erin <- Qualified <$> randomId <*> pure remoteDomain2 - -- frank is a remote we are going to keep around. - frank <- Qualified <$> randomId <*> pure remoteDomain3 - - -- Set up the federation - addFederation remoteDomain1 !!! const 200 === statusCode - addFederation remoteDomain2 !!! const 200 === statusCode - addFederation remoteDomain3 !!! const 200 === statusCode - - connectWithRemoteUser (qUnqualified alice) dee - connectWithRemoteUser (qUnqualified alice) erin - connectWithRemoteUser (qUnqualified alice) frank - - -- they are all in a local conversation - conv <- - responseJsonError - =<< postConvWithRemoteUsers - (qUnqualified alice) - Nothing - defNewProteusConv - { newConvQualifiedUsers = [bob, charlie, dee, erin, frank], - newConvTeam = Just (ConvTeamInfo tid) - } - do - -- conversation access role changes to team only - (_, reqs) <- withTempMockFederator' (mockReply ()) $ do - -- Remove the connection - connectionRemovedFederation remoteDomain1 remoteDomain2 !!! const 200 === statusCode - -- First notification to local clients - WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ - wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 - -- Second notification to local clients - WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ - wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 - -- dee, erin, and frank's remotes don't receive a notification - WS.assertNoEvent (5 # Second) [wsD, wsE, wsF] - -- There should be not requests out to the federtaion domain - liftIO $ reqs @?= [] - - -- only alice, bob, charlie, and frank remain - conv2 <- - responseJsonError - =<< getConvQualified (qUnqualified alice) (cnvQualifiedId conv) - cmOthers (cnvMembers conv2)) @?= sort [bob, charlie, frank] - -testConnectionRemovedNotificationsNoop :: TestM () -testConnectionRemovedNotificationsNoop = do - -- alice, bob are in a team - (tid, alice, [bob]) <- createBindingTeamWithQualifiedMembers 2 - - -- charlie is a local guest - charlie <- randomQualifiedUser - connectUsers (qUnqualified alice) (pure (qUnqualified charlie)) - - let remoteDomain1 = Domain "far-away.example.com" - remoteDomain2 = Domain "far-away-2.example.com" - - -- Setup federation - addFederation remoteDomain1 !!! const 200 === statusCode - addFederation remoteDomain2 !!! const 200 === statusCode - - -- they are all in a local conversation - conv <- - responseJsonError - =<< postConvWithRemoteUsers - (qUnqualified alice) - Nothing - defNewProteusConv - { newConvQualifiedUsers = [bob, charlie], - newConvTeam = Just (ConvTeamInfo tid) - } - do - -- conversation access role changes to team only - (_, reqs) <- withTempMockFederator' (mockReply ()) $ do - -- Remove the connection - connectionRemovedFederation remoteDomain1 remoteDomain2 !!! const 200 === statusCode - -- First notification to local clients - WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ - wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 - -- Second notification to local clients - WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ - wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 - -- There should be not requests out to the federtaion domain - liftIO $ reqs @?= [] - - -- only alice, bob, and charlie remain - conv2 <- - responseJsonError - =<< getConvQualified (qUnqualified alice) (cnvQualifiedId conv) - cmOthers (cnvMembers conv2)) @?= sort [bob, charlie] - -testConnectionRemovedNotificationsNoopDomainA :: TestM () -testConnectionRemovedNotificationsNoopDomainA = do - -- alice, bob are in a team - (tid, alice, [bob]) <- createBindingTeamWithQualifiedMembers 2 - - -- charlie is a local guest - charlie <- randomQualifiedUser - connectUsers (qUnqualified alice) (pure (qUnqualified charlie)) - - let remoteDomain1 = Domain "far-away.example.com" - remoteDomain2 = Domain "far-away-2.example.com" - - -- Setup federation - addFederation remoteDomain1 !!! const 200 === statusCode - addFederation remoteDomain2 !!! const 200 === statusCode - - -- dee is a remote guest - dee <- Qualified <$> randomId <*> pure remoteDomain1 - - connectWithRemoteUser (qUnqualified alice) dee - - -- they are all in a local conversation - conv <- - responseJsonError - =<< postConvWithRemoteUsers - (qUnqualified alice) - Nothing - defNewProteusConv - { newConvQualifiedUsers = [bob, charlie, dee], - newConvTeam = Just (ConvTeamInfo tid) - } - do - -- conversation access role changes to team only - (_, reqs) <- withTempMockFederator' (mockReply ()) $ do - -- Remove the connection - connectionRemovedFederation remoteDomain1 remoteDomain2 !!! const 200 === statusCode - -- First notification to local clients - WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ - wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 - -- Second notification to local clients - WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ - wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 - -- dee and remote doesn't receive a notification - WS.assertNoEvent (5 # Second) [wsD] - -- There should be not requests out to the federtaion domain - liftIO $ reqs @?= [] - - -- alice, bob, charlie, and dee remain - conv2 <- - responseJsonError - =<< getConvQualified (qUnqualified alice) (cnvQualifiedId conv) - cmOthers (cnvMembers conv2)) @?= sort [bob, charlie, dee] - -testConnectionRemovedNotificationsNoopDomainB :: TestM () -testConnectionRemovedNotificationsNoopDomainB = do - -- alice, bob are in a team - (tid, alice, [bob]) <- createBindingTeamWithQualifiedMembers 2 - - -- charlie is a local guest - charlie <- randomQualifiedUser - connectUsers (qUnqualified alice) (pure (qUnqualified charlie)) - - let remoteDomain1 = Domain "far-away.example.com" - remoteDomain2 = Domain "far-away-2.example.com" - - -- Setup federation - addFederation remoteDomain1 !!! const 200 === statusCode - addFederation remoteDomain2 !!! const 200 === statusCode - - -- erin is a remote guest - erin <- Qualified <$> randomId <*> pure remoteDomain2 - - connectWithRemoteUser (qUnqualified alice) erin - - -- they are all in a local conversation - conv <- - responseJsonError - =<< postConvWithRemoteUsers - (qUnqualified alice) - Nothing - defNewProteusConv - { newConvQualifiedUsers = [bob, charlie, erin], - newConvTeam = Just (ConvTeamInfo tid) - } - do - -- conversation access role changes to team only - (_, reqs) <- withTempMockFederator' (mockReply ()) $ do - -- Remove the connection - connectionRemovedFederation remoteDomain1 remoteDomain2 !!! const 200 === statusCode - -- First notification to local clients - WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ - wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 - -- Second notification to local clients - WS.assertMatchN_ (5 # Second) ([wsA, wsB, wsC]) $ - wsAssertFederationConnectionRemoved remoteDomain1 remoteDomain2 - -- erin's remote doesn't receive a notification - WS.assertNoEvent (5 # Second) [wsE] - -- There should be not requests out to the federtaion domain - liftIO $ reqs @?= [] - - -- alice, bob, charlie, and erin remain - conv2 <- - responseJsonError - =<< getConvQualified (qUnqualified alice) (cnvQualifiedId conv) - cmOthers (cnvMembers conv2)) @?= sort [bob, charlie, erin] - --- @END diff --git a/services/galley/test/integration/API/Federation.hs b/services/galley/test/integration/API/Federation.hs index 6e6d466a002..6c71f87448b 100644 --- a/services/galley/test/integration/API/Federation.hs +++ b/services/galley/test/integration/API/Federation.hs @@ -23,7 +23,6 @@ import Bilge hiding (head) import Bilge.Assert import Control.Exception import Control.Lens hiding ((#)) -import Data.Aeson qualified as A import Data.ByteString.Conversion (toByteString') import Data.Domain import Data.Id @@ -34,7 +33,6 @@ import Data.List1 qualified as List1 import Data.Map qualified as Map import Data.ProtoLens qualified as Protolens import Data.Qualified -import Data.Range import Data.Set qualified as Set import Data.Singletons import Data.Time.Clock @@ -83,13 +81,10 @@ tests s = test s "POST /federation/on-conversation-updated : Notify local user about receipt mode update" notifyReceiptMode, test s "POST /federation/on-conversation-updated : Notify local user about access update" notifyAccess, test s "POST /federation/on-conversation-updated : Notify local users about a deleted conversation" notifyDeletedConversation, - test s "POST /federation/leave-conversation : Success" leaveConversationSuccess, test s "POST /federation/leave-conversation : Non-existent" leaveConversationNonExistent, test s "POST /federation/leave-conversation : Invalid type" leaveConversationInvalidType, test s "POST /federation/on-message-sent : Receive a message from another backend" onMessageSent, test s "POST /federation/send-message : Post a message sent from another backend" sendMessage, - test s "POST /federation/on-user-deleted-conversations : Remove deleted remote user from local conversations" onUserDeleted, - test s "POST /federation/update-conversation : Update local conversation by a remote admin " updateConversationByRemoteAdmin, test s "POST /federation/on-conversation-updated : Notify local user about conversation rename with an unavailable federator" notifyConvRenameUnavailable, test s "POST /federation/on-conversation-updated : Notify local user about message timer update with an unavailable federator" notifyMessageTimerUnavailable, test s "POST /federation/on-conversation-updated : Notify local user about receipt mode update with an unavailable federator" notifyReceiptModeUnavailable, @@ -711,69 +706,6 @@ addRemoteUser = do WS.assertNoEvent (1 # Second) [wsC] WS.assertNoEvent (1 # Second) [wsF] -leaveConversationSuccess :: TestM () -leaveConversationSuccess = do - localDomain <- viewFederationDomain - c <- view tsCannon - [alice, bob] <- randomUsers 2 - let qBob = Qualified bob localDomain - remoteDomain1 = Domain "far-away-1.example.com" - remoteDomain2 = Domain "far-away-2.example.com" - qChad <- (`Qualified` remoteDomain1) <$> randomId - qDee <- (`Qualified` remoteDomain1) <$> randomId - qEve <- (`Qualified` remoteDomain2) <$> randomId - connectUsers alice (singleton bob) - connectWithRemoteUser alice qChad - connectWithRemoteUser alice qDee - connectWithRemoteUser alice qEve - - let mock = do - guardRPC "get-users-by-ids" - d <- frTargetDomain <$> getRequest - asum - [ guard (d == remoteDomain1) - *> mockReply [mkProfile qChad (Name "Chad"), mkProfile qDee (Name "Dee")], - guard (d == remoteDomain2) - *> mockReply [mkProfile qEve (Name "Eve")] - ] - - convId <- - decodeConvId - <$> postConvWithRemoteUsersGeneric - (mock <|> mockReply EmptyResponse) - alice - Nothing - defNewProteusConv - { newConvQualifiedUsers = [qBob, qChad, qDee, qEve] - } - let qconvId = Qualified convId localDomain - - (_, federatedRequests) <- - WS.bracketR2 c alice bob $ \(wsAlice, wsBob) -> do - withTempMockFederator' ("get-not-fully-connected-backends" ~> NonConnectedBackends mempty <|> mock <|> mockReply EmptyResponse) $ do - g <- viewGalley - let leaveRequest = FedGalley.LeaveConversationRequest convId (qUnqualified qChad) - respBS <- - post - ( g - . paths ["federation", "leave-conversation"] - . content "application/json" - . header "Wire-Origin-Domain" (toByteString' remoteDomain1) - . json leaveRequest - ) - Causes Alice to be notified --- - groupConvId -> Causes Alice and Alex to be notified --- - extraConvId -> Ignored --- - noBobConvId -> Ignored -onUserDeleted :: TestM () -onUserDeleted = do - cannon <- view tsCannon - let bDomain = Domain "b.far-away.example.com" - cDomain = Domain "c.far-away.example.com" - - alice <- qTagUnsafe <$> randomQualifiedUser - alex <- randomQualifiedUser - (bob, ooConvId) <- generateRemoteAndConvIdWithDomain bDomain True alice - bart <- randomQualifiedId bDomain - carl <- randomQualifiedId cDomain - - connectWithRemoteUser (tUnqualified alice) (tUntagged bob) - connectUsers (tUnqualified alice) (pure (qUnqualified alex)) - connectWithRemoteUser (tUnqualified alice) bart - connectWithRemoteUser (tUnqualified alice) carl - - -- create 1-1 conversation between alice and bob - createOne2OneConvWithRemote alice bob - - -- create group conversation with everybody - groupConvId <- WS.bracketR cannon (tUnqualified alice) $ \wsAlice -> do - convId <- - decodeQualifiedConvId - <$> ( postConvWithRemoteUsers - (tUnqualified alice) - Nothing - defNewProteusConv {newConvQualifiedUsers = [tUntagged bob, alex, bart, carl]} - do - convId <- - fmap decodeQualifiedConvId $ - postConvQualified - (tUnqualified alice) - Nothing - defNewProteusConv {newConvQualifiedUsers = [alex]} - do - (resp, rpcCalls) <- withTempMockFederator' (mockReply EmptyResponse) $ do - let udcn = - FedGalley.UserDeletedConversationsNotification - { FedGalley.user = tUnqualified bob, - FedGalley.conversations = - unsafeRange - [ qUnqualified ooConvId, - qUnqualified groupConvId, - extraConvId, - qUnqualified noBobConvId - ] - } - g <- viewGalley - responseJsonError - =<< post - ( g - . paths ["federation", "on-user-deleted-conversations"] - . content "application/json" - . header "Wire-Origin-Domain" (toByteString' (tDomain bob)) - . json udcn - ) - show rpcCalls) 1 (length rpcCalls) - - -- Assertions about RPC to 'cDomain' - cDomainRPC <- assertOne $ filter (\c -> frTargetDomain c == cDomain) rpcCalls - cDomainRPCReq <- assertRight $ parseFedRequest cDomainRPC - FedGalley.cuOrigUserId cDomainRPCReq @?= tUntagged bob - FedGalley.cuConvId cDomainRPCReq @?= qUnqualified groupConvId - FedGalley.cuAlreadyPresentUsers cDomainRPCReq @?= [qUnqualified carl] - FedGalley.cuAction cDomainRPCReq @?= SomeConversationAction (sing @'ConversationLeaveTag) () - --- | We test only ReceiptMode update here --- --- A : local domain, owns the conversation --- B : bob is an admin of the converation --- C : charlie is a regular member of the conversation -updateConversationByRemoteAdmin :: TestM () -updateConversationByRemoteAdmin = do - c <- view tsCannon - (alice, qalice) <- randomUserTuple - - let bdomain = Domain "b.example.com" - cdomain = Domain "c.example.com" - qbob <- randomQualifiedId bdomain - qcharlie <- randomQualifiedId cdomain - mapM_ (connectWithRemoteUser alice) [qbob, qcharlie] - - let convName = "Test Conv" - WS.bracketR c alice $ \wsAlice -> do - (rsp, _federatedRequests) <- do - let mock = ("get-not-fully-connected-backends" ~> NonConnectedBackends mempty) <|> mockReply EmptyResponse - withTempMockFederator' mock $ do - postConvQualified alice Nothing defNewProteusConv {newConvName = checked convName, newConvQualifiedUsers = [qbob, qcharlie]} - assertFailure ("Expected ConversationUpdateResponseUpdate but got " <> show err) - ConversationUpdateResponseNoChanges -> assertFailure "Expected ConversationUpdateResponseUpdate but got ConversationUpdateResponseNoChanges" - ConversationUpdateResponseUpdate up -> pure up - ConversationUpdateResponseNonFederatingBackends _ -> assertFailure "Expected ConversationUpdateResponseUpdate but got ConversationUpdateResponseNonFederatingBackends" - ConversationUpdateResponseUnreachableBackends _ -> assertFailure "Expected ConversationUpdateResponseUpdate but got ConversationUpdateResponseUnreachableBackends" - - liftIO $ do - cuOrigUserId cnvUpdate' @?= qbob - cuAlreadyPresentUsers cnvUpdate' @?= [qUnqualified qbob] - cuAction cnvUpdate' @?= action - - -- backend A generates a notification for alice - void $ - WS.awaitMatch (5 # Second) wsAlice $ \n -> do - liftIO $ wsAssertConvReceiptModeUpdate cnv qalice newReceiptMode n - - -- backend B does *not* get notified of the conversation update ony of bob's promotion - liftIO $ do - [(_fr, cUpdate)] <- mapM parseConvUpdate $ filter (\r -> frTargetDomain r == bdomain) federatedRequests - assertBool "Action is not a ConversationMemberUpdate" (isJust (getConvAction (sing @'ConversationMemberUpdateTag) (cuAction cUpdate))) - - -- conversation has been modified by action - updatedConv :: Conversation <- fmap responseJsonUnsafe $ getConvQualified alice cnv frTargetDomain r == cdomain) federatedRequests - - (_fr1, _cu1, _up1) <- assertOne $ mapMaybe (\(fr, up) -> getConvAction (sing @'ConversationMemberUpdateTag) (cuAction up) <&> (fr,up,)) dUpdates - - (_fr2, convUpdate, receiptModeUpdate) <- assertOne $ mapMaybe (\(fr, up) -> getConvAction (sing @'ConversationReceiptModeUpdateTag) (cuAction up) <&> (fr,up,)) dUpdates - - cruReceiptMode receiptModeUpdate @?= newReceiptMode - cuOrigUserId convUpdate @?= qbob - cuConvId convUpdate @?= qUnqualified cnv - cuAlreadyPresentUsers convUpdate @?= [qUnqualified qcharlie] - - WS.assertMatch_ (5 # Second) wsAlice $ \n -> do - wsAssertConvReceiptModeUpdate cnv qbob newReceiptMode n - where - _toOtherMember qid = OtherMember qid Nothing roleNameWireAdmin - _convView cnv usr = responseJsonUnsafeWithMsg "conversation" <$> getConv usr cnv - - parseConvUpdate :: FederatedRequest -> IO (FederatedRequest, ConversationUpdate) - parseConvUpdate rpc = do - frComponent rpc @?= Galley - frRPC rpc @?= "on-conversation-updated" - let convUpdate :: ConversationUpdate = fromRight (error $ "Could not parse ConversationUpdate from " <> show (frBody rpc)) $ A.eitherDecode (frBody rpc) - pure (rpc, convUpdate) - getConvAction :: Sing tag -> SomeConversationAction -> Maybe (ConversationAction tag) getConvAction tquery (SomeConversationAction tag action) = case (tag, tquery) of diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 79e6918c630..731857c70bb 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -93,7 +93,6 @@ tests s = [ test s "add user (not connected)" testAddUserNotConnected, test s "add client of existing user" testAddClientPartial, test s "add user with some non-MLS clients" testAddUserWithProteusClients, - test s "add remote user to a conversation" testAddRemoteUser, test s "add remote users to a conversation (some unreachable)" testAddRemotesSomeUnreachable, test s "return error when commit is locked" testCommitLock, test s "post commit that references an unknown proposal" testUnknownProposalRefCommit @@ -482,40 +481,6 @@ testProteusMessage = do welcomeMock) $ - sendAndConsumeCommitBundle commit - pure (events, reqs, qcnv) - - liftIO $ do - req <- assertOne $ filter ((== "on-conversation-updated") . frRPC) reqs - frTargetDomain req @?= qDomain bob - bdy <- case Aeson.eitherDecode (frBody req) of - Right b -> pure b - Left e -> assertFailure $ "Could not parse on-conversation-updated request body: " <> e - cuOrigUserId bdy @?= alice - cuConvId bdy @?= qUnqualified qcnv - cuAlreadyPresentUsers bdy @?= [qUnqualified bob] - cuAction bdy - @?= SomeConversationAction - SConversationJoinTag - ConversationJoin - { cjUsers = pure bob, - cjRole = roleNameWireMember - } - - liftIO $ do - event <- assertOne events - assertJoinEvent qcnv alice [bob] roleNameWireMember event - testAddRemotesSomeUnreachable :: TestM () testAddRemotesSomeUnreachable = do let bobDomain = Domain "bob.example.com" diff --git a/services/galley/test/integration/API/MessageTimer.hs b/services/galley/test/integration/API/MessageTimer.hs index 5900f3656e7..2e85dec5a8f 100644 --- a/services/galley/test/integration/API/MessageTimer.hs +++ b/services/galley/test/integration/API/MessageTimer.hs @@ -23,34 +23,19 @@ where import API.Util import Bilge hiding (timeout) import Bilge.Assert -import Control.Exception import Control.Lens (view) -import Data.Aeson (eitherDecode) -import Data.Domain -import Data.Id import Data.List1 -import Data.List1 qualified as List1 import Data.Misc import Data.Qualified -import Data.Singletons -import Federator.MockServer import Imports hiding (head) -import Network.HTTP.Types qualified as Http import Network.Wai.Utilities.Error import Test.Tasty import Test.Tasty.Cannon (TimeoutUnit (..), (#)) import Test.Tasty.Cannon qualified as WS -import Test.Tasty.HUnit import TestHelpers import TestSetup import Wire.API.Conversation -import Wire.API.Conversation.Action import Wire.API.Conversation.Role -import Wire.API.Event.Conversation -import Wire.API.Federation.API.Common -import Wire.API.Federation.API.Galley qualified as F -import Wire.API.Federation.Component -import Wire.API.Internal.Notification (Notification (..)) tests :: IO TestSetup -> TestTree tests s = @@ -63,8 +48,6 @@ tests s = ], test s "timer can be changed" messageTimerChange, test s "timer can be changed with the qualified endpoint" messageTimerChangeQualified, - test s "timer changes are propagated to remote users" messageTimerChangeWithRemotes, - test s "timer changes unavailable remotes" messageTimerUnavailableRemotes, test s "timer can't be set by conv member without allowed action" messageTimerChangeWithoutAllowedAction, test s "timer can't be set in 1:1 conversations" messageTimerChangeO2O, test s "setting the timer generates an event" messageTimerEvent @@ -143,86 +126,6 @@ messageTimerChangeQualified = do getConvQualified jane qcid !!! const timer1year === (cnvMessageTimer <=< responseJsonUnsafe) -messageTimerChangeWithRemotes :: TestM () -messageTimerChangeWithRemotes = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - let bob = qUnqualified qbob - connectWithRemoteUser bob qalice - - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR c bob $ \wsB -> do - (_, requests) <- - withTempMockFederator' (mockReply EmptyResponse) $ - putMessageTimerUpdateQualified bob qconv (ConversationMessageTimerUpdate timer1sec) - !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu - @?= SomeConversationAction (sing @'ConversationMessageTimerUpdateTag) (ConversationMessageTimerUpdate timer1sec) - - void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvMessageTimerUpdate - evtFrom e @?= qbob - evtData e @?= EdConvMessageTimerUpdate (ConversationMessageTimerUpdate timer1sec) - -messageTimerUnavailableRemotes :: TestM () -messageTimerUnavailableRemotes = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - let bob = qUnqualified qbob - connectWithRemoteUser bob qalice - - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR c bob $ \wsB -> do - (_, requests) <- - withTempMockFederator' (throw $ MockErrorResponse Http.status503 "Down for maintenance") $ - putMessageTimerUpdateQualified bob qconv (ConversationMessageTimerUpdate timer1sec) - !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu - @?= SomeConversationAction (sing @'ConversationMessageTimerUpdateTag) (ConversationMessageTimerUpdate timer1sec) - - void . liftIO . WS.assertMatch (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvMessageTimerUpdate - evtFrom e @?= qbob - evtData e @?= EdConvMessageTimerUpdate (ConversationMessageTimerUpdate timer1sec) - messageTimerChangeWithoutAllowedAction :: TestM () messageTimerChangeWithoutAllowedAction = do -- Create a team and a guest user diff --git a/services/galley/test/integration/API/Roles.hs b/services/galley/test/integration/API/Roles.hs index 4de83e1791f..1fdf61a1e4b 100644 --- a/services/galley/test/integration/API/Roles.hs +++ b/services/galley/test/integration/API/Roles.hs @@ -20,20 +20,14 @@ module API.Roles where import API.Util import Bilge hiding (timeout) import Bilge.Assert -import Control.Exception import Control.Lens (view) import Data.Aeson hiding (json) import Data.ByteString.Conversion (toByteString') -import Data.Domain import Data.Id import Data.List1 -import Data.List1 qualified as List1 import Data.Qualified import Data.Set qualified as Set -import Data.Singletons -import Federator.MockServer import Imports -import Network.HTTP.Types qualified as Http import Network.Wai.Utilities.Error import Test.Tasty import Test.Tasty.Cannon (TimeoutUnit (..), (#)) @@ -42,13 +36,7 @@ import Test.Tasty.HUnit import TestHelpers import TestSetup import Wire.API.Conversation -import Wire.API.Conversation.Action import Wire.API.Conversation.Role -import Wire.API.Event.Conversation -import Wire.API.Federation.API.Common -import Wire.API.Federation.API.Galley qualified as F -import Wire.API.Federation.Component -import Wire.API.Internal.Notification (Notification (..)) tests :: IO TestSetup -> TestTree tests s = @@ -56,10 +44,6 @@ tests s = "Conversation roles" [ test s "conversation roles admin (and downgrade)" handleConversationRoleAdmin, test s "conversation roles member (and upgrade)" handleConversationRoleMember, - test s "conversation role update with remote users present" roleUpdateWithRemotes, - test s "conversation role update with remote users present remotes unavailable" roleUpdateWithRemotesUnavailable, - test s "conversation access update with remote users present" accessUpdateWithRemotes, - test s "conversation role update of remote member" roleUpdateRemoteMember, test s "get all conversation roles" testAllConversationRoles, test s "access role update with v2" testAccessRoleUpdateV2, test s "test access roles of new conversations" testConversationAccessRole @@ -161,236 +145,6 @@ handleConversationRoleMember = do wsAssertMemberUpdateWithRole qcid qalice bob roleNameWireAdmin wireAdminChecks cid bob alice chuck -roleUpdateRemoteMember :: TestM () -roleUpdateRemoteMember = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - qcharlie <- Qualified <$> randomId <*> pure remoteDomain - let bob = qUnqualified qbob - - traverse_ (connectWithRemoteUser bob) [qalice, qcharlie] - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice, qcharlie]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR c bob $ \wsB -> do - (_, requests) <- - withTempMockFederator' (mockReply EmptyResponse) $ - putOtherMemberQualified - bob - qcharlie - (OtherMemberUpdate (Just roleNameWireMember)) - qconv - !!! const 200 === statusCode - - req <- assertOne requests - let mu = - MemberUpdateData - { misTarget = qcharlie, - misOtrMutedStatus = Nothing, - misOtrMutedRef = Nothing, - misOtrArchived = Nothing, - misOtrArchivedRef = Nothing, - misHidden = Nothing, - misHiddenRef = Nothing, - misConvRoleName = Just roleNameWireMember - } - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu - @?= SomeConversationAction (sing @'ConversationMemberUpdateTag) (ConversationMemberUpdate qcharlie (OtherMemberUpdate (Just roleNameWireMember))) - sort (F.cuAlreadyPresentUsers cu) @?= sort [qUnqualified qalice, qUnqualified qcharlie] - - liftIO . WS.assertMatch_ (5 # Second) wsB $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= MemberStateUpdate - evtFrom e @?= qbob - evtData e @?= EdMemberUpdate mu - - conv <- responseJsonError =<< getConvQualified bob qconv omQualifiedId m == qcharlie) (cmOthers (cnvMembers conv)) - liftIO $ - charlieAsMember - @=? Just - OtherMember - { omQualifiedId = qcharlie, - omService = Nothing, - omConvRoleName = roleNameWireMember - } - -roleUpdateWithRemotes :: TestM () -roleUpdateWithRemotes = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - qcharlie <- randomQualifiedUser - let bob = qUnqualified qbob - charlie = qUnqualified qcharlie - - connectUsers bob (singleton charlie) - connectWithRemoteUser bob qalice - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice, qcharlie]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR2 c bob charlie $ \(wsB, wsC) -> do - (_, requests) <- - withTempMockFederator' (mockReply EmptyResponse) $ - putOtherMemberQualified - bob - qcharlie - (OtherMemberUpdate (Just roleNameWireAdmin)) - qconv - !!! const 200 === statusCode - - req <- assertOne requests - let mu = - MemberUpdateData - { misTarget = qcharlie, - misOtrMutedStatus = Nothing, - misOtrMutedRef = Nothing, - misOtrArchived = Nothing, - misOtrArchivedRef = Nothing, - misHidden = Nothing, - misHiddenRef = Nothing, - misConvRoleName = Just roleNameWireAdmin - } - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu - @?= SomeConversationAction (sing @'ConversationMemberUpdateTag) (ConversationMemberUpdate qcharlie (OtherMemberUpdate (Just roleNameWireAdmin))) - F.cuAlreadyPresentUsers cu @?= [qUnqualified qalice] - - liftIO . WS.assertMatchN_ (5 # Second) [wsB, wsC] $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= MemberStateUpdate - evtFrom e @?= qbob - evtData e @?= EdMemberUpdate mu - -roleUpdateWithRemotesUnavailable :: TestM () -roleUpdateWithRemotesUnavailable = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - qcharlie <- randomQualifiedUser - let bob = qUnqualified qbob - charlie = qUnqualified qcharlie - - connectUsers bob (singleton charlie) - connectWithRemoteUser bob qalice - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice, qcharlie]} - let qconv = decodeQualifiedConvId resp - - WS.bracketR2 c bob charlie $ \(wsB, wsC) -> do - (_, requests) <- - withTempMockFederator' (throw $ MockErrorResponse Http.status503 "Down for maintenance") $ - putOtherMemberQualified - bob - qcharlie - (OtherMemberUpdate (Just roleNameWireAdmin)) - qconv - !!! const 200 === statusCode - - req <- assertOne requests - let mu = - MemberUpdateData - { misTarget = qcharlie, - misOtrMutedStatus = Nothing, - misOtrMutedRef = Nothing, - misOtrArchived = Nothing, - misOtrArchivedRef = Nothing, - misHidden = Nothing, - misHiddenRef = Nothing, - misConvRoleName = Just roleNameWireAdmin - } - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu - @?= SomeConversationAction (sing @'ConversationMemberUpdateTag) (ConversationMemberUpdate qcharlie (OtherMemberUpdate (Just roleNameWireAdmin))) - F.cuAlreadyPresentUsers cu @?= [qUnqualified qalice] - - liftIO . WS.assertMatchN_ (5 # Second) [wsB, wsC] $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= MemberStateUpdate - evtFrom e @?= qbob - evtData e @?= EdMemberUpdate mu - -accessUpdateWithRemotes :: TestM () -accessUpdateWithRemotes = do - c <- view tsCannon - let remoteDomain = Domain "alice.example.com" - qalice <- Qualified <$> randomId <*> pure remoteDomain - qbob <- randomQualifiedUser - qcharlie <- randomQualifiedUser - let bob = qUnqualified qbob - charlie = qUnqualified qcharlie - - connectUsers bob (singleton charlie) - connectWithRemoteUser bob qalice - resp <- - postConvWithRemoteUsers - bob - Nothing - defNewProteusConv {newConvQualifiedUsers = [qalice, qcharlie]} - let qconv = decodeQualifiedConvId resp - - let access = ConversationAccessData (Set.singleton CodeAccess) (Set.fromList [TeamMemberAccessRole, NonTeamMemberAccessRole, GuestAccessRole, ServiceAccessRole]) - WS.bracketR2 c bob charlie $ \(wsB, wsC) -> do - (_, requests) <- - withTempMockFederator' (mockReply EmptyResponse) $ - putQualifiedAccessUpdate bob qconv access - !!! const 200 === statusCode - - req <- assertOne requests - liftIO $ do - frTargetDomain req @?= remoteDomain - frComponent req @?= Galley - frRPC req @?= "on-conversation-updated" - Right cu <- pure . eitherDecode . frBody $ req - F.cuConvId cu @?= qUnqualified qconv - F.cuAction cu @?= SomeConversationAction (sing @'ConversationAccessDataTag) access - F.cuAlreadyPresentUsers cu @?= [qUnqualified qalice] - - liftIO . WS.assertMatchN_ (5 # Second) [wsB, wsC] $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= ConvAccessUpdate - evtFrom e @?= qbob - evtData e @?= EdConvAccessUpdate access - -- | Given an admin, another admin and a member run all -- the necessary checks targeting the admin wireAdminChecks :: diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index d329d88530a..db55284fb98 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -130,7 +130,6 @@ import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.API.Message import Wire.API.Message.Proto qualified as Proto -import Wire.API.Routes.FederationDomainConfig (FederationDomainConfig (..)) import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Internal.Galley.ConversationsIntra import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Multi @@ -149,7 +148,6 @@ import Wire.API.User.Auth hiding (Access) import Wire.API.User.Client import Wire.API.User.Client qualified as Client import Wire.API.User.Client.Prekey -import Wire.API.User.Search (FederatedUserSearchPolicy (..)) ------------------------------------------------------------------------------- -- API Operations @@ -1417,31 +1415,6 @@ deleteFederation dom = do delete $ g . paths ["/i/federation", toByteString' dom] -addFederation :: - (MonadHttp m, HasBrig m, MonadIO m) => - Domain -> - m ResponseLBS -addFederation dom = do - b <- viewBrig - post $ - b - . paths ["/i/federation/remotes"] - . json (FederationDomainConfig dom FullSearch) - -connectionRemovedFederation :: - (MonadHttp m, HasGalley m, MonadIO m) => - Domain -> - Domain -> - m ResponseLBS -connectionRemovedFederation origin target = do - g <- viewGalley - post $ - g - . paths ["federation", "on-connection-removed"] - . Bilge.content "application/json" - . header "Wire-Origin-Domain" (toByteString' origin) - . json target - putQualifiedAccessUpdate :: (MonadHttp m, HasGalley m, MonadIO m) => UserId -> @@ -1826,25 +1799,8 @@ assertFederationDeletedEvent :: Fed.Event -> IO () assertFederationDeletedEvent dom e = do - e @?= Fed.FederationDelete dom - -wsAssertFederationConnectionRemoved :: - HasCallStack => - Domain -> - Domain -> - Notification -> - IO () -wsAssertFederationConnectionRemoved domA domB n = do - ntfTransient n @?= False - assertFederationConnectionRemovedEvent domA domB $ List1.head (WS.unpackPayload n) - -assertFederationConnectionRemovedEvent :: - Domain -> - Domain -> - Fed.Event -> - IO () -assertFederationConnectionRemovedEvent domA domB e = do - e @?= Fed.FederationConnectionRemoved (domA, domB) + Fed._eventType e @?= Fed.FederationDelete + Fed._eventDomain e @?= dom -- FUTUREWORK: See if this one can be implemented in terms of: -- diff --git a/services/galley/test/integration/Federation.hs b/services/galley/test/integration/Federation.hs index 63d3fdb3ce6..666c557ad7d 100644 --- a/services/galley/test/integration/Federation.hs +++ b/services/galley/test/integration/Federation.hs @@ -2,54 +2,31 @@ module Federation where -import API.Util -import Bilge.Assert -import Bilge.Response import Cassandra qualified as C -import Cassandra.Exec (x1) -import Control.Lens (view, (^.)) +import Control.Lens ((^.)) import Control.Monad.Catch -import Control.Monad.Codensity (lowerCodensity) import Data.ByteString qualified as LBS import Data.Domain import Data.Id -import Data.List.NonEmpty -import Data.List1 qualified as List1 import Data.Qualified -import Data.Range (toRange) import Data.Set qualified as Set -import Data.Singletons -import Data.Time (getCurrentTime) import Data.UUID qualified as UUID -import Federator.MockServer -import Galley.API.Internal import Galley.API.Util import Galley.App -import Galley.Cassandra.Queries import Galley.Data.Conversation.Types qualified as Types -import Galley.Monad import Galley.Options -import Galley.Run -import Galley.Types.Conversations.Members (LocalMember (..), RemoteMember (..), defMemberStatus, localMemberToOther) +import Galley.Types.Conversations.Members (LocalMember (..), RemoteMember (..), defMemberStatus) import Imports -import Test.Tasty.Cannon (TimeoutUnit (..), (#)) -import Test.Tasty.Cannon qualified as WS import Test.Tasty.HUnit import TestSetup import UnliftIO.Retry import Wire.API.Conversation import Wire.API.Conversation qualified as Public -import Wire.API.Conversation.Action import Wire.API.Conversation.Protocol (Protocol (..)) -import Wire.API.Conversation.Role (roleNameWireAdmin, roleNameWireMember) -import Wire.API.Event.Conversation -import Wire.API.Federation.API.Brig (NonConnectedBackends (NonConnectedBackends)) -import Wire.API.Federation.API.Galley (ConversationUpdate (..), GetConversationsResponse (..)) -import Wire.API.Internal.Notification +import Wire.API.Conversation.Role (roleNameWireMember) import Wire.API.Routes.FederationDomainConfig import Wire.API.Routes.MultiTablePaging import Wire.API.Routes.MultiTablePaging qualified as Public -import Wire.API.User.Search x3 :: RetryPolicy x3 = limitRetries 3 <> exponentialBackoff 100000 @@ -83,223 +60,12 @@ isConvMemberLTests = do liftIO $ assertBool "Qualified UserId (local)" $ isConvMemberL lconv $ tUntagged lUserId liftIO $ assertBool "Qualified UserId (remote)" $ isConvMemberL lconv $ tUntagged rUserId -updateFedDomainsTestNoop' :: TestM () -updateFedDomainsTestNoop' = do - s <- ask - let opts = s ^. tsGConf - -- Don't need the actual server, and we certainly don't want it running. - -- But this is how the env is made, so it is what we do - (_, env) <- liftIO $ lowerCodensity $ mkApp opts - -- Common variables. - -- FUTUREWORK, NEWTICKET: These uuid strings side step issues with the tests hanging. - -- FUTUREWORK, NEWTICKET: Figure out the underlying issue as to why these tests occasionally hang. - let interval = (maxBound :: Int) `div` 2 -- Very large values so that we don't have to worry about automatic updates - remoteDomain = Domain "far-away.example.com" - remoteDomain2 = Domain "far-away-two.example.com" - liftIO $ assertBool "remoteDomain is different to local domain" $ remoteDomain /= opts ^. settings . federationDomain - liftIO $ assertBool "remoteDomain2 is different to local domain" $ remoteDomain2 /= opts ^. settings . federationDomain - -- Setup a conversation for a known remote domain. - -- Include that domain in the old and new lists so - -- if the function is acting up we know it will be - -- working on the domain. - updateFedDomainsTestNoop env remoteDomain interval - -updateFedDomainsTestAddRemote' :: TestM () -updateFedDomainsTestAddRemote' = do - s <- ask - let opts = s ^. tsGConf - -- Don't need the actual server, and we certainly don't want it running. - -- But this is how the env is made, so it is what we do - (_, env) <- liftIO $ lowerCodensity $ mkApp opts - -- Common variables. - let interval = (maxBound :: Int) `div` 2 -- Very large values so that we don't have to worry about automatic updates - remoteDomain = Domain "far-away.example.com" - remoteDomain2 = Domain "far-away-two.example.com" - liftIO $ assertBool "remoteDomain is different to local domain" $ remoteDomain /= opts ^. settings . federationDomain - liftIO $ assertBool "remoteDomain2 is different to local domain" $ remoteDomain2 /= opts ^. settings . federationDomain - - -- Adding a new federation domain, this too should be a no-op - updateFedDomainsAddRemote env remoteDomain remoteDomain2 interval - -updateFedDomainsTestRemoveRemoteFromLocal' :: TestM () -updateFedDomainsTestRemoveRemoteFromLocal' = do - s <- ask - let opts = s ^. tsGConf - -- Don't need the actual server, and we certainly don't want it running. - -- But this is how the env is made, so it is what we do - (_, env) <- liftIO $ lowerCodensity $ mkApp opts - -- Common variables. - let interval = (maxBound :: Int) `div` 2 -- Very large values so that we don't have to worry about automatic updates - remoteDomain = Domain "far-away.example.com" - remoteDomain2 = Domain "far-away-two.example.com" - liftIO $ assertBool "remoteDomain is different to local domain" $ remoteDomain /= opts ^. settings . federationDomain - liftIO $ assertBool "remoteDomain2 is different to local domain" $ remoteDomain2 /= opts ^. settings . federationDomain - - -- Remove a remote domain from local conversations - updateFedDomainRemoveRemoteFromLocal env remoteDomain remoteDomain2 interval - -updateFedDomainsTestRemoveLocalFromRemote' :: TestM () -updateFedDomainsTestRemoveLocalFromRemote' = do - s <- ask - let opts = s ^. tsGConf - -- Don't need the actual server, and we certainly don't want it running. - -- But this is how the env is made, so it is what we do - (_, env) <- liftIO $ lowerCodensity $ mkApp opts - -- Common variables. - let interval = (maxBound :: Int) `div` 2 -- Very large values so that we don't have to worry about automatic updates - remoteDomain = Domain "far-away.example.com" - remoteDomain2 = Domain "far-away-two.example.com" - liftIO $ assertBool "remoteDomain is different to local domain" $ remoteDomain /= opts ^. settings . federationDomain - liftIO $ assertBool "remoteDomain2 is different to local domain" $ remoteDomain2 /= opts ^. settings . federationDomain - - -- Remove a local domain from remote conversations - updateFedDomainRemoveLocalFromRemote env remoteDomain interval - fromFedList :: FederationDomainConfigs -> Set Domain fromFedList = Set.fromList . fmap domain . remotes -deleteFederationDomains :: FederationDomainConfigs -> FederationDomainConfigs -> App () -deleteFederationDomains old new = do - let prev = fromFedList old - curr = fromFedList new - deletedDomains = Set.difference prev curr - env <- ask - -- Call into the galley code - for_ deletedDomains $ liftIO . evalGalleyToIO env . deleteFederationDomain (toRange $ Proxy @500) - constHandlers :: (MonadIO m) => [RetryStatus -> Handler m Bool] constHandlers = [const $ Handler $ (\(_ :: SomeException) -> pure True)] -updateFedDomainRemoveRemoteFromLocal :: Env -> Domain -> Domain -> Int -> TestM () -updateFedDomainRemoveRemoteFromLocal env remoteDomain remoteDomain2 interval = recovering x3 constHandlers $ const $ do - let new = FederationDomainConfigs AllowDynamic [FederationDomainConfig remoteDomain2 FullSearch] interval - old = new {remotes = FederationDomainConfig remoteDomain FullSearch : remotes new} - qalice <- randomQualifiedUser - bobId <- randomId - charlieId <- randomId - let alice = qUnqualified qalice - remoteBob = Qualified bobId remoteDomain - remoteCharlie = Qualified charlieId remoteDomain2 - -- Create a local conversation - conv <- postConv alice [] (Just "remote gossip") [] Nothing Nothing - let qConvId = decodeQualifiedConvId conv - connectWithRemoteUser alice remoteBob - connectWithRemoteUser alice remoteCharlie - _ <- withTempMockFederator' ("get-not-fully-connected-backends" ~> NonConnectedBackends mempty) $ postQualifiedMembers alice (remoteCharlie <| remoteBob :| []) qConvId - -- Remove the remote user from the local domain - liftIO $ runApp env $ deleteFederationDomains old new - -- Check that the conversation still exists. - getConvQualified alice qConvId !!! do - const 200 === statusCode - let findRemote :: Qualified UserId -> Conversation -> Maybe (Qualified UserId) - findRemote u = find (== u) . fmap omQualifiedId . cmOthers . cnvMembers - -- Check that only one remote user was removed. - const (Right Nothing) === (fmap (findRemote remoteBob) <$> responseJsonEither) - const (Right $ pure remoteCharlie) === (fmap (findRemote remoteCharlie) <$> responseJsonEither) - const (Right qalice) === (fmap (memId . cmSelf . cnvMembers) <$> responseJsonEither) - -updateFedDomainRemoveLocalFromRemote :: Env -> Domain -> Int -> TestM () -updateFedDomainRemoveLocalFromRemote env remoteDomain interval = recovering x3 constHandlers $ const $ do - c <- view tsCannon - let new = FederationDomainConfigs AllowDynamic [] interval - old = new {remotes = FederationDomainConfig remoteDomain FullSearch : remotes new} - -- Make our users - qalice <- randomQualifiedUser - qbob <- Qualified <$> randomId <*> pure remoteDomain - let alice = qUnqualified qalice - update = memberUpdate {mupHidden = Just False} - -- Create a remote conversation - -- START: code from putRemoteConvMemberOk - qconv <- Qualified <$> randomId <*> pure remoteDomain - connectWithRemoteUser alice qbob - - fedGalleyClient <- view tsFedGalleyClient - now <- liftIO getCurrentTime - let cu = - ConversationUpdate - { cuTime = now, - cuOrigUserId = qbob, - cuConvId = qUnqualified qconv, - cuAlreadyPresentUsers = [], - cuAction = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qalice) roleNameWireMember) - } - void $ runFedClient @"on-conversation-updated" fedGalleyClient remoteDomain cu - -- Expected member state - let memberAlice = - Member - { memId = qalice, - memService = Nothing, - memOtrMutedStatus = mupOtrMuteStatus update, - memOtrMutedRef = mupOtrMuteRef update, - memOtrArchived = Just True == mupOtrArchive update, - memOtrArchivedRef = mupOtrArchiveRef update, - memHidden = Just True == mupHidden update, - memHiddenRef = mupHiddenRef update, - memConvRoleName = roleNameWireMember - } - -- Update member state & verify push notification - WS.bracketR c alice $ \ws -> do - putMember alice update qconv !!! const 200 === statusCode - void . liftIO . WS.assertMatch (5 # Second) ws $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= qconv - evtType e @?= MemberStateUpdate - evtFrom e @?= qalice - case evtData e of - EdMemberUpdate mis -> do - assertEqual "otr_muted_status" (mupOtrMuteStatus update) (misOtrMutedStatus mis) - assertEqual "otr_muted_ref" (mupOtrMuteRef update) (misOtrMutedRef mis) - assertEqual "otr_archived" (mupOtrArchive update) (misOtrArchived mis) - assertEqual "otr_archived_ref" (mupOtrArchiveRef update) (misOtrArchivedRef mis) - assertEqual "hidden" (mupHidden update) (misHidden mis) - assertEqual "hidden_ref" (mupHiddenRef update) (misHiddenRef mis) - x -> assertFailure $ "Unexpected event data: " ++ show x - - -- Fetch remote conversation - let bobAsLocal = - LocalMember - (qUnqualified qbob) - defMemberStatus - Nothing - roleNameWireAdmin - let mockConversation = - mkProteusConv - (qUnqualified qconv) - (qUnqualified qbob) - roleNameWireMember - [localMemberToOther remoteDomain bobAsLocal] - remoteConversationResponse = GetConversationsResponse [mockConversation] - (rs, _) <- - withTempMockFederator' - (mockReply remoteConversationResponse) - $ getConvQualified alice qconv - responseJsonUnsafe rs - liftIO $ do - assertBool "user" (isJust alice') - let newAlice = fromJust alice' - assertEqual "id" (memId memberAlice) (memId newAlice) - assertEqual "otr_muted_status" (memOtrMutedStatus memberAlice) (memOtrMutedStatus newAlice) - assertEqual "otr_muted_ref" (memOtrMutedRef memberAlice) (memOtrMutedRef newAlice) - assertEqual "otr_archived" (memOtrArchived memberAlice) (memOtrArchived newAlice) - assertEqual "otr_archived_ref" (memOtrArchivedRef memberAlice) (memOtrArchivedRef newAlice) - assertEqual "hidden" (memHidden memberAlice) (memHidden newAlice) - assertEqual "hidden_ref" (memHiddenRef memberAlice) (memHiddenRef newAlice) - -- END: code from putRemoteConvMemberOk - - -- Remove the remote user from the local domain - liftIO $ runApp env $ deleteFederationDomains old new - convIds <- - liftIO $ - C.runClient (env ^. cstate) $ - C.retry x1 $ - C.query selectUserRemoteConvs (C.params C.LocalQuorum (pure alice)) - case find (== qUnqualified qconv) $ snd <$> convIds of - Nothing -> pure () - Just c' -> liftIO $ assertFailure $ "Found conversation where none was expected: " <> show c' - pageToConvIdPage :: Public.LocalOrRemoteTable -> C.PageWithState (Qualified ConvId) -> Public.ConvIdsPage pageToConvIdPage table page@C.PageWithState {..} = Public.MultiTablePage @@ -307,67 +73,3 @@ pageToConvIdPage table page@C.PageWithState {..} = mtpHasMore = C.pwsHasMore page, mtpPagingState = Public.ConversationPagingState table (LBS.toStrict . C.unPagingState <$> pwsState) } - -updateFedDomainsAddRemote :: Env -> Domain -> Domain -> Int -> TestM () -updateFedDomainsAddRemote env remoteDomain remoteDomain2 interval = do - s <- ask - let opts = s ^. tsGConf - localDomain = opts ^. settings . federationDomain - old = FederationDomainConfigs AllowDynamic [FederationDomainConfig remoteDomain FullSearch] interval - new = old {remotes = FederationDomainConfig remoteDomain2 FullSearch : remotes old} - -- Just check against the domains, as the search - -- strategies are outside of this testing scope - newDoms = domain <$> new.remotes - oldDoms = domain <$> old.remotes - liftIO $ assertBool "old and new are different" $ oldDoms /= newDoms - liftIO $ assertBool "old is shorter than new" $ Imports.length oldDoms < Imports.length newDoms - liftIO $ assertBool "new contains old" $ all (`elem` newDoms) oldDoms - liftIO $ assertBool "new elements not in old" $ any (`notElem` oldDoms) newDoms - qalice <- randomQualifiedUser - bobId <- randomId - let alice = qUnqualified qalice - remoteBob = Qualified bobId remoteDomain - -- Create a conversation - - conv <- postConv alice [] (Just "remote gossip") [] Nothing Nothing - -- liftIO $ assertBool ("conv = " <> show conv) False - let convId = decodeConvId conv - let qConvId = Qualified convId localDomain - connectWithRemoteUser alice remoteBob - _ <- withTempMockFederator' ("get-not-fully-connected-backends" ~> NonConnectedBackends mempty) $ postQualifiedMembers alice (remoteBob :| []) qConvId - - -- No-op - liftIO $ runApp env $ deleteFederationDomains old new - -- Check that the conversation still exists. - getConvQualified (qUnqualified qalice) (Qualified convId localDomain) !!! do - const 200 === statusCode - let findRemote :: Conversation -> Maybe (Qualified UserId) - findRemote = find (== remoteBob) . fmap omQualifiedId . cmOthers . cnvMembers - const (Right $ pure remoteBob) === (fmap findRemote <$> responseJsonEither) - const (Right qalice) === (fmap (memId . cmSelf . cnvMembers) <$> responseJsonEither) - -updateFedDomainsTestNoop :: Env -> Domain -> Int -> TestM () -updateFedDomainsTestNoop env remoteDomain interval = do - s <- ask - let opts = s ^. tsGConf - localDomain = opts ^. settings . federationDomain - old = FederationDomainConfigs AllowDynamic [FederationDomainConfig remoteDomain FullSearch] interval - new = old - qalice <- randomQualifiedUser - bobId <- randomId - let alice = qUnqualified qalice - remoteBob = Qualified bobId remoteDomain - -- Create a conversation - convId <- decodeConvId <$> postConv alice [] (Just "remote gossip") [] Nothing Nothing - let qConvId = Qualified convId localDomain - connectWithRemoteUser alice remoteBob - _ <- withTempMockFederator' ("get-not-fully-connected-backends" ~> NonConnectedBackends mempty) $ postQualifiedMembers alice (remoteBob :| []) qConvId - -- No-op - liftIO $ runApp env $ deleteFederationDomains old new - -- Check that the conversation still exists. - getConvQualified (qUnqualified qalice) (Qualified convId localDomain) !!! do - const 200 === statusCode - let findRemote :: Conversation -> Maybe (Qualified UserId) - findRemote = find (== remoteBob) . fmap omQualifiedId . cmOthers . cnvMembers - const (Right $ pure remoteBob) === (fmap findRemote <$> responseJsonEither) - const (Right qalice) === (fmap (memId . cmSelf . cnvMembers) <$> responseJsonEither) diff --git a/services/galley/test/integration/Run.hs b/services/galley/test/integration/Run.hs index e6b8a31d5ce..a890e1004f7 100644 --- a/services/galley/test/integration/Run.hs +++ b/services/galley/test/integration/Run.hs @@ -98,13 +98,6 @@ main = withOpenSSL $ runTests go mempty (pathsConsistencyCheck . treeToPaths . compile $ Galley.API.sitemap), API.tests setup, - testGroup - "Federation Domains" - [ test setup "No-Op" updateFedDomainsTestNoop', - test setup "Add Remote" updateFedDomainsTestAddRemote', - test setup "Remove Remote From Local" updateFedDomainsTestRemoveRemoteFromLocal', - test setup "Remove Local From Remote" updateFedDomainsTestRemoveLocalFromRemote' - ], test setup "isConvMemberL" isConvMemberLTests ] getOpts gFile iFile = do diff --git a/services/spar/default.nix b/services/spar/default.nix index 4bd791dfcfe..e5b1d9bd105 100644 --- a/services/spar/default.nix +++ b/services/spar/default.nix @@ -41,6 +41,7 @@ , MonadRandom , mtl , network-uri +, openapi3 , optparse-applicative , polysemy , polysemy-check @@ -53,10 +54,9 @@ , saml2-web-sso , servant , servant-multipart +, servant-openapi3 , servant-server -, servant-swagger , silently -, swagger2 , tasty-hunit , text , text-latin1 @@ -211,14 +211,14 @@ mkDerivation { metrics-wai mtl network-uri + openapi3 polysemy polysemy-plugin polysemy-wire-zoo QuickCheck saml2-web-sso servant - servant-swagger - swagger2 + servant-openapi3 time tinylog types-common diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index 568034cedde..124cbb93a99 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -618,15 +618,15 @@ test-suite spec , metrics-wai , mtl , network-uri + , openapi3 , polysemy , polysemy-plugin , polysemy-wire-zoo , QuickCheck , saml2-web-sso >=0.19 , servant - , servant-swagger + , servant-openapi3 , spar - , swagger2 , time , tinylog , types-common diff --git a/services/spar/test/Arbitrary.hs b/services/spar/test/Arbitrary.hs index d8b2daf6839..44d8f38ddac 100644 --- a/services/spar/test/Arbitrary.hs +++ b/services/spar/test/Arbitrary.hs @@ -24,8 +24,8 @@ module Arbitrary where import Data.Aeson import Data.Id (TeamId, UserId) +import Data.OpenApi hiding (Header (..)) import Data.Proxy -import Data.Swagger hiding (Header (..)) import Imports import SAML2.WebSSO.Test.Arbitrary () import SAML2.WebSSO.Types diff --git a/services/spar/test/Test/Spar/APISpec.hs b/services/spar/test/Test/Spar/APISpec.hs index 07bfdb8fde3..a82d00c40f6 100644 --- a/services/spar/test/Test/Spar/APISpec.hs +++ b/services/spar/test/Test/Spar/APISpec.hs @@ -27,7 +27,7 @@ import Data.Metrics.Servant (routesToPaths) import Data.Metrics.Test (pathsConsistencyCheck) import Data.Proxy (Proxy (Proxy)) import Imports -import Servant.Swagger (validateEveryToJSON) +import Servant.OpenApi (validateEveryToJSON) import Spar.API as API import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy) import Test.QuickCheck (property) diff --git a/tools/fedcalls/default.nix b/tools/fedcalls/default.nix index 133e6e886bd..2d9d10e326d 100644 --- a/tools/fedcalls/default.nix +++ b/tools/fedcalls/default.nix @@ -3,15 +3,16 @@ # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. { mkDerivation -, aeson , base , containers , gitignoreSource , imports , insert-ordered-containers , language-dot +, lens , lib -, swagger2 +, openapi3 +, text , wire-api }: mkDerivation { @@ -21,13 +22,14 @@ mkDerivation { isLibrary = false; isExecutable = true; executableHaskellDepends = [ - aeson base containers imports insert-ordered-containers language-dot - swagger2 + lens + openapi3 + text wire-api ]; description = "Generate a dot file from swagger docs representing calls to federated instances"; diff --git a/tools/fedcalls/fedcalls.cabal b/tools/fedcalls/fedcalls.cabal index a7bf9ac1981..615a8bbd151 100644 --- a/tools/fedcalls/fedcalls.cabal +++ b/tools/fedcalls/fedcalls.cabal @@ -63,13 +63,14 @@ executable fedcalls -rtsopts -Wredundant-constraints -Wunused-packages build-depends: - aeson - , base + base , containers , imports , insert-ordered-containers , language-dot - , swagger2 + , lens + , openapi3 + , text , wire-api default-language: GHC2021 diff --git a/tools/fedcalls/src/Main.hs b/tools/fedcalls/src/Main.hs index c1b4471da9f..387424fde9c 100644 --- a/tools/fedcalls/src/Main.hs +++ b/tools/fedcalls/src/Main.hs @@ -23,23 +23,13 @@ module Main where import Control.Exception (assert) -import Data.Aeson as A -import Data.Aeson.Types qualified as A +import Control.Lens import Data.HashMap.Strict.InsOrd qualified as HM +import Data.HashSet.InsOrd (InsOrdHashSet) import Data.Map qualified as M -import Data.Swagger - ( PathItem, - Swagger, - _operationExtensions, - _pathItemDelete, - _pathItemGet, - _pathItemHead, - _pathItemOptions, - _pathItemPatch, - _pathItemPost, - _pathItemPut, - _swaggerPaths, - ) +import Data.OpenApi +import Data.OpenApi.Lens qualified as S +import Data.Text qualified as T import Imports import Language.Dot as D import Wire.API.Routes.API @@ -65,7 +55,7 @@ calls = assert (calls' == nub calls') calls' where calls' = mconcat $ parse <$> swaggers -swaggers :: [Swagger] +swaggers :: [OpenApi] swaggers = [ -- TODO: introduce allSwaggerDocs in wire-api that collects these for all -- services, use that in /services/brig/src/Brig/API/Public.hs instead of @@ -101,37 +91,63 @@ data MakesCallTo = MakesCallTo ------------------------------ -parse :: Swagger -> [MakesCallTo] -parse = +parse :: OpenApi -> [MakesCallTo] +parse oapi = mconcat - . fmap parseOperationExtensions + . fmap (parseOperationExtensions allTags) . mconcat . fmap flattenPathItems . HM.toList - . _swaggerPaths + $ oapi ^. S.paths + where + allTags = oapi ^. S.tags + +-- Simple aliases to help track which field is what +type RPC = String + +type Component = String -- | extract path, method, and operation extensions -flattenPathItems :: (FilePath, PathItem) -> [((FilePath, String), HM.InsOrdHashMap Text Value)] +flattenPathItems :: (FilePath, PathItem) -> [((FilePath, String), InsOrdHashSet TagName)] flattenPathItems (path, item) = filter ((/= mempty) . snd) $ catMaybes - [ ((path, "get"),) . _operationExtensions <$> _pathItemGet item, - ((path, "put"),) . _operationExtensions <$> _pathItemPut item, - ((path, "post"),) . _operationExtensions <$> _pathItemPost item, - ((path, "delete"),) . _operationExtensions <$> _pathItemDelete item, - ((path, "options"),) . _operationExtensions <$> _pathItemOptions item, - ((path, "head"),) . _operationExtensions <$> _pathItemHead item, - ((path, "patch"),) . _operationExtensions <$> _pathItemPatch item + [ ((path, "get"),) . view S.tags <$> _pathItemGet item, + ((path, "put"),) . view S.tags <$> _pathItemPut item, + ((path, "post"),) . view S.tags <$> _pathItemPost item, + ((path, "delete"),) . view S.tags <$> _pathItemDelete item, + ((path, "options"),) . view S.tags <$> _pathItemOptions item, + ((path, "head"),) . view S.tags <$> _pathItemHead item, + ((path, "patch"),) . view S.tags <$> _pathItemPatch item ] -parseOperationExtensions :: ((FilePath, String), HM.InsOrdHashMap Text Value) -> [MakesCallTo] -parseOperationExtensions ((path, method), hm) = uncurry (MakesCallTo path method) <$> findCallsFedInfo hm +parseOperationExtensions :: InsOrdHashSet Tag -> ((FilePath, String), InsOrdHashSet TagName) -> [MakesCallTo] +parseOperationExtensions allTags ((path, method), hm) = + uncurry (MakesCallTo path method) <$> findCallsFedInfo allTags hm -findCallsFedInfo :: HM.InsOrdHashMap Text Value -> [(String, String)] -findCallsFedInfo hm = case A.parse parseJSON <$> HM.lookup "wire-makes-federated-call-to" hm of - Just (A.Success (fedcalls :: [(String, String)])) -> fedcalls - Just bad -> error $ "invalid extension `wire-makes-federated-call-to`: expected `[(comp, name), ...]`, got " <> show bad - Nothing -> [] +-- Given a set of tags, and a set of tag names for an operation, +-- parse out the RPC calls and their components +findCallsFedInfo :: InsOrdHashSet Tag -> InsOrdHashSet TagName -> [(Component, RPC)] +findCallsFedInfo allTags = mapMaybe extractStrings . toList + where + magicPrefix :: Text + magicPrefix = "wire-makes-federated-call-to-" + extractStrings :: TagName -> Maybe (Component, RPC) + extractStrings tagName = + tag >>= \t -> + (,) + -- Extract the name and description, and drop everything that is empty + -- This gives us the component name, and as a route may call the same component + -- multiple times, it has to go into the description so it isn't dropped by the set. + <$> fmap T.unpack t._tagDescription + -- Strip off the magic string from the tag names, and drop empty results + -- This also implicitly filters for results that start with the prefix. + -- This gives us the RPC name, as that will be unique for each route, and it + -- doesn't matter if it is set multiple times and dropped in the set, as it + -- still describes that Fed call is made. + <*> fmap T.unpack (T.stripPrefix magicPrefix t._tagName) + where + tag = find (\t -> t._tagName == tagName) allTags ------------------------------ @@ -158,7 +174,7 @@ mkDotGraph inbound = Graph StrictGraph DirectedGraph Nothing (mods <> nodes <> e itemSourceNode (MakesCallTo path method _ _) = method <> " " <> path itemTargetNode :: MakesCallTo -> String - itemTargetNode (MakesCallTo _ _ comp name) = "[" <> comp <> "]:" <> name + itemTargetNode (MakesCallTo _ _ comp rpcName) = "[" <> comp <> "]:" <> rpcName callingNodes :: Map String Integer callingNodes = diff --git a/tools/stern/default.nix b/tools/stern/default.nix index c8c64c0d784..3a5afeaa844 100644 --- a/tools/stern/default.nix +++ b/tools/stern/default.nix @@ -28,16 +28,16 @@ , lib , metrics-wai , mtl +, openapi3 , optparse-applicative , random , retry , schema-profunctor , servant +, servant-openapi3 , servant-server -, servant-swagger , servant-swagger-ui , split -, swagger2 , tagged , tasty , tasty-hunit @@ -78,13 +78,13 @@ mkDerivation { lens metrics-wai mtl + openapi3 schema-profunctor servant + servant-openapi3 servant-server - servant-swagger servant-swagger-ui split - swagger2 text tinylog transformers diff --git a/tools/stern/src/Stern/API/Routes.hs b/tools/stern/src/Stern/API/Routes.hs index f3e7116d514..aae30de2805 100644 --- a/tools/stern/src/Stern/API/Routes.hs +++ b/tools/stern/src/Stern/API/Routes.hs @@ -32,14 +32,14 @@ import Data.Aeson qualified as A import Data.Handle import Data.Id import Data.Kind +import Data.OpenApi qualified as S import Data.Schema qualified as Schema -import Data.Swagger qualified as S import Imports hiding (head) import Network.HTTP.Types.Status import Network.Wai.Utilities import Servant hiding (Handler, WithStatus (..), addHeader, respond) -import Servant.Swagger (HasSwagger (toSwagger)) -import Servant.Swagger.Internal.Orphans () +import Servant.OpenApi (HasOpenApi (toOpenApi)) +import Servant.OpenApi.Internal.Orphans () import Servant.Swagger.UI import Stern.Types import Wire.API.CustomBackend @@ -455,7 +455,7 @@ type SwaggerDocsAPI = SwaggerSchemaUI "swagger-ui" "swagger.json" swaggerDocs :: Servant.Server SwaggerDocsAPI swaggerDocs = swaggerSchemaUIServer $ - toSwagger (Proxy @SternAPI) + toOpenApi (Proxy @SternAPI) & S.info . S.title .~ "Stern API" & cleanupSwagger diff --git a/tools/stern/src/Stern/Types.hs b/tools/stern/src/Stern/Types.hs index b62994c943d..f8ed807492a 100644 --- a/tools/stern/src/Stern/Types.hs +++ b/tools/stern/src/Stern/Types.hs @@ -30,10 +30,10 @@ import Data.Aeson import Data.Aeson.TH import Data.ByteString.Conversion import Data.Json.Util +import Data.OpenApi qualified as Swagger import Data.Proxy import Data.Range import Data.Schema qualified as S -import Data.Swagger qualified as Swagger import Galley.Types.Teams import Imports import Servant.API @@ -127,7 +127,7 @@ instance Swagger.ToSchema ConsentLog where declareNamedSchema _ = pure . Swagger.NamedSchema (Just "ConsentLog") $ mempty - & Swagger.type_ ?~ Swagger.SwaggerObject + & Swagger.type_ ?~ Swagger.OpenApiObject & Swagger.description ?~ "(object structure is not specified in this schema)" newtype ConsentValue = ConsentValue @@ -152,7 +152,7 @@ instance Swagger.ToSchema ConsentLogAndMarketo where declareNamedSchema _ = pure . Swagger.NamedSchema (Just "ConsentLogAndMarketo") $ mempty - & Swagger.type_ ?~ Swagger.SwaggerObject + & Swagger.type_ ?~ Swagger.OpenApiObject & Swagger.description ?~ "(object structure is not specified in this schema)" newtype UserMetaInfo = UserMetaInfo @@ -164,7 +164,7 @@ instance Swagger.ToSchema UserMetaInfo where declareNamedSchema _ = pure . Swagger.NamedSchema (Just "UserMetaInfo") $ mempty - & Swagger.type_ ?~ Swagger.SwaggerObject + & Swagger.type_ ?~ Swagger.OpenApiObject & Swagger.description ?~ "(object structure is not specified in this schema)" newtype InvoiceId = InvoiceId {unInvoiceId :: Text} diff --git a/tools/stern/stern.cabal b/tools/stern/stern.cabal index 0a4be042c59..4cb3eff82ea 100644 --- a/tools/stern/stern.cabal +++ b/tools/stern/stern.cabal @@ -91,13 +91,13 @@ library , lens >=4.4 , metrics-wai >=0.3 , mtl >=2.1 + , openapi3 , schema-profunctor , servant + , servant-openapi3 , servant-server - , servant-swagger , servant-swagger-ui , split >=0.2 - , swagger2 , text >=1.1 , tinylog >=0.10 , transformers >=0.3